Redis看这一篇就够了(三)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Redis看这一篇就够了(三)
压缩列表

压缩列表的数据结构如下:

各个属性说明如下:

  • zlbytes,大小为4字节,记录整个压缩列表占用的内存字节数
  • zltail,大小为4字节,记录压缩列表表尾节点距离压缩列表起始地址有多少字节。通过这个值,压缩列表无需偏移量就能快速确定表尾节点地址。
  • zllen,大小为2字节,当属性值小于65535时,记录了压缩列表包含的节点数量,当属性值等于65535时,节点数量需要遍历获取。
  • entryN:压缩列表的节点,节点长度由节点保存的内容决定。
  • zlend:,大小为1字节,特殊值0xFF(十进制255),用于标记压缩列表的末端

举个例子如下:

各个属性值解释如下:

  • 列表zlbytes属性的值为0xd2(十进制210),表示压缩列表的总长为210字节,O(1)
  • 列表zltail属性的值为0xb3(十进制179),这表示如果我们有一个指向压缩列表起始地址的指针p,那么只要用指针p加上偏移量179,就可以计算出表尾节点entry5的地址,O(1)
  • 列表zllen属性的值为0x5(十进制5),表示压缩列表包含五个节点,O(1)

压缩列表有些特性和需要注意的地方

压缩列表特性

压缩列表包含倒序回溯和连锁更新两个特性,需要注意。

倒序回溯

倒序回溯,因为节点的previous_entry_length属性记录了前一个节点的长度,所以程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,压缩列表的从表尾向表头遍历操作就是使用这一原理实现的

连锁更新

在一个压缩列表中,有多个连续的、长度介于250字节到253字节之间的节点e1至eN,因为e1至eN的所有节点的长度都小于254字节,所以记录这些节点的长度只需要1字节长的previous_entry_length属性,换句话说,e1至eN的所有节点的previous_entry_length属性都是1字节长的。

  1. 如果我们将一个长度大于等于254字节的新节点new设置为压缩列表的表头节点,那么new将成为e1的前置节点
  2. 因为e1的previous_entry_length属性仅长1字节,它没办法保存新节点new的长度,所以程序将对压缩列表执行空间重分配操作,并将e1节点的previous_entry_length属性从原来的1字节长扩展为5字节长
  3. 而接下来引发了一系列的重新分配空间的内存重分配

展示如下:

当然删除一个小节点也会有同样的问题:如果e1至eN都是大小介于250字节至253字节的节点,big节点的长度大于等于254字节(需要5字节的previous_entry_length来保存),而small节点的长度小于254字节(只需要1字节的previous_entry_length来保存):

那么当我们将small节点从压缩列表中删除之后,为了让e1的previous_entry_length属性可以记录big节点的长度,程序将扩展e1的空间,并由此引发之后的连锁更新

因为连锁更新在最坏情况下需要对压缩列表执行N次空间重分配操作,而每次空间重分配的最坏复杂度为O(N),所以连锁更新的最坏复杂度为O(N^2)

要注意的是,尽管连锁更新的复杂度较高,但它真正造成性能问题的几率是很低的

  • 首先,压缩列表里要恰好有多个连续的、长度介于250字节至253字节之间的节点,连锁更新才有可能被引发,在实际中,这种情况并不多见
  • 其次,即使出现连锁更新,但只要被更新的节点数量不多,就不会对性能造成任何影响:比如说,对三五个节点进行连锁更新是绝对不会影响性能的

因为以上原因,ziplistPush等命令的平均复杂度仅为O(N)

数据对象下数据结构转换

每种数据对象由至少两种数据编码实现,但某个key在同一时间一定是某一个数据编码,数据编码会随着数据对象存储数据的变化而发生不可逆的切换

String类型对象

由上表可知,String类型有三种展现形式:int、embstr的sds实现,raw的sds实现,在不同的场景下使用不同的展现形式

编码类型[int/embstr sds->raw sds]

String类型对象包含如下的几种转换场景:

  • 整数场景:如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码设置为int。需要注意的是可以用long double类型表示的浮点数在Redis中也是作为字符串值来保存的

  • embstr的sds场景:如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值

  • raw的sds场景:如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw

    以上提到了两个编码方式embstr和raw,区别是什么呢?
  • 相同点:embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象
  • 不同点:raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr两个结构

既然创造了embstr,一定是有优势的:

  • 内存分配次数少:embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次
  • 内存释放快:释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数
  • 缓存利用率高:因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势

编码转换

int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换为raw编码的字符串对象

  1. int编码的字符串对象,如果我们向对象执行了一些命令,使得这个对象保存的不再是整数值,而是一个字符串值,那么字符串对象的编码将从int变为raw
  2. embstr编码的字符串对象实际上是只读的。当我们对embstr编码的字符串对象执行任何修改命令时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令。因为这个原因,embstr编码的字符串对象在执行修改命令之后,总会变成一个raw编码的字符串对象

以下是一些常用操作的对应关系:

List类型对象

列表对象的编码可以是ziplist或者linkedlist

编码类型[ziplist->linkedlist]

举个例子,如果我们执行以下RPUSH命令,那么服务器将创建一个列表对象作为numbers键的值:

  • ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。
  • linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素

    linkedlist编码的列表对象在底层的双端链表结构中包含了多个字符串对象,字符串对象是Redis五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象

编码转换

当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码,对于使用ziplist编码的列表对象来说,当使用ziplist编码所需的两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行,原本保存在压缩列表里的所有列表元素都会被转移并保存到双端链表里面,对象的编码也会从ziplist变为linkedlist

  • 列表对象保存的所有字符串元素的长度都小于64字节
  • 列表对象保存的元素数量小于512个

不能满足这两个条件的列表对象需要使用linkedlist编码,以上两个条件的上限值是可以通过配置文件修改的。以下是一些常用操作的对应关系:

Hash类型对象

哈希对象的编码可以是ziplist或者hashtable.

编码类型【ziplist->hashtable】

ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾:

  • 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后
  • 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向

指针指向的压缩列表表示如下:

hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:

  • 字典的每个键都是一个字符串对象,对象中保存了键值对的键
  • 字典的每个值都是一个字符串对象,对象中保存了键值对的值

hashtable实现方式如下:

编码转换

当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码,对于使用ziplist编码的列表对象来说,当使用ziplist编码所需的两个条件的任意一个不能被满足时,对象的编码转换操作就会被执行,原本保存在压缩列表里的所有键值对都会被转移并保存到字典里面,对象的编码也会从ziplist变为hashtable

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
  • 哈希对象保存的键值对数量小于512个

这两个条件的上限值是可以修改的。以下是一些编码操作的常用命令:

Set类型对象

集合对象的编码可以是intset或者hashtable

编码类型【inset->hashtable】

intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面

hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL

编码转换

当集合对象可以同时满足以下两个条件时,对象使用intset编码,对于使用intset编码的集合对象来说,当使用intset编码所需的两个条件的任意一个不能被满足时,就会执行对象的编码转换操作,原本保存在整数集合中的所有元素都会被转移并保存到字典里面,并且对象的编码也会从intset变为hashtable

  • 集合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不超过512个

不能满足这两个条件的集合对象需要使用hashtable编码,第二个条件的上限值是可以修改的,以下是一些常用命令:

ZSet类型对象

有序集合的编码可以是ziplist或者skiplist

编码类型

ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score),压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向

指针指向的压缩列表表示如下:

skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:

  • zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的
  • zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性实现的

有序集合每个元素的成员都是一个字符串对象,而每个元素的分值都是一个double类型的浮点数。值得一提的是,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,也不会因此而浪费额外的内存

其具体实现方式如下:

为什么同时使用两种方式实现

有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现,但无论单独使用字典还是跳跃表,在性能上对比起同时使用字典和跳跃表都会有所降低

  • 如果我们只使用字典来实现有序集合,那么虽然以O(1)复杂度查找成员的分值这一特性会被保留,但是,因为字典以无序的方式来保存集合元素,所以每次在执行范围型操作——比如ZRANK、ZRANGE等命令时,程序都需要对字典保存的所有元素进行排序,完成这种排序需要至少O(NlogN)时间复杂度,以及额外的O(N)内存空间(因为要创建一个数组来保存排序后的元素)
  • 如果我们只使用跳跃表来实现有序集合,那么跳跃表执行范围型操作的所有优点都会被保留,但因为没有了字典,所以根据成员查找分值这一操作的复杂度将从O(1)上升为O(logN)

字典用于快速查找分值,跳跃表用于执行范围操作

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
存储 消息中间件 NoSQL
redis入门到精通系列(一):入门redis看这一篇就够了
如果你是计算机专业学生 ,那么一定使用过关系型数据库mysql。在请求量小的情况下,使用mysql不会有任何问题,但是一旦同时有成千上万个请求同时来访问系统时,就会出现卡顿甚至系统崩溃的情况。最典型的例子就是早期的12306购票网站,一旦到了购票高峰期,12306肯定崩溃。造成这个原因的罪魁祸首就是关系型数据库。
4853 0
redis入门到精通系列(一):入门redis看这一篇就够了
|
6月前
|
NoSQL 网络安全 Redis
Redis进阶-Redis使用建议一二事
Redis进阶-Redis使用建议一二事
33 0
|
6月前
|
存储 NoSQL Linux
【Redis入门】 —— 关于Redis的一点儿知识
【Redis入门】 —— 关于Redis的一点儿知识
|
存储 缓存 NoSQL
前端了解这些 Redis 操作就够了
前端了解这些 Redis 操作就够了
1731 0
|
存储 NoSQL 安全
Redis看这一篇就够了(二)
Redis看这一篇就够了(二)
99 0
Redis看这一篇就够了(二)
|
存储 缓存 监控
Redis看这一篇就够了(四)
Redis看这一篇就够了(四)
63 0
Redis看这一篇就够了(四)
|
存储 消息中间件 SQL
Redis看这一篇就够了(一)
Redis看这一篇就够了
153 0
|
存储 缓存 监控
Redis看这一篇就够了(六)
Redis看这一篇就够了(六)
105 0
|
存储 消息中间件 缓存
Redis看这一篇就够了(五)
Redis看这一篇就够了(五)
155 0
|
缓存 NoSQL 算法
94. 熟悉Redis吗,项目中你是如何对Redis内存进行优化的(二)
94. 熟悉Redis吗,项目中你是如何对Redis内存进行优化的(二)
127 0
94. 熟悉Redis吗,项目中你是如何对Redis内存进行优化的(二)