场景题:点赞系统设计

刷到此贴的友友春招/暑期必上岸!!!

前言

鼠鼠在秋招的过程中多次被问到场景题,中大厂的考察频率相当之高,一般会放在最后一个问题用来拖时间,也遇到过上来就问你怎么设计一个系统(面试官以此来决定后面对你的态度)。所以鼠鼠准备开这个场景题栏目,分享在秋招过程中遇到的场景题以及如何进行回答,感兴趣和感觉有帮助的友友点个关注和赞吧,你们的点赞和关注是鼠鼠持续更新下去的最大动力!!!

对于场景题,鼠鼠觉得拿到一道题,首先要思考的是业务逻辑,然后就是在这个业务上会有多大的qps请求量,面试官经常会对你设计的方案和系统提出高并发/大流量的情况下会出现什么问题,你如何去解决,从而考察你设计系统的高可用性和系统性。

话不多说开启今天的主题,点赞系统设计吧!!!

大家在刷牛客的时候,刷到优质的帖子会点赞加收藏,认为这个博主有不错的产出还会点一个关注(暗示友友们给我点个免费的关注和赞嘛,球球了)那么点赞其实也就成了牛客的核心功能之一,因为每日用户这么多的情况下,点赞所承受的并发其实是相当高的。而且点赞无论是对贴主还是看客,都是比较重要的,涉及到发帖的质量和自身的曝光度。一个大V的帖子,其点赞量,其实对后台的系统的压力,数据库的压力是很具有考验的。

在这种情况下应该如何设计点赞系统?那么关于系统设计题,我在上篇文章里已经提到了一些通用的方法论,感兴趣的小伙伴可以去看一看,当然我这里也贴心的贴在这里,帮助大家更好的理解。

(1)明晰项目诉求:点赞系统即为存储每一个用户发布的帖子有哪些人点赞了,总的点赞数是多少;每一个用户具体点赞了哪些帖子,赞过的帖子总数是多少(其实这个逻辑友友们点开自己的牛客收藏和点赞其实就一目了然了)整体上是一个多对多的关系。同时关注请求量,是否需要做高并发/高可用的扩展设计;此外对实时性是否也要要求,追求强一致性还是最终一致性

(2)确定业务逻辑:对于每个帖子,需要记录点赞的用户,点赞的总人数,取消点赞时点赞数要相应减1,用户查看自己发布的帖子需要显示总点赞数,以及展示一个点赞用户的列表(简单概括为增删改查)。同理,对于观看帖子的用户,需要记录用户点赞了哪些帖子,用户点赞帖子的总数,在我的 - 点赞页面需要展示出用户点赞过的帖子……

(3)架构设计:存储(MySQL/Redis)+服务(是否需要拆分为多个服务)

友友们会发现,整体的业务逻辑并不是很复杂,但是因为这是面试题,面试官就会问你在高并发的情况下你设计的系统会有什么问题。这其实也是我回答场景题的惯用套路,根据请求量一步一步迭代自己的设计,我们在实际生产过程中也同样是这样一个步骤,先思考最简单的实现,然后一步一步迭代,直到可以抗住高并发,实现高可用(满足面试官的要求

业务逻辑梳理清楚了,接下来我们就来到存储与服务设计了,因为点赞信息是比较重要的,所以这里还是考虑使用MySQL做持久化存储。(做过黑马点评的友友可能直接就会想到Redis做存储,利用incrby 自增命令快速实现点赞功能,这当然是可以的,但是从系统设计和信息丢失的角度来看,先从MySQL入手可以保证数据的安全性,也更能体现面试者的系统设计能力,逐步优化的一个思想)

数据表的设计

点赞记录表

1)type,也就是业务类型,你是为视频点赞还是为评论点赞,还是为其他业务类型点赞

2)作者ID

3)作品ID

4)userID:点赞人ID

5)点赞时间

6)点赞动作(1 like/ 2 unlike)

点赞计数表

1)type,也就是业务类型,你是为视频点赞还是为评论点赞,还是为其他业务类型点赞

2)作者ID

3)作品ID

4)点赞数量

5)取消点赞数量

以上是我的点赞表的设计思路,还有其他的点赞表设计,大家可以根据自己的理解进行修改。 在请求量不大的情况下,可以直接用MySQL承载。后端服务会处理点赞的业务逻辑,然后将点赞记录存储到数据库。

点赞的业务逻辑

1 )前端用户发起点赞请求到达后端服务。后端服务会判断表内是否有之前的点赞记录,因为前端用户可能会重复的去点击点赞按钮。

2.1 )如果没有记录就插入点赞记录,之后点赞计数表加一 2.2 )如果表内有了用户对作品的点赞记录,就去比较点赞的时间

3.1)如果时间小于表内记录的时间,说明当前的请求是之前的请求,丢掉就可以了

3.2)如果是大于表内的记录的点赞时间,就继续判断状态

4.1)如果已经是点赞状态了,说明就是重复的点赞请求,就丢掉就可以了

4.2)如果是取消点赞的状态,就更新记录为点赞状态即可,同时可以增加点赞计数表的数量加一 整个点赞的逻辑就处理完了。

流程图如下:

取消点赞的逻辑

1)当前端用户取消点赞,后端服务检查计数表内是否有对应用户对该作品的记录。

2.1)如果没有,说明之前没有点赞过,直接丢到返回前端即可。 2.2)如果有记录,继续判断时间,如果时间小于数据库内记录时间,说明是之前的请求,直接丢掉就可以了。

3.1)如果是大于表内记录时间,说明就继续判断记录的点赞状态。

3.2)如果表内是取消点赞的状态,说明是重复请求了,直接丢到就可以了。

4)如果表内是点赞状态,更改为取消状态,之后点赞数减一即可。

流程图如下:

并发问题

整个过程是有并发的问题,需要加分布式锁。

点赞过程中的并发问题

1)重复插入问题:在并发情况下,可能会有多个点赞请求同时到达后端,并且都检查到数据库中没有该用户对该作品的点赞记录,然后都执行插入操作,导致重复插入点赞记录。

2)计数不准确问题:当多个点赞请求并发执行时,可能会出现点赞计数表加一的操作不是原子性的情况。例如,两个并发请求同时读取了点赞计数表中的值为 10,然后各自执行加一操作并更新回数据库,最终结果可能是 11 而不是正确的 12。

取消点赞过程中的并发问题

1)重复减一问题:类似于点赞过程中的计数不准确问题,当多个取消点赞请求并发执行时,如果点赞数减一的操作不是原子性的,可能会导致点赞数被多减。例如,当前点赞数为 5,两个并发的取消点赞请求同时读取点赞数并执行减一操作,最终结果可能是 4 而不是正确的 3。

2)状态更新不一致问题:在并发环境下,可能会出现两个请求同时判断点赞状态为点赞,然后一个请求先将其更新为取消状态并减一,另一个请求再更新时可能会出现错误的结果。

可以采用数据库事务来确保操作的原子性和一致性,或者使用分布式锁来保证同一时间只有一个请求能执行点赞或取消点赞的操作。同时,在数据库设计上,可以考虑使用乐观锁或悲观锁机制来防止数据冲突。

查询点赞列表和点赞数

在获取作品点赞数时,通常有两种方式:一种是逐个获取每个作品的点赞数,另一种是通过批量接口一次性获取多个作品的点赞数。

逐个获取作品点赞数:如果有多个作品需要获取点赞数,每次都单独向服务器发送请求获取一个作品的点赞数,那么每一次请求都会产生一次网络 IO。例如,要获取 10 个作品的点赞数,就需要进行 10 次网络请求,这会消耗较多的网络资源和时间。

使用批量接口获取点赞数:批量接口允许在一次请求中传递多个作品的标识(如作品 ID),服务器接收到请求后,会一次性处理这些作品的点赞数查询,并将结果返回。这样,无论要获取多少个作品的点赞数,都只需要一次网络请求。例如,同样是获取 10 个作品的点赞数,通过批量接口,只需一次网络请求就能完成,相比逐个获取,大大减少了网络 IO 的次数,提高了数据获取的效率,通过批量接口依次获取来减少网络IO的次数,节省了网络带宽和请求时间。

引入Redis作为缓存

当流量大起来以后,此时数据库的读写锁的竞争就会加剧。考虑加入缓存层Redis。

既然引入了缓存,那么面试官肯定会问到一个经典的问题,那就是:缓存和数据库的一致性是如何解决的,这里有多种解决方案,但常用的其实也就是两种:先更新数据库,再删除缓存;通过canle订阅binlog日志,再更新缓存,实现最终一致性。

关于缓存和数据库的一致性后面我也会专门出一个专题分享给大家。

引入Redis作为缓存后,所有的写请求还是会先写入到数据库,读请求会优先到达缓存层去检查数据,没有再去数据库内加载数据填充到缓存中。

Redis中的数据存储

Redis会存储两类数据:存储点赞列表和点赞数

1)点赞列表:使用zset结构,key由 userid:type 组成,表示的含义即为某个用户执行点赞/评论的作品

value就是作品ID+时间戳的set的集合,基于zset可以根据时间戳进行排序 注意zset的长度是有限制的,如果无限制的往zset内添加数据就可能会有big key的问题,可能会阻塞Redis的主线程,影响正常的请求。

2)点赞数:使用string结构即可。key是作品ID,value是具体的点赞数

业务逻辑

查询点赞数:先是去Redis查看某个作品的点赞数,如果存在直接返回给客户端,如果不存在则查询数据库计数表内的数据。然后存储到Redis内,最后返回给客户端数量即可。

点赞列表查询:首先还是会去Redis内查询点赞列表,如果不存在,去mysql里查询,将数据写回到Redis的zset内即可。这里依然要注意zset长度,最后会返回给客户端就可以了。如果存在就进行zset分页(类似于MySQL的分页查询,不用直接返回所有的数据,而是根据分数,这里是时间戳,查询所需要的页面数据即可)。如果页面超过了zset分页范围,超出的部分就会去数据库内查询,拼接结果返回客户端即可。如果未超过zset长度限制,直接返回当前页的数据给前端就可以了。

对于写请求,前端用户发起点赞请求,设计到缓存的更新。这里建议采用canal监听mysql的binlog。根据binlog数据变化,更新缓存内的数据。

涉及到两个操作。一个是更新点赞记录,一个是更新点赞数量,由于点赞记录用的是zset处理,并且是有长度限制的。如果超过长度限制,要删除掉多余的元素。整个操作不是原子的,所以用lua脚本来保证整个过程的原子性。

没有采用直接删除缓存类的数据因为点赞是一个很频繁的动作,如果频繁的删除数据,其实会频繁的去数据库加载数据,缓存的命中率其实就会很低,实际上并没有给数据库降低压力。

架构升级

随着业务数据的上升,单机的mysql面临着严重的考验,mysql处理io的能力是有限的,此时考虑到mysql的分库分表来分担单库的磁盘压力,单库的压力。但分库分表其实是会带来系统的复杂性的,还有就是大V数据分布不均匀的问题。当然这里我们只是从系统架构的角度去一步步迭代设计,大家对于分库分表的常见问题也需要有了解,后面我也会出专门的文章来给大家做分享

那么不做分库分表的话,运用三板斧之一的消息队列同样也可以通过异步消息来为数据库来消峰填谷。

那么这里既然使用到了MQ,那么相关的消息不丢失以及消息消费的幂等性问题也就随之而来了,因为本文主题是点赞系统的设计,这些八股问题我们留到后面的文章中再做解释。

当写请求到达时候直接发送到mq,然后返回给前端,点赞成功即可。mq之后就是消费任务,可以订阅点赞的topic进行消费,然后进行点赞的业务逻辑,存入数据库和Redis。

新的架构模式下的业务逻辑

1)一个点赞请求过来的时候,消息方法MQ后可以直接返回客户端点赞成功了,后续由MQ异步的处理写数据库和缓存的操作,这里为了保证消息不丢失可以采用同步发送,将消息持久化到磁盘上。 2)消费任务监听消息的到达进行点赞写入的业务逻辑。这里的点赞还是和前面的初步设计时是一样的,这里不过多赘述了。

3)按照我们之前的架构是先写数据库,然后由canal监听binlog日志的变化,最后更新Redis缓存

总结:

相信大家看完这篇文章对点赞系统这样一个高并发系统的设计已经有了一个初步的印象了。

这里总结概括一下就是:先考虑流量不大的情况,设计出初步的业务逻辑,当流量大起来以后,考虑使用Redis作为缓存,这里考虑缓存和数据库的一致性问题;如果Redis也扛不住了,那就再加上消息队列,进行流量削峰,同时通过异步调用提高客户端的响应速度;最后考虑到数据量庞大的问题,采用分库分表来解决存储和读写效率问题。

大家以后遇到高并发系统就可以围绕这样一个大致思路去作答。这里还有网关/限流等概念没有涉及到,点个关注吧,后续的文章会陆续补充欧,希望可以为你的面试带来帮助!

PS:

其实在整个分析过程中大家可以发现,场景题其实就会把我们背的那些八股和技术运用起来,所以在学习场景题的时候就可以把八股文进行问题,有点像单词背不住就去读阅读文章,在读文章的时候记住八股文,在上面的分析过程中我也有几处进行了随机的八股提问。扫码登录这个过程里MySQL,Redis,分库分表,MQ用的很多,那友友们是不是可以顺带复习一下相关八股呢?

……

好了如果大家有什么问题的话欢迎来评论区交流。包括但不限于文章创作改正意见,后续分享内容(面经,知识输出,经验分享等等),都看到这了,点个免费的关注和赞不过分吧

#大家都开始春招面试了吗##我发现了面试通关密码##暑期实习 ##春招##场景题##八股#

全部评论
这流程图好眼熟啊,ds 吗
点赞 回复 分享
发布于 04-09 22:25 北京
mark一下
点赞 回复 分享
发布于 04-09 11:32 天津
mark大佬
点赞 回复 分享
发布于 04-01 16:51 湖北
最近被场景题问懵了,为什么都要拷打我一个没有任何实习经历的菜b啊
点赞 回复 分享
发布于 04-01 16:44 上海
mark一下
点赞 回复 分享
发布于 03-28 19:41 河北

相关推荐

评论
34
108
分享

创作者周榜

更多
牛客网
牛客企业服务