点赞系统介绍
一个评论的点赞数越高,排名也就越靠前,热度也随之更高
用户回答和评论的欲望就会增加,网站的活跃度也会越来越高
点赞功能在电商、社交、游戏等几乎所有的互联网项目中都广泛使用
其中蕴含着很多的技术方案,最重要的就是如何处理点赞的高并发
系统设计规范
一个通用点赞系统需要满足下列特性:
-
通用:点赞业务在设计的时候不要与业务系统耦合,必须同时支持不同业务的点赞功能
-
独立:点赞功能是独立系统,并且不依赖其它服务,这样才具备可迁移性
-
并发:一些热点业务点赞会很多,所以点赞功能必须支持高并发
-
安全:要做好并发安全控制,避免重复点赞
技术栈
Maven、SpingCloud、RabbitMQ、MybatisPlus、xxl-job、Redis
数据库表设计
点赞的数据结构分两部分
一是点赞记录:谁给什么点赞的记录
二是与业务关联的点赞数:每一个业务都有自己的一个点赞数
字段
点赞记录表字段:主键id、用户id、业务id、业务类型、创建时间、更新时间
系统需求接口
点赞按钮有灰色
和点亮
两种状态。也就是说需要实现查询用户点赞状态的接口
这样前端才能根据点赞状态渲染不同效果,要实现的接口包括:
• 点赞/取消点赞
• 查询是否点赞
第一次点击是点赞,按钮会高亮
第二次点击是取消,点赞按钮变灰
返回值设计:
• 方案一:无返回值,200就是成功,页面直接把点赞数+1展示给用户即可
• 方案二:返回点赞数量,页面渲染
这里推荐使用方案一,因为方案二每次统计点赞数量也有很大的性能消耗。
一也有缺陷,两个人同时点赞或取消都会加减一,不会加减二,没有及时的同步数据
点赞业务的几点需求:
-
用户不能重复点赞
-
点赞就新增一条点赞记录,取消点赞就删除记录
-
点赞数由具体的业务方保存,需要通知业务方更新点赞数
由于业务方的类型很多,比如互动问答、笔记、课程等
所以通知方式必须是低耦合的,这里建议使用MQ来实现。
整体实现思路
增删点赞逻辑
点赞系统发送
调用流程
根据前端传入的参数判断走点赞还是取消逻辑
点赞或取消
失败直接结束
点赞或取消
成功则统计当前业务点赞数
使用rabbitMqHelper将返回结果投递给MQ
rabbitMqHelper.send(交换机,routingKey,业务id,点赞数)
雪花算法
每个业务的业务id是由雪花算法生成的
因为在微服务项目中或者多业务存储场景中
使用雪花算法可以避免不同业务相同id的出现
比如评论业务和笔记业务存储在同一个点赞记录表中
点赞操作
先查询当前用户id是否对当前业务id点过赞,统计存在记录,返回count
如果点了count>0,则直接返回false结束,点赞失败
若没点过则保存一条记录,从ThreadLocal中拿用户id
取消点赞
直接根据业务id和用户id删除记录即可
业务系统监听
在业务微服务中建立一个MQ监听类,使用同样的实体类接收数据,并保存到数据库中
此时数据库两张表被更改
一张多一条记录,一张点赞数加一
查询是否点赞
在点赞和取消操作之前,需要提前查询当前用户是否给当前显示业务点过赞
返回值是所有点过赞的业务id的集合
使用Fegin远程调用remark微服务,接收一批id参数,返回一批点过赞的id
高并发的优化
暴露问题
每一次点赞都伴随4次数据库的读写
点赞操作波动较大,会出现点赞量瞬间激增的情况
对于数据库是一个巨大的压力
改进思路
高并发读优化:1.优化sql和代码 2.添加缓存 3.页面静态化
高并发写优化:1.优化sql和代码 2.同步写变为异步写 3.合并写请求
合并写请求是将60条redis数据1分钟向mysql保存一次
缺点是数据存在1分钟的延迟,非必要场景可以接受
最终优化方案
优化逻辑
提交点赞后,首先去redis中查询是否点过赞
点过直接结束,没有点过赞则向redis新增一条记录
新增后在redis中查询一次点赞数
查询结果再保存在redis中,并通过定时任务和MQ向mysql同步数据
数据存在一定时间的延迟
数据结构
点赞数量缓存的时候,需要存3个参数来适配MQ的参数列表,所以需要redis中能存3个值的数据类型
Hash:key-hashkey-hashvalue 没有排序
Zset:key-value-source 有source的自动排序,有跳表的索引,空间占用大
缓存点赞记录:Set
缓存点赞数量:ZSet
Redis优化实现
redis中有新增覆盖机制,可以不必查询直接保存
如果数据不存在则直接保存成功,并返结果
如果数据存在则保存失败,并返回结果
调用流程
根据前端传入的参数判断走点赞还是取消逻辑
点赞或取消
失败直接结束
点赞或取消
成功则统计当前业务点赞数
将点赞数保存到redis中
使用ZSet组装key、value、source
使用redisTemplate新增数据
//组装和新增 String likedKey = "likes:biz:" + recordDTO.getBizId(); Long source = redisTemplate.opsForSet().size(likedKey); String value = recordDTO.getBizId().toString(); String key = "likes:times:type:" + recordDTO.getBizType(); redisTemplate.opsForZSet().add(key, value, source);
新增点赞记录
组装参数 key---likes:biz:业务id
组装参数 value---ThreadLocal中拿到用户id
使用redisTemplate新增记录add
Long num = redisTemplate.opsForSet().add(key,value)
获取返回值num判断不为空不为0则成功
删除点赞记录
组装参数 key---likes:biz:业务id
组装参数 value---ThreadLocal中拿到用户id
使用redisTemplate新增记录remove
Long num = redisTemplate.opsForSet().remove(key,value)
获取返回值num判断不为空不为0则成功
查询是否点赞
从redis查询用户点过赞的业务,查询点赞记录
判断当前业务id为key所对应的集合中是否有当前用户
使用redisTemplate的isMember()方法判断
redisTemplate.opsForSet().isMember(likedKey, UserContext.getUser().toString()
定时同步数据
XXL-JOB定时任务:配置文件中xxl-job的配置
在点赞微服务中新增一个定时任务的类,同步所有的点赞数的缓存记录
准备一个集合装载所有的Key,遍历这个集合
使用redisTemplate的popMin()方法得到每个key对应的集合对象tuples
遍历tuples集合进行数据转换
循环内准备一个实体类集合装载数据转换后的对象集合
将当前这个实体类集合通过RabbitMQ投递到队列中
业务微服务监听队列,准备一个集合参数接收数据
定义并开启一个定时任务执行器