直接上测试结果, 向redis
中写入值(KV相同)为5000001..5100000
共10万个字符串类型KV
数据.
先自以为是的单计算一下需要多少内存: K/V相同, 均为整形, 各占用8字节, 那么一条数据会占用16字节. 10万数据大概占用155kb. 但事实真的是这样么? (如果是这样我也不用写这篇文章了)
实际测试一下, 这是一个刚刚启动的redis
状态, 还没有写入任何数据:
使用脚本向其中set
这10万条数据后:
怎么说? 是不是发现比之前计算的数据要大的多? 算一下每条数据占用的内存大小: (7520440-871864)/100000≈66
也就是说, 只是简单的整形KV
, redis
写入内存后也会占用66字节. 啊, 这?
为了解释这个问题, 我研究了字符串类型在redis
中的实现.
为了搞懂这个问题, 我去翻了Redis
的源码. 以下源码基于Redis5.0
首先, Redis
在内存中使用一个map
来记录所有的K/V
映射关系. 其实现就是比较常见的数组加链表. 每一个元素使用结构体dictEntry保存, 源码如下:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
其中三个字段各占用8字节, 用于保存元素的结构体占用16字节. next
字段用于链表指向下个元素, k/v
均是指向redisObject类型的指针(value 的整形先跳过, 后面会提到), 其源码如下:
#define LRU_BITS 24
typedef struct redisObject {
unsigned type:4; // 4bit
unsigned encoding:4; // 4bit
unsigned lru:LRU_BITS; // 3字节
int refcount; // 4字节
void *ptr; // 8字节
} robj;
RedisObject
类型共占用16字节. 很明显, 对应的数据就保存在ptr
指针中. Redis
中保存字符串有几种情况, 我用一张小图简单说明不同情况下redisObject
的数据存放.
为了减少内存占用, Redis
在存储的时候根据不同的值使用了不同的存储方式. 为什么是44B 呢? 在注释中给出了解释, 是为了令连续内存占用为64B. 至于sds
结构, 这里不展开了.
至此, 我们可以重新计算内存占用了, 每个元素的内存占用大致如下:
加起来就是8*7=56B
. 但是这跟我们实际测量的66B
还是有些差距的呀? 经过查找, Redis
使用的内存分配库为jemalloc, 而jemalloc
在内存分配的时候申请的内存为2的n 次方. 因此, 虽然dictEntry
只需要24B 的内存, 但还是会分配32B.
这样一来, 每个元素的内存占用就是: 32+8*4=64B
, 与我们之前分析的几乎没差.
在刚刚看dictEntry
的时候, 还有个疑惑, 就是value
是一个union
, 整形可以直接保存在value
变量中, 不需要额外的redisObject
, 这样的话, 不就可以节省16B 么?
是的, 没有错. 如果使用incr key
命令将值放进去, 你就会发现内存占用减少了, 每个元素占用大概48B
至此, 经过查找, 我们发现, 在向Redis
中使用字符串类型保存整形的时候, 虽然我们以为每个元素只需要16B, 但最终还是会占用64B, 这意味着如果需要存储10G 的数据, 需要至少40G 的内存才能放得下, 这着实有些夸张了. 当存储的元素本身不大的时候, 元素所带的额外信息就会是内存占用的大头了.
那么对于我们遇到的这种情况, 有没有什么办法能够减少内存占用呢?
通过查找, 发现了压缩列表这个结构, 在Redis
的注释中对其进行了详细的解释.
在元素不多的时候, hash/set/list
等均使用ziplist
, 其优势是可以减少内存占用, 但缺点是时间复杂度较高. 可以使用命令object encoding key
查看元素内部类型
于是, 我尝试着将k=5012345 v=5012345
的数据, 改为k=5012 field=345 v=5012345
的形式以hash
的方式进行存储, 这里为了使用ziplist
, 修改了配置文件hash-max-ziplist-entries=1024
.当我将这10万条数据以hash
的方式放进去后, 浅看结果:
我想到内存占用会降低一些, 但没想到低了这么多, 每个元素占用内存大概: (1824216-871952)/100000≈10B
.
其中必然是存在这一些共享内容的, 比如元素5012345/5112345
的field
均为345
.
但是, 这个内存占用着实有些令我震惊了, 回头抽时间好好研究研究压缩列表这玩意.
注意, hash-max-ziplist-entries
配置不要盲目的调高, 压缩列表虽然会降低内存占用, 但是会将时间复杂度降低到 O(n).