一、点赞功能整体剖析
在各类社交、电商等项目里,都具有点赞功能,为确保其能便捷地融入不同项目的多样化业务场景,点赞功能的实现应该和其他模块相互独立,即实现解耦。如此一来,后续有需求时,只需将精心设计好的点赞微服务模块嵌入项目即可,使其具备良好的可移植特性。与此同时,一个通用性强的点赞功能还得拥有应对高并发读写请求的出色能力。
1.直接写入Mysql
直接写入Mysql是最简单的做法。
做两个表即可,
1、post_like
记录文章被赞的次数,已有多少人赞过这种数据就可以直接从表中查到;
2、user_like_post
记录用户赞过了哪些内容, 当打开文章列表时,显示的有没有赞过的数据就在这里面;
缺点
1、数据库读写压力大
热门文章会有很多用户点赞,甚至是短时间内被大量点赞, 直接操作数据库从长久来看不是很理想的做法。
二、基于 MYSQL 与 MQ 搭建点赞功能架构
要打造一套完备且实用的点赞功能体系,像避免同一用户对同一条信息重复点赞,以及让每条数据能依据点赞数降序排列等功能实现,需存储两类关键数据以备后续调用。
一方面,构建用户点赞记录表,该表负责存储每一条点赞详情。每当用户点赞成功,相应数据便录入此表;若用户取消点赞,对应记录随即从表中移除。通过这种方式,后续就能据此判断当前用户是否已对特定业务数据点过赞。为达成此功能,杜绝用户重复点赞同一业务数据的情况,表内需涵盖用户 id、业务数据 id、业务类型等字段,凭借这些字段组合,便能精准知晓 “何人” 对 “何事” 进行了点赞操作。
另一方面,设立点赞数表,用以呈现每条业务数据的总点赞数,方便前端展示页面信息。鉴于点赞数与具体业务数据紧密挂钩,可选择在需要点赞功能的业务数据中新增点赞数字段,所有涉及点赞数操作的业务只需针对该字段进行处理即可。这意味着点赞数由具体业务方负责维护,一旦该字段需更新,由于点赞功能牵涉众多业务方类型,可借助 MQ(消息队列)以低耦合的方式实现通知功能。预先在各个微服务中部署监听器,每当点赞或取消点赞致使点赞数改变,便向交换机发送 MQ 通知,交换机再依据需求把消息转发至对应的队列,以供不同微服务接收并处理消息,完成数据更新。
整个业务流程可参照相应图示。
此外,由于用户的点赞记录统一存储于点赞微服务内,点赞微服务还需对外公开一个 Feign 接口,以供其他微服务调用,用于判断某用户是否已对特定业务数据点赞。用户 id 通常可从 ThreadLocal 中获取,所以调用接口时仅需设置待查询业务数据的 id 参数,点赞微服务接收到请求后,前往用户点赞表中检索匹配信息。
三、引入 Redis
redis存储随后批量刷回数据库
redis主要的特点就是快, 毕竟主要数据都在内存嘛;
另外为啥我选择redis而不是memcache的主要原因在于redis支持更多的数据类型, 例如hash, set, zset等。
下面具体的会用到这几个类型。
优点
1、性能高
2、缓解数据库读写压力
其实我更多的在于缓解写压力, 真的读压力, 通过mysql主从甚至通过加入redis对热点数据做缓存都可以解决,
写压力对于前面的方案确实是不大好使。
缺点
1、开发复杂
这个比直接写mysql的方案要复杂很多, 需要考虑的地方也很多;
2、不能保证数据安全性
redis挂掉的时候会丢失数据, 同时不及时同步redis中的数据, 可能会在redis内存置换的时候被淘汰掉;
不过对于我们点赞而已, 稍微丢失一点数据问题不大;
具体设计
Mysql设计
这一块和写入写mysql是一样的,毕竟是要落地存储的。
所以还是同样的需要post_like
, user_like_post
这两表存储文章被点赞的个数(等统计), 用户对那些文章点了赞(取消赞)。
这两表分别通过post_id
, user_id
进行关联。
redis设计部分:
post_set
在redis中弄一个set存放所有被点赞的文章
post_user_like_set_{$post_id}
对每个post以post_id作为key, 搞一个set存放所有对该post点赞的用户;
post_user_like_{$post_id}_{$user_id}
将每个用户对每个post的点赞情况放到一个hash里面去, hash的字段就
随意跟进需求来处理就行了。
为啥用hash
只所以用hash是因为完全可以用hash来存储一个点赞的对象, 对应数据库的一行记录。
当然有同学会说用key, value也可以, 将所有的数据序列化(json_encode
等)
后全部放到value里面去。 反复序列化也是一个很大的开销不是, hash可以很
方便的修改某个字段, 而序列化和反序列化的操作。
post_{$post_id}_counter
对每个post维护一个计数器, 用来记录当前在redis中的点赞数,
这里我们只用counter记录尚未同步到mysql中的点赞数(可以为负), 每次
刷回mysql中时将counter中的数据和数据库已有的赞数相加即可。
用户点赞/取消赞
获取user_id
, post_id
, 查询该用户是否已经点过赞, 已点过则不允许再次点赞,
或者设计为前端允许用户点, 只是后台不重复计算;
这里需要注意的是用户点赞的记录可能在数据库中, 也可能在缓存中, 所以查询的时候
缓存和数据库都要查询, 缓存没有再查询数据库。
将用户的点赞/取消赞的情况记录在redis中, 具体为:
1、写入post_set
将post_id
写入post_set
2、写入post_user_like_set_{$post_id}
将user_id
写入post_user_like_set_{$post_id}
3、写入post_user_like_{$post_id}_{$user_id}
将用户点赞数据, 例如赞状态, post_id, user_id, ctime(操作时间), mtime(修改时间)写入post_user_like_{$post_id}_{$user_id}
中
4、更新post_{$post_id}_counter
更新post_{$post_id}_counter
, 这里的更新稍晚复杂一点, 需要和前面一样先获取当前用户是否对这个post点过赞
如果点过, 并且本次是取消赞, counter减一, 如果没点过, 本次是点赞, counter加一。
如果原来是取消赞的情况, 本次是点赞, counter加一。
同步刷回数据库
循环从post_set
中pop出来一个post_id
至到空
根据{$post_id}
, 每次从post_user_like_set_{$post_id}
中pop出来一个user_id
直到空
根据post_id
, user_id
, 直接获取对应的hash表的内容(post_user_like_{$post_id}_{$user_id}
将hash表中的数据写入user_like_post
表中
将post_{$post_id}_counter
中的数据和post_like
中的数据相加, 将结果写入到post_like
表中
页面展示
1、查询用户点赞情况
前面已经说过, 需要同时查询redis和mysql
2、查询post点赞统计
同样需要查询redis中的post_{$post_id}_counter
和mysql的post_like
表, 并将两者相加
得到的结果才是正确的结果
总结
解决了mysql读写的问题
但没有针对用户量较大的场景考虑分表的设计, 可以考虑针对user_id或者post_id进行分表
凭借 MYSQL 与 MQ 组合,基本能实现点赞功能需求,但 MYSQL 在应对高并发场景时存在显著短板,难以契合点赞功能的高并发特性。况且在上述实现流程中,一次点赞业务往往涉及多次数据库读写操作,若点赞业务短期内访问量骤增,数据库将承受极大压力。
点赞业务涵盖高并发的读操作与写操作,可分别实施优化策略。针对并发读优化,既然选用 MYSQL,SQL 优化自是首要考量,同时,添加缓存不失为一种有效手段,能大幅减少数据库查询频次,有力提升系统在高并发环境下的性能表现,而缓存工具首推 Redis。在处理并发写请求时,除优化 SQL 外,还可尝试将同步写转换为异步写,或者把多次写请求合并后统一提交等操作。这些优化举措得以实施的前提是,用户对点赞功能的精准度要求相较于其他业务相对宽松,点赞数据的延迟刷新在用户可接受范围内。
综合上述两方面优化思路,最终优化后的点赞业务流程如下:
若要在 Redis 中缓存用户点赞数据,推荐采用 Set 数据结构。以业务数据 id 作为键(key),用户 id 作为值(value),如此一来,既能精准记录每条业务数据获得了哪些用户的点赞,又因 Set 数据结构不允许重复元素存在,同一用户对同一业务再次点赞时,插入操作会失败,完美契合业务规则。
至于点赞数量的缓存,可依据业务是否存在按点赞数排序需求,在 Hash 与 ZSet 之间抉择。若无需排序,优先选用存储空间占用较小的 Hash;若需按点赞数排序,则可考虑 ZSet,将点赞数存入其 score 字段,ZSet 便能自动依据点赞数完成排序,便捷高效。
四、点赞功能总结
综合权衡后,确定的点赞功能实施方案为:当产生点赞数据需要存储时,直接借助 Redis 缓存记录,摒弃 MYSQL 存储这部分数据的方式;而点赞数信息因与业务数据紧密关联,仍需运用 MYSQL 存储。数据发生变动时,优先修改 Redis 中的数据,最后编写一个定时任务,每隔特定时长向 MQ 发送消息,把 Redis 缓存中最新的点赞数同步至数据库,以此达成理想的点赞功能效果。