1. 基础对象
Redis的存储是以key-value
的形式的。Redis中的key一定是字符串,value可以是string、list、hash、set、sortset这几种常用的。
类型 | 作用 | 底层数据结构 |
---|---|---|
string | 简单的key-value | int, embstr(sds), raw |
list | 有序列表,可做简单队列 | 压缩列表,双向链表 |
hash | 哈希表, 存储结构化数据 | 压缩列表,哈希 table |
set | 无序列表(去重), 提供一系列的交集、并集、差集的命令 | 整数集合,哈希 table |
sortset | 有序集合映射, 排行榜 | 压缩列表, 跳跃表 |
1.1 string
int 编码:保存的是可以用 long 类型表示的整数值。
embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。
raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)
使用场景
- 缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
- 计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
- session:常见方案spring session + redis实现session共享,
1.2 list
列表对象的编码可以是 ziplist 或者 linkedlist。新版本列表对象的编码是quicklist。
使用技巧
lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpush+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)
使用场景
- 微博TimeLine: 有人发布微博,用lpush加入时间轴,展示新的列表信息。
- 消息队列
1.3 hash
- ziplist:key和value的字符串长度都小于64字节
&&
键值对总数量小于512 - hashtable:key和value的字符串长度大于64字节
||
键值对总数量大于512
使用场景
- 缓存: 直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等。
1.4 set
- intset:保存的元素全都是整数
&&
总数量小于512 - hashtable:保存的元素不是整数
||
总数量大于512
使用场景
- 标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。
- 点赞,或点踩,收藏等,可以放到set中实现
1.5 zset
- ziplist:元素长度小于64
&&
总数量小于128 - skiplist:元素长度大于64
||
总数量大于128
skiplist能够达到插入的时间复杂度为O(logn),根据成员查分值的时间复杂度为O(1)
使用场景
- 排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
2.底层数据结构
2.1 简单动态字符串 - SDS
常数复杂度获取字符串长度
由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。
杜绝缓冲区溢出
减少修改字符串的内存重新分配次数
由于len属性和alloc属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略。
- 二进制安全
- 兼容部分 C 字符串函数
2.2 压缩列表 - ZipList
Ziplist 是内存紧凑型列表,节省内存空间、提升内存使用率。适用列表数量较少,元素size较小的场景。
连续存储:Ziplist中的元素是连续存储的,不像普通的双向链表那样使用指针进行连接。这样可以减少指针的使用,从而节省内存。
紧凑存储:Ziplist通过使用紧凑的存储格式,可以节省内存空间。它将不同长度的字符串和整数存储在一起,使用特定的编码方式进行压缩,从而减少了存储空间的占用。
快速访问:由于Ziplist使用连续存储,可以通过偏移量(offset)快速访问到指定位置的元素,而无需像普通链表那样遍历。
2.3 快表 - QuickList
quicklist这个结构是Redis在3.2版本后新加的, 之前的版本是list(即linkedlist), 用于String数据类型中。
它是一种以ziplist为结点的双端链表结构. 宏观上, quicklist是一个链表, 微观上, 链表中的每个结点都是一个ziplist。
2.4 字典/哈希表 - Dict
1. 解决哈希冲突
链地址法。通过字典里面的 *next 指针指向下一个具有相同索引值的哈希表节点。
2. 扩容和收缩
当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤:
ht[0].used当前的值为4,4*2=8,而8(2 3)恰好是第一个大于等于4的2的n次方,所以程序会将ht[1]哈希表的大小设置为8。图4-9展示了ht[1]在分配空间之后,字典的样子。
将ht[0]包含的四个键值对都rehash到ht[1]
释放ht[0],并将ht[1]设置为ht[0],然后为ht[1]分配一个空白哈希表。至此,对哈希表的扩展操作执行完毕,程序成功将哈希表的大小从原来的4改为了现在的8。
3. 触发扩容的条件
1、服务器目前没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于1。
2、服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于5。
ps:负载因子 = 哈希表已保存节点数量 / 哈希表大小。
对于一个大小为512,包含256个键值对的哈希表来说,这个哈希表的负载因子为:load_factor = 256 / 512 = 0.5
另一方面,当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。
4. 渐近式 rehash
如果保存在Redis中的键值对只有几个几十个,那么 rehash 操作可以瞬间完成,但是如果键值对有几百万,几千万甚至几亿,那么要一次性的进行 rehash,势必会造成Redis一段时间内不能进行别的操作。
渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。
Redis采用渐进式 rehash,在渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行增加操作,一定是在新的哈希表上进行的。
2.5 整数集 - IntSet
整数集合(intset)是集合类型的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。
2.6 跳表 - ZSkipList
- 跳跃表是一种平衡的数据结构:与平衡二叉搜索树相比,跳跃表的插入、删除和查找操作的平均时间复杂度都是O(log n),其中n是元素的数量。
- 跳跃表是一种有序数据结构:跳跃表中的元素是有序排列的,使得查找操作可以通过跳跃的方式快速定位到目标元素。
- 跳跃表通过多级索引实现快速查找:跳跃表中的每一层都是原始链表的一个子集,每一级索引的元素数量逐渐减少。通过在不同级别之间跳跃,可以快速定位到目标元素。
- 跳跃表支持高效的插入和删除操作:相比平衡二叉搜索树,跳跃表的插入和删除操作更加简单高效,不需要进行平衡操作,只需要更新索引层的指针即可。
- 跳跃表相对简单易实现:相比其他平衡数据结构如红黑树等,跳跃表的实现相对简单,容易理解和实现。
- 跳跃表相对于其他数据结构,可能会占用更多的内存空间来存储额外的索引层。
为什么Redis选择使用跳表而不是红黑树来实现有序集合?
Redis 中的有序集合(zset) 支持的操作:插入一个元素,删除一个元素,查找一个元素,有序输出所有元素,按照范围区间查找元素(比如查找值在 [100, 356] 之间的数据)。
其中,前四个操作红黑树也可以完成,且时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。按照区间查找数据时,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了,非常高效。
3. 其他数据对象
3.1 HyperLogLogs(基数统计)
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
1 | 127.0.0.1:6379> pfadd key1 a b c d e f g h i # 创建第一组元素 |
HyperLogLog只能统计基数的大小(也就是数据集的大小,集合的个数),他不能存储元素的本身,不能向set集合那样存储元素本身,也就是说无法返回元素。
3.2 Bitmap (位存储)
Bitmap 即位图数据结构,都是操作二进制位来进行记录,只有0 和 1 两个状态。
两个状态的,都可以使用 Bitmaps!如果存储一年的打卡状态需要多少内存呢? 365 天 = 365 bit 1字节 = 8bit 46 个字节左右!
1 | 127.0.0.1:6379> setbit sign 0 1 |
3.3 geospatial (地理位置)
geo底层的实现原理实际上就是Zset。
Redis 的 Geo 在 Redis 3.2 版本就推出了! 这个功能可以推算地理位置的信息: 两地之间的距离, 方圆几里的人。
1 | # 添加地理位置 |
3.4 Stream
Redis5.0 中还增加了一个数据结构Stream,从字面上看是流类型,但其实从功能上看,应该是Redis对消息队列(MQ,Message Queue)的完善实现。
用过Redis做消息队列的都了解,基于Reids的消息队列实现有很多种,例如:
- PUB/SUB,订阅/发布模式 : 但是发布订阅模式是无法持久化的,如果出现网络断开、Redis 宕机等,消息就会被丢弃;
- 基于List LPUSH+BRPOP 或者 基于Sorted-Set的实现 ,支持了持久化,但是不支持多播,分组消费等。