为了后面分享的顺利进行,我这里只讲几个需要用到的操作:
- 添加 member 命令格式:zadd key score member [score member ...]
- 增加 member 的 score 命令格式:zincrby key increment member
- 获取 member 排名命令格式:zrank/zrevrank key member
- 返回指定排名范围内的 member 命令格式:zrange/zrevrange key start end [withscores]
先看第一个:添加 member。
比如我们把示意图中的数据添加到到有序集合里面去,语法是这样的:
- zadd key score member [score member ...]
意思是可以一次添加一对或者多对 score-member,比如下面这两个命令:
- zadd sport:ranking:20210227 10026 why
- zadd sport:ranking:20210227 10158 mx 30169 les 48858 skr 66079 jay
执行之后,返回的数字代表添加成功的 member 个数。
我用专门操作 Redis 的 RDM 可视化工具来查看插入的数据,和我自己画的示意图相差无几:
接着看第二个:增加 member 的 score
微信运动排行榜的数据是实时更新的。
目前 member 为 why 的步数是 10268,假设我吃完晚饭出门跑步去了,又跑了 5000 步。
这时得更新我的步数,就用 zincrby 命令,语法是这样的:
- zincrby key increment member
对应上面场景的执行命令是这样的:
- zincrby sport:ranking:20210227 5000 why
执行完成后,会返回 why 的步数,可以看到从 10026 变成了 15026 :
所以我们只需要更新 score 就行了,至于排名的变化,Redis 会帮忙保证的。
然后看第三个命令:获取 member 排名
语法是这样的:
- 获取 member 排名:zrank key member
- 获取 member 排名:zrevrank key member
首先,排名都是 0 开始计算的。
zrank 是按照分数从低到高返回 member 排名。
zrevrank 是按照分数从高到低返回 member 排名。
比如现在要获取 jay 的排名,用 zrank 返回结果就是 4。
- zrank sport:ranking:20210227 jay
当用 zrevrank 时,jay 的排名就是 0:
- zrevrank sport:ranking:20210227 jay
所以,在微信步数排行榜的这个需求中,步数越多排名越靠前,我们应该用 zrevrank。
第四个需要掌握的命令是:返回指定排名范围内的 member。
- zrange/zrevrange key start end [withscores] 返回指定排名范围内的 member
这个命令就很关键了。
zrange 是按照 score 从低到高返回指定排名范围内的 member。
zrevrange 是按照 score 从高到低返回指定排名范围内的 member。
在这里,我只演示 zrevrange 的命令。
比如我要获取步数排名前三的 member:
- zrevrange sport:ranking:20210227 0 2
这个命令有个可选参数:withscores
当带上这个参数之后,会返回对应 member 的 score:
你想,这不就是排行榜 top N 的场景吗?
假设我现在要获取所有用户的排名,怎么写呢?
如下:
- zrevrange sport:ranking:20210227 0 -1
这就是当前的微信步数排行榜,jay 步数最多,mx 步数最少。
咦,怎么回事,排行榜好久就出来了呢?
你想想,讲完几个 API 操作,好像功能就实现了呢?
是的,确实是这样的,甚至我们只需要这两个 API 就能完成排行榜的需求:
- zadd key score member [score member ...] 添加 member
- zrange/zrevrange key start end [withscores] 返回指定排名范围内的 member
好了,如果大家喜欢的话,感谢大家一键三连。本次的文章就到这里了...
那是不可能的。
索然无味的 API 文章多没有意思啊。
虽然前面的部分我们已经可以基于 Redis 的有序集合加上几个简单的命令,就可以实现排行榜需求了。
但是前面只是铺垫,接下来,好戏才刚刚开始。
再次审视排行榜
上面的微信步数排行榜有个问题,你发现了吗?
就上面这个场景而言,所有人来看,看到的都是这样的排序:
而真实情况是,每个人看见的数据排行数据来源自己的微信好友,而微信好友各不相同,所以看到的排行榜也各不相同。
这个特性,我们并没有体现出来。
我们上面的场景更加类似于游戏排行榜,所有的人看到的全服排行榜都是一样的。
那么怎么保证我们每个人看到的各不相同呢?
你思考一下,该从什么角度去解决这个问题呢?
有序集合的 key 不同,就获取到不同的 value 集合。
我们当前的 key 是 sport:ranking:20210227,里面只包含了某一天的信息。
只要我们在 key 里面加上用户的属性就可以了,假设我的微信号是 why。
那么 key 可以设计为这样 sport:ranking:why:20210227。
这样,由于 key 里面多了用户信息,每个人的 key 都各不相同,就像这样的:
对应的命令如下:
- zadd sport:ranking:why:20210227 10026 why 10158 mx 30169 les 48858 skr 66079 jay
- zadd sport:ranking:mx:20210227 7688 赵四 9688 刘能 10026 why 10158 mx 54367 大脚
why 和 mx 看到的都是各自好友某一天的微信步数排行榜。
只要把 key 设计好了,这个问题就迎刃而解了。
但是你仔细思考一下,真的就迎刃而解了吗?
这个问题,我在写第一版的时候可能是被猪油蒙蔽了双眼,没发现。
有种“只缘身在此山中”的味道,一心想着 Redis 了。
你想,如果每个用户都有在redis有一个自己的排行榜,一个用户的分数更新的时候就需要对所有好友的zset更新,这多大的代价啊,对吧?
当以用户为纬度做排行榜的时候,就会出现排行榜巨多的情况,导致维护成本升高。
Redis能做,但不是最佳方案。
那么用什么方案去做呢?
我提个思路吧:
每个用户看到的排行榜不一样,我们其实不用时时刻刻帮用户维护好排行榜。
维护好了,用户还不一定来看,出力不讨好的节奏。
所以还不如延迟到用户请求的阶段。
当用户请求查看排行榜的时候,再去根据用户的好友关系,循环获取好友的步数,生成排行榜。
具体方案,大家自己思考一下吧。
另外多说一嘴,前段时间不是微信支持了修改微信号吗,赢得一大片叫好声。
其实我当时认真的想了一下,从技术上的实现来说这个需求到底有多难。
我不知道有没有历史技术债务在里面。
但是就说当前这个场景,key 里面包含了微信号,注意是微信号,不是微信昵称。
因为在设计之初,产品打包票说:放心,微信号绝对全局唯一,一旦确定,不可变更。
结果呢,现在要变化了。
产品屁颠屁颠的说:怎么实现我不管,这个需求用户呼吁很大,赶紧上线。
你说,对这些类似场景的冲击有多大?
其实冲击也不算特别大,一个字段的变化而已。
但是,微信 14 亿用户啊。
一个简单的需求,涉及到这个体量之后,就一句话:
量变引起质变。
好了,好了,扯远了。说回来。
当我把目光再次放到微信排行榜上的时候,我发现,其实我只是给了一个阉割版的排行榜。
是的,我们现在可以获取到 why 的当前步数是 1680 步,当前排名是 814 名。
比如还是沿用上面的例子,假设现在要获取我的微信好友 jay 的微信步数排行榜情况。
先获取 jay 的名次:
- zrevrank sport:ranking:why:20210227 jay
名次为 0,程序里面可以对其进行加一操作。就是第一名了。
接着获取 jay 的今日步数:
- zscore sport:ranking:why:20210227 jay
66079,步数也有了。
现在我们知道了:why 的好友 jay 今日运动步数 66079 步,在 why 的微信好友中排第一名。
但是你仔细看,这上面我还漏了两个字段:
- 微信头像
- 朋友点赞个数
两个字段应该怎么放呢?
放数据库里面当然可以,但是我们主要还是说一下 Redis 的解决方案。
这个时候其实我们想要存储的是 User 对象,对象里面有这几个字段:昵称、头像图片链接、点赞数、步数。
你说,这个用 Redis 的啥数据结构来存?
可不就得用 Hash 结构了吗。
Hash 结构同样涉及到 key 和 value,那么它们分别是什么呢?
key 就是我们的有序集合的 key 后面再加上好友昵称,比如这样的: