Redis使用场景梳理
一、Sorted Set(有序集合)- 排行榜
排行榜是业务开发中常见的一个场景。
1. 场景一:选手报名参加活动,观众可以对选手进行投票,每个观众对同一名选手只能投一票,活动期间最多投N票
1)功能1:返回TOP 10的选手信息及投票数
2)功能2:返回活动总参与选手数及总投票数
3)功能3:对于每个选手,返回自己的投票数,排名,距离上一名差的票数
实现 :
Redis的有序集合是一个非常高效的数据结构,可以替代数据库里一些很难实现的操作。它的一个典型应用场景就是排行榜。
这里面有一些问题需要注意:
1)在score相同的情况下,redis使用集合成员自身的字典顺序来排序,而所谓的字典排序其实就是“ABCDEFG”这样的排序,在首字母相同的情况下,redis会再比较后面的字母,还是按照字典排序。
2)在有些情况下这个可能不满足实际要求,因此需要按实际情况重新设计score,比如如果要求同分数情况下按时间排序,时间戳越小,越排前。
3)使用双精度浮点数类型作为score,结构为:分数+'.'+(MAX-时间戳),变为浮点数
说明:
1)这里只提及了与redis有序集合的相关实现,具体细节,比如需要记录总票数的话,可以单独维护一个可以使用incr来记录。
2)如果需要返回top10的选手具体信息,那么member就可以由上面的名称替换成用户唯一标识openid之类的,然后使用其到DB中去查询选手具体信息来返回结果。
2. 场景二:游戏中存在各种各样的排行榜
比如玩家的等级排名、分数排名等。玩家在排行榜中的名次是其实力的象征,位于榜单前列的玩家在虚拟世界中拥有无尚荣耀,所以名次也就成了核心玩家的追求目标。
一个典型的游戏排行榜包括以下常见功能:
1)功能1:能够记录每个玩家的分数;
2)功能2:能够对玩家的分数进行更新;
3)功能3:能够查询每个玩家的分数和名次;
4)功能4:能够按名次查询排名前N名的玩家;
5)功能5:能够查询排在指定玩家前后M名的玩家。
实现:
总结,在实现排行榜的功能时,我们发现常用的命令:
ZADD :记录/更新每个玩家的分数
ZSCORE :查询玩家的分数
ZREVRANK:查询玩家的名次(按分数从大到小排列)
ZREVRANGE:按名次查询排名前N名的玩家
ZRANK: 返回有序集中指定成员的排名(按分数从小到大)
注意: ZREVRANK/ZRANK 查询到的名次,指的都是元素所在的索引下标
3. 实效性
真实场景中肯定会有时间段的划分,例如查看日榜、周榜、月榜。只需要按照最小的单位按照时间区分成不同的集合,最后求出这些集合的并集即可。
从排行榜的实效性上划分,主要分为:
1)实时榜:基于当前一段时间内数据的实时更新,进行排行。例如:当前一小时内游戏热度实时榜,当前一小时内明星送花实时榜等
2)历史榜:基于历史一段周期内的数据,进行排行。例如:日榜(今天看昨天的),周榜(上一周的),月榜(上个月的),年榜(上一年的)
相关命令:ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] : 计算给定的一个或多个有序集的并集
例如:
二、弹幕/最新列表- 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 作用:让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除
补充: ltrim(裁剪)-可以用于做弹幕,只显示最新的N条评论
说明:
1) list 和 zset 都可以用做排行榜,但是和list不同的是zset它能够实现动态的排序。list 中的元素时可以重复的,如果要实现排行榜,也只是计算好的结果push到列表中去,所以一般都是用zset来做排行榜。
2) 使用 list 实现的轻量级消息队列与消息中间件相比,没有高级特性也没有ACK保证,无法做到数据不重不漏,是一种比较简陋的消息队列。如果业务简单而且对消息的可靠性不是那么严格可以尝试使用。
关于Redis如何来实现消息队列,以及与消息中间件相比的劣势到底体现在那些地方,可以参考我的另一篇文章《Redis实现消息队列》。
三、社交网络- Set(集合)
点赞、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大。
可以对两个set(集合)提供交集、并集、差集操作。例如:查找两个人共同的好友等。
1)sinter命令可以获得A和B两个用户的共同好友,另外,有一个相似的命令:sinterstore ,将给定集合之间的交集存储在指定的集合中
2)sismember命令可以判断A是否是B的好友;
3)scard命令可以获取好友数量;
4)关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合
5)首页展示随机:美团首页有很多推荐商家,但是并不能全部展示,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 查询用户邮箱地址
2)存储用户全部信息:用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息
注意:存储的value 是经过序列化或者json编码后的字符串。如果想要修改某个用户字段,必须将用户信息字符串全部查询出来,解析成相应的用户信息对象,修改完后在序列化/json编码后变成字符串存入。
3)分布式锁
在一个集群环境下,多个web应用时对同一个商品进行抢购和减库存操作时,可能出现超卖时会用到分布式锁
相关命令: SETNX命令(SET if Not eXists)
关于分布式锁,实际情况要考虑的细节更多,可以参考我整理过的一篇相关文章《Redis的分布式锁》
五、 购物车/用户信息 - hash (哈希)
购物车:hset [key] [field] [value] 命令, 可以实现以用户Id,商品Id为field,商品数量goodsnum为value,恰好构成了购物车的3个要素。
存储对象:hash 类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
六、 附近的人/商店/停车场- geo
自Redis 3.2开始,Redis基于geohash和有序集合提供了地理位置相关功能。
扩展一下:
Redis中的geo是基于geohash和有序集合提供的地理位置相关功能。 相关命令使用起来也是非常简单。这里再扩展一下geohash的实现过程以及原理分析,也可以参考下《GeoHash核心原理解析》这篇文章。
GeoHash基本原理
GeoHash是一种地址编码,通过切分地图区域为小方块(切分次数越多,精度越高),它能把二维的经纬度编码成一维的字符串。也就是说,理论上geohash字符串表示的并不是一个点,而是一个矩形区域,只要矩形区域足够小,达到所需精度即可。
优点:使用GeoHash将二维的经纬度转换成字符串,这样既可以保护隐私(只表示大概区域位置而不是具体的点),又比较容易做缓存。
如果在小块范围内递归对半划分呢?
编码特性
不难看出这样的编码方式仅用一个字符串保存经纬度信息,并且精度由字符串从头到尾的长度决定,编码长度越长,精度越高。GeoHash值的前缀相同的位数越多,代表的位置越接近,可以方便索引。(反之不成立,位置接近的GeoHash值不一定相似)。
但这种方案的缺点是:从geohash的编码算法中可以看出,靠近每个方块边界两侧的点虽然十分接近,但所属的编码会完全不同。实际应用中,需要通过去搜索环绕当前方块周围的8个方块来解决该问题。
除此之外,这个方案也无法直接得到距离,需要程序协助进行后续的排序计算。
注意:geohash算法有两个问题
1. 边界问题
由于GeoHash是将区域划分为一个个规则矩形,并对每个矩形进行编码,这样在查询附近餐馆信息时会导致以下问题,比如红色的点是我们的位置,绿色和黄色的两个点分别是附近的两个餐馆,但是在查询的时候会发现距离较远餐馆的黄色的点的GeoHash编码与我们一样(因为在同一个GeoHash区域块上),而较近餐馆的GeoHash编码与我们不一致。这个问题往往产生在边界处。
解决的思路很简单,我们查询时,除了使用定位点的GeoHash编码进行匹配外,还使用周围8个区域的GeoHash编码,这样可以避免这个问题。
2. 曲线突变
现有的GeoHash算法使用的是Peano空间填充曲线,这种曲线会产生突变,造成了编码虽然相似但距离可能相差很大的问题,因此在查询附近餐馆时候,首先筛选GeoHash编码相似的餐馆的点,然后进行实际距离计算。
举个栗子:
根据经纬度获取附近的人。具体实现:
1)给定经纬度,计算geohash
2)根据半径范围选取最小的区块,例如600m附近,可以使用6位的geohash作为最小区块
3)由于自身可能在最小区块内的任意位置,因此需要一并获取最小区块的周围8个临近区块
4)数据库中筛选geohash的6位前缀在这9个区域中的所有用户,然后计算距离,排除距离外的用户
geohash只是空间索引的一种方式,特别适合点数据,而对线、面数据采用R树索引更有优势
参考链接:
https://www.jianshu.com/p/557e0faa15fc
https://segmentfault.com/a/1190000018636887
https://segmentfault.com/a/1190000022800471
https://www.cnblogs.com/LBSer/p/3310455.html
https://github.com/GongDexing/Geohash