用最朴素的语言讲懂Redis中的五种常用数据结构(听完不懂我负责)

49 阅读9分钟

全局视角来看:

redis的数据结构采用了广泛存储的思想,它的key都是统一用了string的结构,key单独使用了hashtable的存储方式,相当于是一个全局的字典树,在value统一使用了redisObject进行了封装,这个redisObject有它的type以及encoding,还有一个ptr的指针指向真实的数据结构。key所对应的就是redisObject这个对象。

String:

在string的结构中,有两种存储结构,一种是int类型的,当value是int类型时,ptr会直接指向一个longlong类型的结构,如果涉及到字符操作时就会使用row这个结构,无论是row还是embstr都是支持sds存储的,sds它的设计目的是为了避免c中字符串存储所带来的性能问题和安全问题,性能问题就是在获取字符串长度时,是需要遍历获取的,时间复杂度是O(n),性能比较低,sds去统一维护了长度。还有就是c的字符串有内存溢出的问题,频繁操作字符串需要不断的对内存作出修改,可能会导致内存溢出异常,所以string使用了embstr和raw规避这种问题,添加数据时如果小于1mb就会分配1mb的空间,大于1mb就是1mb加上当前的大小,预留了空间做扩展防止内存溢出,在对embstr做操作时他也会转成raw的模式,embstr默认是44个字节大小之内的字符串,raw是默认大于44个字节。还有一个问题就是二进制安全问题,在c中读取的时候遇到/0符号默认就是终止,所以读操作不是安全的。

List

在list结构中,它采用的是quicklist的结构,quicklist中维护了一个双向链表,每个节点下都去维护了一个ziplist,ziplist中存储了ziplist的占用内存的大小,以及节点的个数,和一个开始和结束的节点,它的读取效率非常高,没有采用传统的这种prev和next的指针,而是使用物理内存偏移量和前一个节点所占内内存的大小去定位,它在内存中是连续的,但是如果节点数量多有一个节点被重构之后意味着它后面的元素都需要修改偏移量,所以这点不是很友好,所以在他的上层使用了一个双向链表的结构,对于每个几点都有一个阈值,在添加元素的时候会判断当前ziplist元素是否到达阈值,如果到达阈值就会创建下一个节点。

Hash

在hash结构中,它使用了hashtable的结构,和key的存储结构是一样的,dicthashtable中存储了size,used元素表示当前桶的大小,在定位桶的时候还是用到了传统的hash寻址,通过位运算定位桶,在没有覆盖的情况下使用的是头插法,他也有扩容的机制,取决于当前有没有后台线程进行bgsave,如果存在bgsave的情况的话,它是判断used/size是否到达五倍,也就是元素数量是否到达桶个数的五倍,如果到达就进行扩容结果,如果没有bgsave的话,是一倍的时候进行扩容,扩容的时候主要是用到了两张hash表以及一个rehashindex,他为了避免一次性扩容所带来的时间开销,它使用了rehash方式进行扩容,使用了一个新的数组,他会扩容为当前used的2倍向上取2的幂次方,这个思想和hahsmap一样,也是为了尽量避免少的数据迁移,在迁移结束之后会将新的hashtable置为h0,酒店数组置换为null,每次增删改查是都会基于这个rehashindex进行扩容,每次迁移一个桶的大小。

Set

在set结构中,采用了inset结构,其实也是使用的ziplist的结构,int类型的话他也会保证它的有序性。在inset触发阈值时就会转换为哈希表的结构(哈希表是只存储了它的key)。

ZSet

在zset结构中,它使用了哈希表以及skiplist,他们两是配合使用的,如果我需要查询指定成绩或者指定的field会使用哈希表,因为hash表的查询速度是O(1),但是hash表的结构不适合范围查询,所以使用了skiplist的机构,它是根据构建不同层级的方法逐渐缩小查找的范围在进行精确查找,是一种二分查找的思想。他会构建一个头节点,对后面的每一个元素进行遍历,有一个随机函数,类似于抛硬币的方式决定需不需要添加到上级索引,在这它的key是score,value可以看作是它原来的field,在一级索引构建结束之后会继续构建二级索引,对现在二级索引上已经有的元素进行判断,方法类似于一级索引,在查询的时候先去基于三级索引查找逐个锁定范围。

再来说一下使用场景,我说完你就会记住:

1.String:

1.适合用于缓存,这是最常见的说法,但是所有的结构都是为了缓存,

2.使用于计数器,提供了incre,increBy ,decre,decreBy,可以做增减,比如需要去对某个url的访问次数做统计,可以在网管层使用它做递增。

3.分布式锁,setNx命令 如果不存在就设置,面试的时候需要记住这么几个点,它是一个原子性的指令,把判断的过程和设置的过程放在了一起,所以是安全的,可以类比cas的思想。还需要记住设置过期时间,一旦宕机将会导致其他节点无法抢占锁造成阻塞,还需要注意,释放锁的时候需要保证添加版本号或者线程id做判断,不能让别的线程释放掉自己的锁,这会导致业务执行没有结束,并且你这个判断的过程和删除的过程也应该保证原子性,还有一个问题就是业务没有执行结束可能因为一些io阻塞导致超时释放,这个问题如何避免(在插入之后会判断当前的等待时间和超时时间,如果时间足够我们去订阅锁的频道,采用samphore去抢占资源)?接下来需要上点难度,redsiion客户端完美的解决了这些问题,他用的就是redis中hash的结构,现在可以再回头去看一眼hash表的结构。在key中,存储的就是互斥资源,也就是我们业务中所定义的key,这是为了保证互斥,field他是采用了uuid+threadId,保证的是不会被其他线程误删,value存到是什么呢?想想ReentrantLock和Synchronized的特性,value存储的就是它重入的次数,保证加锁释放锁的次数需要对称相等。那么是如何解决的锁的续期机制的呢?如果我们传入了锁的过期时间,就会基于当前时间没过(过期时间/3)做一次定时任务为锁续命,如果没有传入就是默认30秒,也就是30/10去做一次续期,那么它的定时任务是如何实现的呢?时间轮!不要被它的名字吓到,所有你见过的名词在你了解之后你会发现不过如此。我也不懂redisson的时间伦,但是面试官问到我说的出来。现在想象一下钟表的结构,他把一天按照小时刻画了十二个维度,那么如果我把所需要执行的任务丢在我需要的范围之内的话,此时有一个定时任务不停的去判断现在执行到哪一个时间了,我的指针该指向哪一个范围了?那么任务如何分配的?你一定懂,你不懂就不会看到这篇文章,没错,就是hash位运算或者取模。将任务分配到这个范围之内只需要指针不断的变化就会执行到我所需要执行的任务了。那么我们的定时任务可能会涉及到分钟为单位,那么我们只需要在扩展一个新的时间轮,先把任务放到这小时的时间轮子中,执行到这的时候把目前范围内的任务降级扔到分钟的轮子,而这个分钟的轮子又有一个指针去判断。

2.List

list它使用了quicklist存储,他维护的头节点尾节点可以按照顺序弹出,那么按照顺序弹出的就有通知,通知一般是按照时间的维度弹出,还有点赞列表,朋友圈。或者也可以实现队列,但是redis并没有提供ack的机制所以消息不能保证它的可靠性。

3.set

这个结构有一个关键的点需要记忆,如果如果都是int类型并且大小在阈值之内,那么它是顺序的,但是不保证唯一性。它最多的使用场景一点是去重,一点是可以取合集,交集,并集,会使用在共同关注好友就可以取交集,不同好友可以取差集,一级一些喜好标签也可以,原理都一样。

4.zset

他又配置权重score,可以适用于排行榜,这个点需要记住。

还需要记住,不是所有的操作都是使用跳表,还有哈希表

5.hash

可以存储对象 购物车 但是记住不管哪个都不能只依赖缓存,因为redis是ap模型