• Redis使用场景梳理


    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

  • 相关阅读:
    【转】Java操作CSV文件导入导出
    【转】Java压缩和解压文件工具类ZipUtil
    Python之multiprocessing.Pool(创建多个子进程)
    Openstack平台虚拟机疏散失败提示(pymysql.err.OperationalError) (2013, 'Lost connection to MySQL server during query')问题
    kubernetes部署redis主从高可用集群
    Ceph性能测试
    python日志模块
    kubernetes删除pod,pod一直处于Terminating状态
    python执行提示“ImportError: No module named OpenSSL.crypto”
    二进制部署kubernetes集群_kube-apiserver提示"watch chan error: etcdserver: mvcc: required revision has been compacted'
  • 原文地址:https://www.cnblogs.com/hld123/p/14657264.html
Copyright © 2020-2023  润新知