redis空间优化
redis空间优化
1. 精简键名和键值
精简键名和键值是最直观的减少内存占用的方式; 但是要把握好度, 不要单纯为了节省空间而使用不容易理解的键名;
2. 内部编码
很多时候单靠精简键名和键值所减少的空间远远不够, 还需要根据redis内部编码规则来节省更多的空间, redis为每一种数据类型提供了至少两种编码方式:
数据类型 | 内部编码 | OBJECT ENCODING命令结果 |
---|---|---|
字符类型 | REDIS_ENCODING_RAW, REDIS_ENCODING_INT, REDIS_ENCODING_EMBSTR | “raw”, “int”, “embstr” |
列表类型 | REDIS_ENCODING_LINKEDLIST, REDIS_ENCODING_ZIPLIST | “linkedlist”, “ziplist” |
散列类型 | REDIS_ENCODING_HT, REDIS_ENCODING_ZIPLIST | “hashtable”, “ziplist” |
集合类型 | REDIS_ENCODING_HT, REDIS_ENCODING_INTSET | “hashtable”, “intset” |
有序集合类型 | REDIS_ENCODING_SKIPLIST, REDIS_ENCODING_ZIPLIST | “skiplist”, “ziplist” |
说明
- 如果想要查看一个键的内部编码方式, 可以使用命令 OBJECT ENCODING key;
- 以散列类型为例, 一般散列类型是以散列表实现的, 这样可以实现O(1)复杂度的查找和赋值; 但是让元素较少的时候O(1)和O(n)的区别并不是特别大, 这时候redis会自动采用一种更加紧凑但是性能可能差一点的编码方式; 当内部元素增加, redis又会自动调整为散列表;
- redis的每一个键值都一个redisObject机构体保存;
typedef struct redisObject
{
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
3. 字符串类型
redis使用一个sdshdr类型来存储字符串, 而redisObject的ptr字段执行该变量的地址.
/*
* 保存字符串对象的结构
*/
struct sdshdr
{
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
说明
-
存储键值所需要空间如何计算? 如set key foobar占用空间就是: sizeof(redisObject) + sizeof(sdshdr) + strlen(foobar) = 30;
-
如果键值可以使用64位有符号整型表示的时候, Redis将使用long类型来存储, 那么实际占用空间就是sizeof(redisObject) = 16, 例如 set key 123456;
-
redis在启动后会预先建立10000个分别存储0到9999这些数字的redisObject类型对象作为共享对象; 如果设置的键值在这个范围内, 就直接引用这个共享对象而不是重新建立redisObject, 此时存储键值占用的空间为0字节;
-
当通过配置文件maxmemory设置了Redis最大空间大小时候, Redis不会使用共享内存, 因为每个键值都需要使用一个redisObject来记录其LRU信息;
-
Redis3.0 引入了REDIS_ENCODING_EMBSTR的字符串编码方式, 与REDIS_ENCODING_RAW编码类似, 也是基于sdshdr实现;
-
键值内容超过39字节时候, Redis使用REDIS_ENCODING_EMBSTR编码, 当对使用REDIS_ENCODING_EMBSTR编码的键值进行任何修改操作时候, 自动转换为REDIS_ENCODING_RAW编码;
总结
使用字符串类型存储对象ID这种小的数字是非常的节省存储空间, redis只需要一个存储键名和一个共享对象的引用即可;
4. 散列类型
散列表编码方式有两种: REDIS_ENCODING_HT和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来存储, 转换过程透明;
ZIPLIST编码方式
- 紧凑型编码方式; 牺牲部分读取性能以换取较高的空间利用率, 适合元素较少的时候;
结构示意图:
说明:
- zlbytes 表示整个结构占用的空间;
- zltail 表示最后一个元素的偏移量; ztail可以是程序直接跳转到尾部而无需遍历, 执行尾部弹出等操作速度较快;
- zllen 存储的是元素的数量, 类型为uint16_t;
- zlen单字节表示, 标识结构的末尾, 值永远是255;
- REDIS_ENCODING_ZIPLIST中每个元素由四部分组成如图所示;
总结
关于散列类型的空间优化, 主要是在两种编码方式的切换时机上; 从结构可以看出, 使用编码方式REDIS_ENCODING_ZIPLIST的时候, 做值的更新, 首先需要找到key, 然后删除, 然后更新, 删除和插入都是需要移动后面的元素的, 可见效率有多低;
所以, 建议不要将配置中hash-max-ziplist-entries和hash-max-ziplist-value的值设置的过小;
5. 列表类型
列表类型内部编码方式为: REDIS_ENCODING_LINKEDLIST 和 REDIS_ENCODING_ZIPLIST;
- REDIS_ENCODING_LINKEDLIST 双向链表, 链表的每个元素都是redisObject, 这种情况下优化方式和字符串类型一样;
- REDIS_ENCODING_ZIPLIST 编码方式具体的表现和散列表一样;
- 新版本增加了REDIS_ENCODING_QUICKLIST编码方式, 该编码方式是上面两种编码方式的结合; 原理是将一个长列表分成若干个以链表形式组成的ziplist;
QUICKLIST的结构是一个空间和时间上的折中, 首先要了解LINKDLIST和ZIPLIST的区别:
- 双向链表便于在表的两端进行push和pop操作, 但是内存开销大, 每个节点需要额外的保存两个指针; 双向链表个节点是单独内存块, 地址不连续, 易产生内存碎片;
- ziplist是一整块连续内存; 存储效率很高, 但不利于修改操作, 每一次变动都会引起一次内存的realloc; 特别是ziplist长度过长的时候, 一个realloc会导致大批量的数据拷贝, 性能低;
QUICKLIST正是结合了两者的优点; 但是QUICKLIST包含多长的ziplist合适. 长度是由配置文件中下面配置项控制:
//默认是-2 表示QUICKLIST中ziplist不超过8kb
list-max-ziplist-size -2
6. 集合类型
集合类型编码方式为: REDIS_ENCODING_HT和REDIS_ENCODING_INTSET; 当集合中所有的元素都是整数且元素的个数小于配表中的set-max-intset-entries参数指定的值时候, Redis会使用REDIS_ENCODING_INTSET编码存储该集合, 否则使用REDIS_ENCODING_HT存储;
REDIS_ENCODING_INTSET编码存储的结构体如下:
typedef struct intset
{
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
- 根据encoding不同, 每个元素占用的字节大小不同, 默认的encoding是INTSET_ENC_INT16, 当新增元素无法使用两个字节表示时候, 会升级为INTSET_ENC_INT32或者INTSET_ENC_INT64; 并调整之前元素的位置和长度;
- REDIS_ENCODING_INTSET一有序的方式存储元素, 无论是添加还是删除元素, redis都需要调整后面元素的内存位置; 元素多时性能差;
7. 有序集合类型
内部编码是: REDIS_ENCODING_SKIPLIST和REDIS_ENCODING_ZIPLIST; 切换时机根据配置文件下面配置项:
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
具体规则和散列类型以及列表类型一样;
cuipf
