异教徒与巫师绿色中文版
1.26G · 2025-11-17
计算Redis容量,并不只是仅仅计算key占多少字节,value占多少字节,因为Redis为了维护自身的数据结构,也会占用部分内存,本文章简单介绍每种数据类型(String、Hash、Set、ZSet、List)占用内存量,供做Redis容量评估时使用。当然,大多数情况下,key和value就是主要占用,能解大部分问题
在看这里之前,可以先看一下底层 - 数据结构 这篇文章
jemalloc是一种通用的内存管理方法,着重于减少内存碎片和支持可伸缩的并发性,做redis容量评估前必须对jemalloc的内存分配规则有一定了解。
jemalloc基于申请内存的大小把内存分配分为三个等级:small,large,huge:
对于64位系统,一般chunk大小为4M,页大小为4K,内存分配的具体规则如下:
jemalloc在分配内存块时会分配大于实际值的2^n的值,例如实际值时6字节,那么会分配8字节
| 数据类型 | 占用量 |
|---|---|
| dicEntry | 主要包括3个指针,key、value、哈希冲突时下个指针,耗费容量为8*3=24字节,jemalloc会分配32字节的内存块 |
| dict结构 | 88字节,jemalloc会分配 96 字节的内存块 |
| redisObject | type(4bit)、encoding(4bit)、lru(24bit)、int(8byte)、ptr指针(8byte)。因此redisObject结构占用(4+4+24)/8 +4+8 = 16字节。 |
| key_SDS | key的长度 + 9,jemalloc分配 >= 该值的2^n的值 |
| val_SDS | value的长度 + 9,jemalloc分配 >= 该值的2^n的值 |
| key的个数 | 所有的key的个数 |
| bucket个数 | 大于key的个数的2^n次方,例如key个数是2000,那么bucket=2048 |
| 指针大小 | 8 byte |
Redis内存占用主要可以划分为如下几个部分:
数据:Redis数据占用内存dataset.bytes包括key-value占用内存、dicEntry占用内存、SDS占用内存等。
初始化内存:redis启动初始化时使用的内存startup.allocated,属于额外内存overhead.total的一部分。
主从复制内存:用于主从复制,属于额外内存一部分。
缓冲区内存:缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF缓冲区等;其中,客户端缓冲存储客户端连接的输入输出缓冲;复制积压缓冲用于部分复制功能;AOF缓冲区用于在进行AOF重写时,保存最近的写入命令。在了解相应功能之前,不需要知道这些缓冲的细节;这部分内存由jemalloc分配,因此会统计在used_memory中。
内存碎片:内存碎片是Redis在分配、回收物理内存过程中产生的。例如,如果对数据的更改频繁,而且数据之间的大小相差很大,可能导致redis释放的空间在物理内存中并没有释放,但redis又无法有效利用,这就形成了内存碎片。
redis容量评估模型根据key类型而有所不同。
一个简单的set命令最终会产生4个消耗内存的结构,中间free掉的不考虑:
当key个数逐渐增多,redis还会以rehash的方式扩展哈希表节点数组,即增大哈希表的bucket个数,每个bucket元素都是个指针(dictEntry*),占8字节,bucket个数是超过key个数向上求整的2的n次方。
真实情况下,每个结构最终真正占用的内存还要考虑jemalloc的内存分配规则,综上所述,string类型的容量评估模型为:
总内存消耗 = (dictEntry大小 + redisObject大小 +key_SDS大小 + val_SDS大小)×key个数 + bucket个数 ×指针大小
即:
总内存消耗 = (32 + 16 + key_SDS大小 + val_SDS大小)×key个数 + bucket个数 × 8
string类型容量评估测试脚本如下:
#!/bin/sh
old_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "before test, memory used: $old_memory"
for((i=1000; i<3000; i++))
do
./redis-cli -h 0 -p 10009 set test_key_$i test_value_$i > /dev/null
sleep 0.2
done
new_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "after test, memory used: $new_memory"
let difference=new_memory-old_memory
echo "difference is: $difference"
测试用例中,key长度为 13,value长度为15,key个数为2000,根据上面总结的容量评估模型,容量预估值为2000 ×(32 + 16 + 32 + 32) + 2048× 8 = 240384
运行测试脚本,得到结果如下:
结果都是240384,说明模型预估的十分精确。
哈希对象的底层实现数据结构可能是listpack或者hashtable,当同时满足下面这两个条件时,哈希对象使用listpack这种结构(此处列出的条件都是redis默认配置,可以更改):
可以看出,业务侧真实使用场景基本都不能满足这两个条件,所以哈希类型大部分都是hashtable结构,因此本篇文章只讲hashtable。
与string类型不同的是,hash类型的值对象并不是指向一个SDS结构,而是指向又一个dict结构,dict结构保存了哈希对象具体的键值对,hash类型结构关系如图所示:
一个hmset命令最终会产生以下几个消耗内存的结构:
因为hash类型内部有两个dict结构,所以最终会有产生两种rehash,一种rehash基准是field个数,另一种rehash基准是key个数,结合jemalloc内存分配规则,hash类型的容量评估模型为:
总内存消耗 = [(redisObject大小 ×2 +field_SDS大小 + val_SDS大小 + dictEntry大小)× field个数 + field_bucket个数× 指针大小 + dict大小 + redisObject大小 +key_SDS大小 + dictEntry大小 ] × key个数 + key_bucket个数 × 指针大小
即:
总内存消耗 = [(16 ×2 +field_SDS大小 + val_SDS大小 + 32)× field个数 + field_bucket个数× 8 + 96 + 16 +key_SDS大小 + 32 ] × key个数 + key_bucket个数 × 8
hash类型容量评估测试脚本如下:
#!/bin/sh
value_prefix="test_value_123456789012345678901234567890123456789012345678901234567890_"
old_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "before test, memory used: $old_memory"
for((i=100; i<300; i++))
do
for((j=100; j<300; j++))
do
./redis-cli -h 0 -p 10009 hset test_key_$i test_field_$j $value_prefix$j > /dev/null
done
sleep 0.5
done
new_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "after test, memory used: $new_memory"
let difference=new_memory-old_memory
echo "difference is: $difference"
测试用例中,key长度为 12,field长度为14,value长度为75,key个数为200,field个数为200,根据上面总结的容量评估模型,容量预估值为[(16 + 16 + 32 + 96 + 32)×200 + 256×8 + 96 + 16 + 32 + 32 ]× 200 + 256× 8 = 8126848
运行测试脚本,得到结果如下:
结果相差40,说明模型预测比较准确。
同哈希对象类似,有序集合对象的底层实现数据结构也分两种:listpack或者skiplist,当同时满足下面这两个条件时,有序集合对象使用ziplist这种结构(此处列出的条件都是redis默认配置,可以更改):
业务侧真实使用时基本都不能同时满足这两个条件,因此这里只讲skiplist结构的情况。skiplist类型的值对象指向一个zset结构,zset结构同时包含一个字典和一个跳跃表,占用的总字节数为16,具体定义如下(redis.h/zset):
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素,dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素,这两种数据结构会通过指针来共享相同元素的成员和分值,没有浪费额外的内存。zset类型的结构关系如图所示:
一个zadd命令最终会产生以下几个消耗内存的结构:
因为每个zskiplistNode节点的层数都是根据幂次定律随机生成的,而容量评估需要确切值,因此这里采用概率中的期望值来代替单个节点的大小,结合jemalloc内存分配规则,经计算,单个zskiplistNode节点大小的期望值为53.336。
zset类型内部同样包含两个dict结构,所以最终会有产生两种rehash,一种rehash基准是成员个数,另一种rehash基准是key个数,zset类型的容量评估模型为:
总内存消耗 = [(val_SDS大小 + redisObject大小 + zskiplistNode大小 + dictEntry大小)×value个数 +value_bucket个数 ×指针大小 + 32层zskiplistNode大小 + zskiplist大小 + dict大小 + zset大小 + redisObject大小 + key_SDS大小 + dictEntry大小 ] ×key个数 +key_bucket个数 × 指针大小
即:
总内存消耗 = [(val_SDS大小 + 16 + 53.336 + 32)×value个数 +value_bucket个数 × 8 + 640 +32 + 96 + 16 + 16 + key_SDS大小 + 32 ] ×key个数 +key_bucket个数 × 8
zset类型容量评估测试脚本如下:
#!/bin/sh
value_prefix="test_value_123456789012345678901234567890123456789012345678901234567890_"
old_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "before test, memory used: $old_memory"
for((i=100; i<300; i++))
do
for((j=100; j<300; j++))
do
./redis-cli -h 0 -p 10009 zadd test_key_$i $j $value_prefix$j > /dev/null
done
sleep 0.5
done
new_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "after test, memory used: $new_memory"
let difference=new_memory-old_memory
echo "difference is: $difference"
测试用例中,key长度为 12,value长度为75,key个数为200,value个数为200,根据上面总结的容量评估模型,容量预估值为[(96 + 16 + 53.336 + 32)×200 + 256×8 + 640 + 32 + 96 + 16 + 16 + 32 + 32 ] ×200 + 256 × 8 = 8477888
运行测试脚本,得到结果如下:
结果相差672,说明模型预测比较准确。
列表对象的底层实现数据结构同样分两种:listpack或者linkedlist,当同时满足下面这两个条件时,列表对象使用listpack这种结构(此处列出的条件都是redis默认配置,可以更改):
因为实际使用情况,这里同样只讲linkedlist结构。linkedlist类型的值对象指向一个list结构,具体结构关系如图所示:
一个rpush或者lpush命令最终会产生以下几个消耗内存的结构:
list类型内部只有一个dict结构,rehash基准为key个数,综上,list类型的容量评估模型为:
总内存消耗 = [(val_SDS大小 + redisObject大小 + listNode大小)× value个数 + list大小 + redisObject大小 + key_SDS大小 + dictEntry大小 ] × key个数 + key_bucket个数 × 指针大小
即:
总内存消耗 = [(val_SDS大小 +16 + 32)× value个数 + 16 + 32 + key_SDS大小 + 32 ] × key个数 + key_bucket个数 × 8
list类型容量评估测试脚本如下:
#!/bin/sh
value_prefix="test_value_123456789012345678901234567890123456789012345678901234567890_"
old_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "before test, memory used: $old_memory"
for((i=100; i<300; i++))
do
for((j=100; j<300; j++))
do
./redis-cli -h 0 -p 10009 rpush test_key_$i $value_prefix$j > /dev/null
done
sleep 0.5
done
new_memory=`./redis-cli -h 0 -p 10009 info|grep used_memory:|awk -F: '{printf "%d", $2}'`
echo "after test, memory used: $new_memory"
let difference=new_memory-old_memory
echo "difference is: $difference"
测试用例中,key长度为 12,value长度为75,key个数为200,value个数为200,根据上面总结的容量评估模型,容量预估值为[(96 + 16 + 32) ×200 + 48 + 16 + 32 + 32 ] × 200 + 256 ×8 = 5787648
运行测试脚本,得到结果如下:
结果都是5787648,说明模型预估的十分精确。
一个sadd命令最终会产生以下几个消耗内存的结构:
set与hash类似,只是value部分没有具体的值。与hash类型一样,内部有两个dict结构,所以最终会有产生两种rehash,一种rehash基准是member个数,另一种rehash基准是key个数,结合jemalloc内存分配规则,hash类型的容量评估模型为:
总内存消耗 = [(redisObject大小 +member_SDS大小 + dictEntry大小)× member个数 + member_bucket个数× 指针大小 + dict大小 + redisObject大小 +key_SDS大小 + dictEntry大小 ] × key个数 + key_bucket个数×指针大小
即:
总内存消耗 = [(16 +member_SDS大小 + 32)× member个数 + member_bucket个数× 8 + 96 + 16 +key_SDS大小 + 32 ] × key个数 + key_bucket个数×8