Redis使用场景梳理

一、Sorted Set(有序集合)- 排行榜 

    排行榜是业务开发中常见的一个场景。

    1.  场景一:选手报名参加活动,观众可以对选手进行投票,每个观众对同一名选手只能投一票,活动期间最多投N票

    1)功能1:返回TOP 10的选手信息及投票数

    2)功能2:返回活动总参与选手数及总投票数

    3)功能3:对于每个选手,返回自己的投票数,排名,距离上一名差的票数

    实现 :

有序集合是一个非常高效的数据结构,可以替代数据库里一些很难实现的操作。它的一个典型应用场景就是排行榜。

    

redis sortset结构 redis sortset应用_redis

    

redis sortset结构 redis sortset应用_redis sortset结构_02

    这里面有一些问题需要注意:

    1)在score相同的情况下,redis使用集合成员自身的字典顺序来排序,而所谓的字典排序其实就是“ABCDEFG”这样的排序,在首字母相同的情况下,redis会再比较后面的字母,还是按照字典排序。

    2)在有些情况下这个可能不满足实际要求,因此需要按实际情况重新设计score,比如如果要求同分数情况下按时间排序,时间戳越小,越排前。

    3)使用双精度浮点数类型作为score,结构为:分数+'.'+(MAX-时间戳),变为浮点数

    说明:

    1)这里只提及了与redis有序集合的相关实现,具体细节,比如需要记录总票数的话,可以单独维护一个可以使用incr来记录。

    2)如果需要返回top10的选手具体信息,那么member就可以由上面的名称替换成用户唯一标识openid之类的,然后使用其到DB中去查询选手具体信息来返回结果。

游戏中存在各种各样的排行榜

    比如玩家的等级排名、分数排名等。玩家在排行榜中的名次是其实力的象征,位于榜单前列的玩家在虚拟世界中拥有无尚荣耀,所以名次也就成了核心玩家的追求目标。

    一个典型的游戏排行榜包括以下常见功能:

     1)功能1:能够记录每个玩家的分数;

     2)功能2:能够对玩家的分数进行更新;

     3)功能3:能够查询每个玩家的分数和名次;

     4)功能4:能够按名次查询排名前N名的玩家;

     5)功能5:能够查询排在指定玩家前后M名的玩家。

     实现:

     

redis sortset结构 redis sortset应用_字符串_03

    总结,在实现排行榜的功能时,我们发现常用的命令:

     ZADD  :记录/更新每个玩家的分数

     ZSCORE  :查询玩家的分数

     ZREVRANK:查询玩家的名次(按分数从大到小排列)

     ZREVRANGE:按名次查询排名前N名的玩家

     ZRANK:  返回有序集中指定成员的排名(按分数从小到大)

     注意: ZREVRANK/ZRANK 查询到的名次,指的都是元素所在的索引下标

     3.  实效性

     真实场景中肯定会有时间段的划分,例如查看日榜、周榜、月榜。只需要按照最小的单位按照时间区分成不同的集合,最后求出这些集合的并集即可。

     从排行榜的实效性上划分,主要分为:

     1)实时榜:基于当前一段时间内数据的实时更新,进行排行。例如:当前一小时内游戏热度实时榜,当前一小时内明星送花实时榜等

     2)历史榜:基于历史一段周期内的数据,进行排行。例如:日榜(今天看昨天的),周榜(上一周的),月榜(上个月的),年榜(上一年的)

     相关命令:ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]]  : 计算给定的一个或多个有序集的并集

     例如:

      

redis sortset结构 redis sortset应用_Redis_04

二、弹幕/最新列表- List(列表)

     朋友圈的点赞列表、评论列表、排行榜、消息队列

     实现:Redis的 list (列表)结构

     1) LPUSH 命令和 LRANGE 命令能实现最新列表的功能,每次通过 LPUSH 命令往列表里插入新的元素,然后通过 LRANGE 命令读取最新的元素列表。

     2) LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。

     3) LPOP 和 RPUSH(或者反过来,lpush和rpop)能实现队列的功能

     相关命令:LTRIM KEY_NAME START STOP 作用:让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除

弹幕,只显示最新的N条评论

       

redis sortset结构 redis sortset应用_redis sortset结构_05

     说明:

     1) list 和 zset 都可以用做排行榜,但是和list不同的是zset它能够实现动态的排序。list 中的元素时可以重复的,如果要实现排行榜,也只是计算好的结果push到列表中去,所以一般都是用zset来做排行榜。

轻量级消息队列与消息中间件相比,没有高级特性也没有ACK保证,无法做到数据不重不漏,是一种比较简陋的消息队列。如果业务简单而且对消息的可靠性不是那么严格可以尝试使用。


三、社交网络- Set(集合)

      点赞、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大。

      可以对两个set(集合)提供交集、并集、差集操作。例如:查找两个人共同的好友等。

      

redis sortset结构 redis sortset应用_redis_06

sinterstore

    2)sismember命令可以判断A是否是B的好友;

    3)scard命令可以获取好友数量;

    4)关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合

展示随机:美团首页有很多推荐商家,但是并不能全部展示,set类型适合存放所有需要展示的内容,而srandmember命令则可以从中随机获取几个。

    6)存储某活动中中奖的用户ID ,因为有去重功能,可以保证同一个用户不会中奖两次。

   四、 String (字符串)-计数器/缓存

    string 类型在 redis 中是二进制安全(binary safe)的,这意味着 string 值关心二进制的字符串,不关心具体格式,可以用它存储 json 格式或 JPEG 图片格式的字符串

    1.  计数器

    什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数、高并发的秒杀活动、分布式序列号的生成等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。

    Redis提供的incr可以实现原子性的递增,内存操作,性能非常好,非常适用于这些计数场景。

1 <?php
 2 
 3 // redis记录该用户投票次数
 4 $voteNum = $redis->incr('votes:' . $openid);
 5 if ($voteNum > 4) {
 6     // 投票已达上限,计数器还原
 7     $redis->decr('votes:' . $openid);
 8     return [-1, [], '抱歉,您的投票次数达到上限,活动期间最多投4票~!'];
 9 }
10 ...
11 // DB操作:记录投票信息,返回操作结果$res
12 if ($res) {
13     return [0, [], '恭喜您,投票成功~!'];
14 } else {
15     //插入数据失败,计数器还原
16     $redis->decr('votes:' . $openid);
17     return [-1, [], '抱歉,投票失败~!'];
18 }

    2.  缓存

    1) 存储用户某个单独的信息:比如根据用户 id 查询用户邮箱地址

     

redis sortset结构 redis sortset应用_Redis_07

    2)存储用户全部信息:用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息

序列化或者json编码后的字符串。如果想要修改某个用户字段,必须将用户信息字符串全部查询出来,解析成相应的用户信息对象,修改完后在序列化/json编码后变成字符串存入。

    3)分布式锁

    在一个集群环境下,多个web应用时对同一个商品进行抢购和减库存操作时,可能出现超卖时会用到分布式锁

SETNX命令(SET if Not eXists)

    关于分布式锁,实际情况要考虑的细节更多

   五、 购物车/用户信息 - hash (哈希)

    购物车:hset [key] [field] [value] 命令, 可以实现以用户Id,商品Id为field,商品数量goodsnum为value,恰好构成了购物车的3个要素。

    存储对象:hash 类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。

   

redis sortset结构 redis sortset应用_redis_08

     六、 附近的人/商店/停车场- geo

     自Redis 3.2开始,Redis基于geohash和有序集合提供了地理位置相关功能。

     

redis sortset结构 redis sortset应用_Redis_09

     扩展一下:

     Redis中的geo是基于geohash和有序集合提供的地理位置相关功能。 相关命令使用起来也是非常简单。

     GeoHash基本原理

地址编码,通过切分地图区域为小方块(切分次数越多,精度越高),它能把二维的经纬度编码成一维的字符串。也就是说,理论上geohash字符串表示的并不是一个点,而是一个矩形区域,只要矩形区域足够小,达到所需精度即可。

     优点:使用GeoHash将二维的经纬度转换成字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存。

    

redis sortset结构 redis sortset应用_redis_10

   如果在小块范围内递归对半划分呢?

     

redis sortset结构 redis sortset应用_字符串_11

     编码特性

     不难看出这样的编码方式仅用一个字符串保存经纬度信息,并且精度由字符串从头到尾的长度决定,编码长度越长,精度越高。GeoHash值的前缀相同的位数越多,代表的位置越接近,可以方便索引。(反之不成立,位置接近的GeoHash值不一定相似)。

     但这种方案的缺点是:从geohash的编码算法中可以看出,靠近每个方块边界两侧的点虽然十分接近,但所属的编码会完全不同。实际应用中,需要通过去搜索环绕当前方块周围的8个方块来解决该问题。
除此之外,这个方案也无法直接得到距离,需要程序协助进行后续的排序计算。

     

redis sortset结构 redis sortset应用_Redis_12

    注意:geohash算法有两个问题

    1.   边界问题

    由于GeoHash是将区域划分为一个个规则矩形,并对每个矩形进行编码,这样在查询附近餐馆信息时会导致以下问题,比如红色的点是我们的位置,绿色和黄色的两个点分别是附近的两个餐馆,但是在查询的时候会发现距离较远餐馆的黄色的点的GeoHash编码与我们一样(因为在同一个GeoHash区域块上),而较近餐馆的GeoHash编码与我们不一致。这个问题往往产生在边界处。

   解决的思路很简单,我们查询时,除了使用定位点的GeoHash编码进行匹配外,还使用周围8个区域的GeoHash编码,这样可以避免这个问题。

     

redis sortset结构 redis sortset应用_Redis_13

     2.   曲线突变

     现有的GeoHash算法使用的是Peano空间填充曲线这种曲线会产生突变,造成了编码虽然相似但距离可能相差很大的问题,因此在查询附近餐馆时候,首先筛选GeoHash编码相似的餐馆的点,然后进行实际距离计算。

   举个栗子:

   根据经纬度获取附近的人。具体实现:

   1)给定经纬度,计算geohash

   2)根据半径范围选取最小的区块,例如600m附近,可以使用6位的geohash作为最小区块

   3)由于自身可能在最小区块内的任意位置,因此需要一并获取最小区块的周围8个临近区块

   4)数据库中筛选geohash的6位前缀在这9个区域中的所有用户,然后计算距离,排除距离外的用户 

点数据,而对线、面数据采用R树索引更有优势

 

参考链接:

https://www.jianshu.com/p/557e0faa15fc

https://github.com/GongDexing/Geohash