Redis 中ZREVRANGEBYSCORE(zrangeByScoreWithScores) 使用不当导致的分页 Bug!

JAVA herman 1726浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog,发送下载链接帮助你免费下载!
本博客日IP超过1800,PV 2600 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog,之前的微信号好友位已满,备注:返现
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

最近我们的电商系统中在大促时出现了一个分页 Bug,问题产生的原因就是 Redis 的 ZREVRANGEBYSCORE 指令使用不当导致的。表现在 java 代码中就是 zrangeByScoreWithScores 方法使用不当。本文来说说这个 Sorted Set 有序集合实现分页产生的 Bug 问题。

首先,我们线上的电商系统正在进行一个大促活动。买家非常的多,导致一时间产生了非常多的评论。开发人员在存储(保存)评论时,使用 Redis 中的 SortSet 这个数据结构。每新增一条评论,他都通过 zadd 命令将起保存至 Redis 和 MySQL 中,查询时直接在 Redis 中取出来。代码抽象一下,如下所示:

boolean operated;
try (Jedis jedis = pool.getResource()) {
    operated = jedis.zadd(COMMUNITY_PRAISE + commentId, 
    	-System.currentTimeMillis() / 1000, 
    	accountId) == 1;
}
if (operated) {
    commentDao.incrPraiseCount(comment);
}

顺便说一下,System.currentTimeMillis() 是毫秒,System.currentTimeMillis() / 1000 就相当于精确到秒。

这个活动是大促,参与活动的需要评论。活动比预期的热,导致同一秒的评论就比较多,导致 zrangeByScoreWithScores 在取出评论列表时,遇到同一秒的超过 10 条的评论,就会出现重复数据。

取出评论列表的逻辑大致抽象如下:

Set<Tuple> praisedIds = jedis.zrangeByScoreWithScores(COMMUNITY_PRAISE + id, 
	lastScore, 0, 0, 10);
List<Comment> result = new ArrayList<>();
for (Tuple praisedId : praisedIds) {
	// 其他逻辑
    result.add(comment);
}
return result;

看到没?他每次拿 lastScore 记录,也就是上一页评论的最后一条数据的时间。来获取新的 10 条评论数据。由于同一秒评论的记录超过 10 条后,导致每次 APP 每次下拉刷新获取到重复数据的概率比较高。于是就有客户投诉,看评论中的数据都是一样的。无论怎么刷新都是 10 条一样的数据,和上一页的数据一样。

这个 zrangeByScoreWithScores 其实就是 Redis 中的 ZREVRANGEBYSCORE 指令。完整指令如下:

ZREVRANGEBYSCORE key max min WITHSCORES LIMIT offset count

Redis zrevrangebyscore 指令

其中这个 limit 是可以分页的。这个分页对下拉刷新也有一个问题,那就是在你下拉刷新时,刚好新增了几个评论,所以刷新出来的数据也可能有重复的数据。

后来这个程序员针对这个问题有改了一下,每次拿到 lastScore 后加 1。我看到这个代码后,又找他了,这个做法肯定不行的。也就是说你每次拿到上一页最后一条的 lastScore 后,再加 1。就相当与时间上加了 1 秒,中间肯定会漏掉一些数据的。

那么该怎么办呢?如果你想使用 lastScore 进行分页,就必须保证它唯一。不然就有概率发生刷新列表时,出现重复数据。

Redis 在时事新闻中的应用

我们这个和时事新闻还有些不一样,新闻的写入时间重复概率没有我们这个高。尤其是我们在大促的活动当中。

最后说一下这个问题的解决办法。一种就是保证 lastScore 唯一,或者说时间在进一步精确。还有一种就是 lastScore + limit 的组合来实现分页。或者就是单纯的使用 limit 来实现,但是也要注意不能刷新出重复数据。

SortSet 结构

我们的做法,最终是参考了上图。

将每个主题的 topicId 作为 set 的 key,将与该主题关联的评论的 createDate 和 commentId 分别作为 set 的 score 和 member,commentId 的顺序就根据 createDate 的大小进行排列。

当需要查询某个主题某一页的评论时,就可主题的 topicId 通过指令 zrevrange topicId (page-1)×10 (page-1)×10+perPage 这样就能找出某个主题下某一页的按时间排好顺序的所有评论的 commintId。page 为查询第几页的页码,perPage 为每页显示的条数。

当找到所有评论的 commentId 后,就可以把这些 commentId 作为 key 去 Hash 结构中去查询该条评论对应的内容。

这样就利用 SortSet 和 Hash 两种结构在 Redis 中达到了分页和排序的目的。

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加QQ1群:135430763(2000人群已满),QQ2群:454796847(已满),QQ3群:187424846(已满)。QQ群进群密码:xttblog,想加微信群的朋友,之前的微信号好友已满,请加博主新的微信号:xttblog,备注:“xttblog”,添加博主微信拉你进群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作可添加助理微信进行沟通!

本文原文出处:业余草: » Redis 中ZREVRANGEBYSCORE(zrangeByScoreWithScores) 使用不当导致的分页 Bug!