Redis数据类型内部编码规则及优化方式
Redis的每个键值都是使用一个redisObject结构体保存的,redisObject的定义如下:
typedef struct redisObject {
unsigned type:4;
unsigned notused:2; /* Not used */
unsigned encoding:4;
unsigned lru:22; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
1.字符串类型
Redis使用一个sdshdr类型的变量来存储字符串,而redisObject的ptr字段指向的是该变量的地址。sdshdr的定义如下:
struct sdshdr {
int len;
int free;
char buf[];
};
其中len字段表示的是字符串的长度,free字段表示buf中的剩余空间,而buf字段存储的才是字符串的内容。
所以当执行 SET key foobar时,存储键值需要占用的空间是 sizeof(redisObject) + sizeof(sdshdr) + strlen("foobar") = 30字节[8] ,如图4-4所示。
而当键值内容可以用一个64位有符号整数表示时,Redis会将键值转换成long类型来存储。如 SET key 123456,实际占用的空间是 sizeof(redisObject) = 16 字节,比存储"foobar"节省了一半的存储空间,如图4-5所示。
图4-4 字符串键值"foobar"使用 RAW 编码时的存储结构
图4-5 字符串键值"123456"的内存结构
redisObject中的refcount字段存储的是该键值被引用数量,即一个键值可以被多个键引用。Redis启动后会预先建立10000个分别存储从0到9999这些数字的redisObject类型变量作为共享对象,如果要设置的字符串键值在这10000个数字内(如 SET key1 123)则可以直接引用共享对象而不用再建立一个 redisObject 了,也就是说存储键值占用的空间是0字节,如图4-6所示。
由此可见,使用字符串类型键存储对象ID这种小数字是非常节省存储空间的,Redis只需存储键名和一个对共享对象的引用即可。
图4-6 当执行了 SET key1 123 和 SET key2 123 后,key1 和 key2两个键都直接引用了一个已经建立好的共享对象,节省了存储空间
提示 当通过配置文件参数 maxmemory 设置了 Redis 可用的最大空间大小时,Redis不会使用共享对象,因为对于每一个键值都需要使用一个 redisObject 来记录其LRU信息。
此外Redis 3.0新加入了 REDIS_ENCODING_EMBSTR 的字符串编码方式,该编码方式与REDIS_ENCODING_RAW类似,都是基于sdshdr实现的,只不过sdshdr的结构体与其对应的分配在同一块连续的内存空间中,如图4-7所示。
图4-7 字符串键值"foobar"使用 EMBSTR 编码时的存储结构
使用REDIS_ENCODING_EMBSTR编码存储字符串后,不论是分配内存还是释放内存,所需要的操作都从两次减少为一次。而且由于内存连续,操作系统缓存可以更好地发挥作用。当键值内容不超过39字节时,Redis 会采用 REDIS_ENCODING_EMBSTR编码,同时当对使用REDIS_ENCODING_EMBSTR编码的键值进行任何修改操作时(如APPEND命令), Redis会将其转换成REDIS_ENCODING_RAW编码。
2.散列类型
散列类型的内部编码方式可能是 REDIS_ENCODING_HT 或 REDIS_ENCODING_ZIPLIST[9] 。在配置文件中可以定义使用REDIS_ENCODING_ZIPLIST方式编码散列类型的时机:
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
当散列类型键的字段个数少于hash-max-ziplist-entries参数值且每个字段名和字段值的长度都小于 hash-max-ziplist-value 参数值(单位为字节)时,Redis 就会使用 REDIS_ ENCODING_ZIPLIST 来存储该键,否则就会使用 REDIS_ENCODING_HT。转换过程是透明的,每当键值变更后Redis都会自动判断是否满足条件来完成转换。
REDIS_ENCODING_HT编码即散列表,可以实现O(1)时间复杂度的赋值取值等操作,其字段和字段值都是使用 redisObject 存储的,所以前面讲到的字符串类型键值的优化方法同样适用于散列类型键的字段和字段值。
提示 Redis的键值对存储也是通过散列表实现的,与 REDIS_ENCODING_HT 编码方式类似,但键名并非使用 redisObject 存储,所以键名"123456"并不会比"abcdef"占用更少的空间。之所以不对键名进行优化是因为绝大多数情况下键名都不会是纯数字。
补充知识 Redis 支持多数据库,每个数据库中的数据都是通过结构体 redisDb 存储的。redisDb的定义如下:
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id;
} redisDb;
dict类型就是散列表结构,expires存储的是数据的过期时间。当Redis启动时会根据配置文件中 databases参数指定的数量创建若干个 redisDb类型变量存储不同数据库中的数据。
REDIS_ENCODING_ZIPLIST 编码类型是一种紧凑的编码格式,它牺牲了部分读取性能以换取极高的空间利用率,适合在元素较少时使用。该编码类型同样还在列表类型和有序集合类型中使用。REDIS_ENCODING_ZIPLIST 编码结构如图 4-8 所示,其中 zlbytes是 uint32_t类型,表示整个结构占用的空间。zltail也是 uint32_t类型,表示到最后一个元素的偏移,记录 zltail 使得程序可以直接定位到尾部元素而无需遍历整个结构,执行从尾部弹出(对列表类型而言)等操作时速度更快。zllen是uint16_t类型,存储的是元素的数量。zlend是一个单字节标识,标记结构的末尾,值永远是255。
图4-8 REDIS_ENCODING_ZIPLIST编码的内存结构
在REDIS_ENCODING_ZIPLIST中每个元素由4个部分组成。
第一个部分用来存储前一个元素的大小以实现倒序查找,当前一个元素的大小小于254字节时第一个部分占用1个字节,否则会占用5个字节。
第二、三个部分分别是元素的编码类型和元素的大小,当元素的大小小于或等于 63个字节时,元素的编码类型是ZIP_STR_06B(即0<<6),同时第三个部分用6个二进制位来记录元素的长度,所以第二、三个部分总占用空间是1字节。当元素的大小大于63且小于或等于16383字节时,第二、三个部分总占用空间是2字节。当元素的大小大于16383字节时,第二、三个部分总占用空间是5字节。
第四个部分是元素的实际内容,如果元素可以转换成数字的话Redis会使用相应的数字类型来存储以节省空间,并用第二、三个部分来表示数字的类型(int16_t、int32_t等)。
使用REDIS_ENCODING_ZIPLIST编码存储散列类型时元素的排列方式是:元素1存储字段1,元素2存储字段值1,依次类推,如图4-9所示。
例如,当执行命令 HSET hkey foo bar命令后,hkey键值的内存结构如图4-10所示。
图4-9 使用 REDIS_ENCODING_ZIPLIST编码存储散列类型的内存结构
图4-10 hkey键值的内存结构
下次需要执行 HSET hkey foo anothervalue时Redis需要从头开始找到值为 foo的元素(查找时每次都会跳过一个元素以保证只查找字段名),找到后删除其下一个元素,并将新值anothervalue插入。删除和插入都需要移动后面的内存数据,而且查找操作也需要遍历才能完成,可想而知当散列键中数据多时性能将很低,所以不宜将 hash-max-ziplist-entries和hash-max-ziplist-value两个参数设置得很大。
3.列表类型
列表类型的内部编码方式可能是 REDIS_ENCODING_LINKEDLIST或 REDIS ENCODING_ZIPLIST。同样在配置文件中可以定义使用REDIS_ENCODING_ZIPLIST方式编码的时机:
list-max-ziplist-entries 512
list-max-ziplist-value 64
具体转换方式和散列类型一样,这里不再赘述。
REDIS_ENCODING_LINKEDLIST编码方式即双向链表,链表中的每个元素是用redis Object 存储的,所以此种编码方式下元素值的优化方法与字符串类型的键值相同。
而使用 REDIS_ENCODING_ZIPLIST 编码方式时具体的表现和散列类型一样,由于REDIS_ENCODING_ZIPLIST 编码方式同样支持倒序访问,所以采用此种编码方式时获取两端的数据依然较快。
Redis最新的开发版本新增了 REDIS_ENCODING_QUICKLIST编码方式,该编码方式是REDIS_ENCODING_LINKEDLIST和REDIS_ENCODING_ZIPLIST的结合,其原理是将一个长列表分成若干个以链表形式组织的 ziplist,从而达到减少空间占用的同时提升REDIS_ENCODING_ZIPLIST编码的性能的效果。
4.集合类型
集合类型的内部编码方式可能是 REDIS_ENCODING_HT 或 REDIS_ENCODING_INTSET。当集合中的所有元素都是整数且元素的个数小于配置文件中的set-max-intset-entries参数指定值(默认是512)时Redis会使用REDIS_ENCODING_INTSET编码存储该集合,否则会使用REDIS_ENCODING_HT来存储。
REDIS_ENCODING_INTSET编码存储结构体intset的定义是:
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
其中contents存储的就是集合中的元素值,根据encoding的不同,每个元素占用的字节大小不同。默认的encoding是INTSET_ENC_INT16(即2个字节),当新增加的整数元素无法使用 2 个字节表示时,Redis 会将该集合的 encoding 升级为 INTSET_ENC_INT32(即4个字节)并调整之前所有元素的位置和长度,同样集合的encoding还可升级为INTSET_ENC_INT64(即8个字节)。
REDIS_ENCODING_INTSET编码以有序的方式存储元素(所以使用SMEMBERS命令获得的结果是有序的),使得可以使用二分算法查找元素。然而无论是添加还是删除元素, Redis 都需要调整后面元素的内存位置,所以当集合中的元素太多时性能较差。
当新增加的元素不是整数或集合中的元素数量超过了set-max-intset-entries参数指定值时,Redis会自动将该集合的存储结构转换成REDIS_ENCODING_HT。
注意 当集合的存储结构转换成 REDIS_ENCODING_HT 后,即使将集合中的所有非整数元素删除,Redis也不会自动将存储结构转换回 REDIS_ENCODING_INTSET。因为如果要支持自动回转,就意味着Redis在每次删除元素时都需要遍历集合中的键来判断是否可以转换回原来的编码,这会使得删除元素变成了时间复架度为O(n)的操作。
5.有序集合类型
有序集合类型的内部编码方式可能是 REDIS_ENCODING_SKIPLIST 或 REDIS_ENCODING_ZIPLIST。同样在配置文件中可以定义使用REDIS_ENCODING_ZIPLIST方式编码的时机:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
具体规则和散列类型及列表类型一样,不再赘述。
当编码方式是REDIS_ENCODING_SKIPLIST时,Redis使用散列表和跳跃列表(skip list)两种数据结构来存储有序集合类型键值,其中散列表用来存储元素值与元素分数的映射关系以实现O(1)时间复杂度的 ZSCORE 等命令。跳跃列表用来存储元素的分数及其到元素值的映射以实现排序的功能。Redis对跳跃列表的实现进行了几点修改,其中包括允许跳跃列表中的元素(即分数)相同,还有为跳跃链表每个节点增加了指向前一个元素的指针以实现倒序查找。
采用此种编码方式时,元素值是使用 redisObject 存储的,所以可以使用字符串类型键值的优化方式优化元素值,而元素的分数是使用double类型存储的。
使用REDIS_ENCODING_ZIPLIST编码时有序集合存储的方式按照“元素1的值,元素1的分数,元素2的值,元素2的分数”的顺序排列,并且分数是有序的。
Redis数据类型内部编码规则及优化方式的更多相关文章
- 04Redis入门指南笔记(内部编码规则简介)
Redis是一个基于内存的数据库,所有的数据都存储在内存中.所以如何优化存储,减少内存空间占用是一个非常重要的话题.精简键名和键值是最直观的减少内存占用的方式,如将键名very.important.p ...
- Redis5种常用数据类型的使用以及内部编码
String 字符串类型是redis的最基本类型,首先无论值是什么数据类型,其键都是字符串,且其他数据类型的数据结构都是在字符串的基础上搭建的,相信读者能够体会到字符串在redis的地位是有多么的重要 ...
- 面试官问我redis数据类型,我回答了8种
面试官:小明呀,redis 有几种数据结构呀? 小明:8 种 面试官:那你说一下分别是什么? 小明:raw,int,ht,zipmap,linkedlist,ziplist,intset,skipli ...
- openssl ans.1编码规则分析及证书密钥编码方式
1 数据编码格式 openssl的数据编码规则是基于ans.1的,ans.1是什么 ? 先上高大上的解释 ASN.1(Abstract Syntax Notation One), 是一种结构化的描述语 ...
- Redis的五种数据结构的内部编码
type命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串).hash(哈希). list(列表).set(集合).zset(有序集合),但这些只是Redis对外的数据结构. 实 ...
- 高可用Redis(一):通用命令,数据结构和内部编码,单线程架构
1.通用API 1.1 keys命令和dbsize命令 keys * 遍历所有key keys [pattern] 遍历模式下所有的key dbsize 计算Redis中所有key的总数 例子: 12 ...
- Redis入门到高可用(四)—— Redis的五种数据结构的内部编码
Redis的五种数据结构的内部编码
- Redis数据类型、两种模型、事务、内部命令
1.redis数据类型 a.字符串,使用场景:常规key-value缓存应用 set name lixiang get name append name 123 # 字符串追加 mset key va ...
- [翻译] C# 8.0 新特性 Redis基本使用及百亿数据量中的使用技巧分享(附视频地址及观看指南) 【由浅至深】redis 实现发布订阅的几种方式 .NET Core开发者的福音之玩转Redis的又一傻瓜式神器推荐
[翻译] C# 8.0 新特性 2018-11-13 17:04 by Rwing, 1179 阅读, 24 评论, 收藏, 编辑 原文: Building C# 8.0[译注:原文主标题如此,但内容 ...
随机推荐
- CSS学习(三)特指度和层叠
一.特指度 特制度的一般形式是0,0,0,0 行内样式,第一位的特指度加一 id选择符,第二位的特指度加一 类选择符.属性选择符.伪类,第三位的特指度加一 元素选择符.伪元素,第四位的特指度加一 特指 ...
- vi/vim 常用命令总结
目录 Linux vi/vim编辑 vim键盘图 vim的三种模式 命令模式.输入模式.输出模式 vim使用实例 vi/vim按键说明 第一部分:一般模式可用的光标移动.复制粘贴.搜索替换等 第二部分 ...
- 菜鸡的Java笔记 - java 断言
断言:assert (了解) 所谓的断言指的是在程序编写的过程之中,确定代码执行到某行之后数据一定是某个期待的内容 范例:观察断言 public class Abnorma ...
- 菜鸡的Java笔记 开发支持类库
开发支持类库 SupportClassLibrary 观察者设计模式的支持类库 content (内容) 什么是观察者设计模式呢? ...
- R数据分析:二分类因变量的混合效应,多水平logistics模型介绍
今天给大家写广义混合效应模型Generalised Linear Random Intercept Model的第一部分 ,混合效应logistics回归模型,这个和线性混合效应模型一样也有好几个叫法 ...
- Kubernetes 入门基础
我们要学习 Kubernetes,就有首先了解 Kubernetes 的技术范围.基础理论知识库等,要学习 Kubernetes,肯定要有入门过程,在这个过程中,学习要从易到难,先从基础学习. 接下来 ...
- [bzoj1927]星际竞速
考虑没有爆发,那么相当于是带权最小不可交路径覆盖,由于只能从编号小的到编号大的,因此一定是DAG,而DAG的最小路径覆盖可以拆点并跑最大流,那么带权的只需要跑费用流即可(S向i连(1,0)的边,i'向 ...
- [atAGC106E]Medals
暴力二分答案+网络流,点数为$o(nk)$,无法通过 考虑Hall定理,即有完美匹配当且仅当$\forall S\subseteq V_{left}$,令$S'=\{x|\exists y\in V_ ...
- IE 跨域设置
开发的时候会发现IE下跨域无法访问,报错: Failed to load resource: net::ERR_CONNECTION_REFUSED 解决方法有两种: 自己写代理服务,访问代理服务,代 ...
- 重新整理 .net core 实践篇——— 权限中间件源码阅读[四十六]
前言 前面介绍了认证中间件,下面看一下授权中间件. 正文 app.UseAuthorization(); 授权中间件是这个,前面我们提及到认证中间件并不会让整个中间件停止. 认证中间件就两个作用,我们 ...