Redis为什么快?1. 内存操作、2. 多路复用、3. 高效数据结构,这节学习的就是Redis底层的数据结构应用场景。

1 全局哈希表

在Redis里set一个key-value时,会存储到Redis里的全局哈希表里。Redis的key一定是一个字符串,使用哈希函数对这个key取一个哈希值,然后对哈希表的长度取模,然后就能分到哈希表的一个表项(桶)里。

Redis底层有渐进式的rehash和动态扩容机制,把发生哈希碰撞的概率降得很低,所以Redis的全局哈希表性能很高。

redis哈希表实现 redis哈希使用场景_redis哈希表实现


key都是字符串,但是value有各种各样的数据类型,所以一般谈Redis的数据结构都是在谈各种各样的value结构,如字符串string、哈希hash、列表list、集合set、有序集合zset等。

使用type [key]可以看到某个key对应的value的逻辑数据类型,使用object encoding [key]可以看到某个key对应的value底层实际存储使用的数据编码类型。

2 string应用场景

在底层存储时,如果能转成int,那么底层会用int来存;如果长度小于等于44字节,那么底层会用embstr来存;否则(大于44字节),底层会用raw来存。

2.1 用作缓存和分布式锁

除了直接用来存一个字符串或者数字,也可以用来存储一个对象(结构体)。存对象的时候要么直接存序列化之后的数据,要么就用MSETMGET这种一次设置或取出多个key-value的方式来做。

还有就是可以用来作为分布式锁,这个时候key一般用来标识要锁定的资源对象(如果要做分段锁,那么也是对key进行分段),value一般用来标识加锁的是哪个线程(分布式环境下的唯一标识)。

redis哈希表实现 redis哈希使用场景_数据结构_02

2.2 用作计数器

由于数字也是用string来存的,Redis还提供了一个INCR命令可以给数字值加上1,所以可以作为天然的计数器,比如文章的阅读数量。

redis哈希表实现 redis哈希使用场景_缓存_03

2.3 用作分布式系统全局序列号

例如数据库做了分库分表,那么数据库的自增主键就没法用数据库自带的auto increment来保证了,这个时候就可以借助Redis来维护一个自增id,也是用这个INCR的命令。

但是这样做会有很大的性能问题,实际生产环境里可能有很多张表,而且比如订单这种产生的量非常大,如果用Redis这样宝贵的资源来产生主键id,那不仅性能不够,而且也占Redis自己用来缓存的能力资源。

解决方案是,每个实例在获取新的自增id的时候多获取一些,例如一次性加100,而不是只获取1个。然后只要自己获取到的这100个id没用完,就都是在自己的内存里做++操作。对应到Redis里,可以用INCRBY [key] [num]来指定一次让这个数字加多少。

redis哈希表实现 redis哈希使用场景_缓存_04

3 hash应用场景

这里指的不是Redis全局哈希表,而是全局哈希表key-value的value部分本身是一个哈希表,即这个value本身也是一个key-value结构(为了和全局哈希表的key区分开,这里叫field-value)。

3.1 更优美的对象缓存

有了hash结构就可以在设置对象缓存的时候,把一类对象聚合到一个hash结构里了(像一个单独的表一样),而不用全都扔到全局哈希表里了。

使用HMSET命令可以在一个hash指定的结构里存储多个field-value,HMGET是相应的批量取出的操作。

redis哈希表实现 redis哈希使用场景_redis哈希表实现_05


在学习string的时候也提到对象存储,相比用string直接存json串的方式,用hash结构来做对象存储更合适。例如,要设置某个字段的值,在string+json串的存储方式里要把整个结构从Redis里读到程序中作反序列化,然后写值,然后再序列化,再写回Redis;而如果使用hash结构,只要把要设置的字段的field拼接出来,然后直接设置就可以了。

redis哈希表实现 redis哈希使用场景_zset_06

3.2 实例:购物车

用数据库去实现购物车功能还是比较麻烦的,但是用Redis的哈希结构就很容易。在下面的例子里,给用户id前面加一个cart:前缀作为Redis全局哈希表的key,value是一个hash结构,就表示这个用户的购物车,则购物车操作都可以简化为对Redis的hash结构的操作。

redis哈希表实现 redis哈希使用场景_zset_07


用Redis缓存来存购物车信息,那么Redis挂了怎么办?这种场景可以直接靠Redis缓存,见持久化机制。

4 list应用场景

Redis中的列表list是用压缩列表ziplist和双向链表linkedlist来实现的,既有基本的最左最右插入和弹出的操作,也有返回一个索引区间内元素的操作,也支持阻塞的弹出操作(所以可以作为消息队列用)。

redis哈希表实现 redis哈希使用场景_数据结构_08

4.1 分布式场景下栈、队列、阻塞队列

利用这些API的组合可以实现栈、队列、阻塞队列等数据结构:

redis哈希表实现 redis哈希使用场景_数据结构_09


为什么不用程序内存中的数据结构?例如用JDK里的栈、队列之类的,实际上只能在这个实例里使用,不能在分布式环境中共享,所以Redis这里实现的数据结构是给分布式场景使用的。

4.2 实例:微博、微信公众号消息流 & 推荐feed流

这种消息流数据量很大,而且后发文的在最前面,这种场景可以用list来存储。

redis哈希表实现 redis哈希使用场景_数据结构_10


例如A关注的人B发消息,那么就把B的消息id往A的列表里LPUSH过去,当A进入页面去看的时候,可以用LRANGE取最左侧的若干个元素(图里用正向索引),就能拿到最新发的一些消息了, 这种发文去更新follower的方式称为写扩散

redis哈希表实现 redis哈希使用场景_缓存_11


如果大V不是特别大,只有几千几万的粉丝,那么用上面的方式实现,往这些所有粉丝的list里去存是很快的(还可以用Redis管道Pipeline来把所有要执行的命令打包,管道执行多条命令的网络开销实际上相当于执行一条命令的网络开销)。

如果是超级大V,有大量的粉丝,那么可以在业务层面作一些优化,比如发的时候先只发给那些在线的用户,然后其他粉丝再慢慢发。

5 set应用场景

redis哈希表实现 redis哈希使用场景_Redis_12

5.1 实例:抽奖程序

因为用户抽奖天然就是在一个无序、不重复的用户集合中选出若干个,所以可以利用SRANDMEMBER这个Redis原生命令来实现抽奖程序,用SMEMBERS还能看到集合中的所有元素(也就是查看所有的抽奖用户)。

redis哈希表实现 redis哈希使用场景_zset_13


使用SRANDMEMBER随机挑若干元素,不会把元素删除掉;使用SPOP命令随机挑若干元素,会把抽出来的元素从集合里删除掉。因此如果要实现分级的抽象程序,比如一等奖1个,二等奖2个,三等奖10个,而且不能有用户同时获取两个奖,这个时候就可以用SPOP这个命令来实现。

5.2 实例:点赞、收藏功能

点赞、收藏这种功能不是仅仅一个数字能解决的事情,还要记录点赞的用户是谁(一方面是用户可能会删除点赞或者查看自己的收藏,另一方面是端上可能有展示点赞的用户都有谁的需求),所以也可以直接用set集合来处理。

redis哈希表实现 redis哈希使用场景_redis哈希表实现_14


SISMEMBER这种判断是否在集合内的命令可以用来判断用户是否点赞过,端上通过这个判断结果就能展示用户自己的点赞结果。

另外要注意,对于微信的端上展示逻辑,使用SMEMBERS命令拿出来集合中的所有元素其实是集合中的所有元素,也就是所有给这个用户的点赞用户,因此只适用于给用户自己展示。

另外,用集合来保存点赞的用户id,是无序的,有的时候要求点赞的展示是有序的,就不能用集合来做。

5.3 实例:社交应用关注模型

例如,A关注了B和C,那么A作为key的value就是一个存了B和C的集合。

redis哈希表实现 redis哈希使用场景_zset_15


要注意SDIFF这个求差集的命令,SDIFF set1 set2 ... setn是用set1里的元素减去后面所有集合的元素的并集。用set的集合操作,可以简单实现这种“共同关注”和“我关注的人也关注了”之类的关注模型逻辑,但是实际上业务后端还有一些更复杂的关注模型。下图是微博的“共同关注”和“我关注的人也关注了”。

redis哈希表实现 redis哈希使用场景_Redis_16


上面这两类信息可以用来作为推荐系统的输入数据,如果A和B两个用户的“共同关注”和“我关注的人也关注了”越多,实际就能说明这两个用户的相似度可能越高,这些信息都能用来作为给推荐算法分析的基础。

6 zset应用场景

redis哈希表实现 redis哈希使用场景_zset_17

6.1 实例:有序的点赞展示

zset有序集合除了存储集合中的元素之外,还存储了一个数值,那么如果要让前面5.2中的点赞是有序的,就可以把每个用户的点赞时间存进去,端上拿出来展示的时候按点赞的时间从大到小排序。

6.2 实例:热搜 & 排行榜

带有排序属性的都可能是zset的应用场景,比如微博热搜或者新闻排行耪等场景。