一、线程数目
- 操作数据结构的语句(即执行指令)是主线程(单线程,天然具备隔离性、不需要加锁)
- 别的线程主要用于优化:避免 IO 密集型和 CPU 密集型过多占用主线程
二、IO 密集型层面的优化
1. 磁盘 IO
- rdb:通过 fork 子进程来持久化
- aof:有异步刷盘机制(可选)
2. 网络 IO
- 通过 reactor 模型管理多个连接(主线程)
- 数据的接收,数据发送分摊到多个 io 子进程中
三、CPU 密集型层面的优化
- 分治思想:协议的解析(命令接收完之后)、协议的加密(响应回复前)分摊到多个 io 子进程
- 数据结构的切换:value 的不同数据结构底层存储方式在数据量不同情况下采取不同的策略(在执行效率与空间占比间保持平衡)
- 渐进式数据迁移:rehash 的两种方式:
- 每执行一条指令 copy 一个槽位
- 当服务器空闲时,定时 1ms 进行 rehash 的数据迁移
四、数据结构层面优化
| 编码类型 | 特点(内存 / 性能) | 适用场景 |
|---|
| 紧凑编码(ziplist/intset/embstr) | 内存占用极少,存储密度高,但增删改需要频繁重排内存 | 数据量小、访问不频繁 |
| 高效编码(dict/skiplist/raw) | 内存占用高(有结构开销),但增删改查效率高(O (1)/O (logN)) | 数据量大、访问频繁 |
简单说:数据量小时用'紧凑编码'省内存,数据量大时用'高效编码'保性能。
1. 哈希(ziplist → dict)
- ziplist(压缩列表):
- 内存上:所有键值对 / 元素连续存储,无冗余指针(dict 每个键值对要存两个指针,skiplist 每个节点有多层指针),内存密度是 dict 的 1/5 以上;
- 性能上:增删改需要重排整个列表(O (n)),但数据量小(≤512/128 个元素)时,n 很小,性能损耗可忽略;
- dict/skiplist:
- 内存上:dict 有哈希表开销(桶数组 + 链表);
- 性能上:dict 查改 O (1), 数据量大时远快于 ziplist 的 O (n)。
设计目的:小哈希省内存,大哈希用 dict 保证读写性能。
1.1 hashTypeSet
int hashTypeSet(robj *o, sds field, sds value, int flags) {
int update = 0;
if (o->encoding == OBJ_ENCODING_LISTPACK) {
unsigned char *zl, *fptr, *vptr;
zl = o->ptr;
fptr = lpFirst(zl);
if (fptr != NULL) {
fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
if (fptr != NULL) {
vptr = lpNext(zl, fptr);
serverAssert(vptr != NULL);
update = 1;
zl = lpReplace(zl, &vptr, (unsigned char*)value, sdslen(value));
}
}
if (!update) {
zl = lpAppend(zl, (unsigned char*)field, sdslen(field));
zl = lpAppend(zl, (unsigned char*)value, sdslen(value));
}
o->ptr = zl;
if (hashTypeLength(o) > server.hash_max_listpack_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
}
else if (o->encoding == OBJ_ENCODING_HT) {
dictEntry *de = dictFind(o->ptr, field);
if (de) {
sdsfree(dictGetVal(de));
if (flags & HASH_SET_TAKE_VALUE) {
dictGetVal(de) = value;
value = NULL;
} else {
dictGetVal(de) = sdsdup(value);
}
update = 1;
} else {
sds f, v;
if (flags & HASH_SET_TAKE_FIELD) {
f = field;
field = NULL;
} else {
f = sdsdup(field);
}
if (flags & HASH_SET_TAKE_VALUE) {
v = value;
value = NULL;
} else {
v = sdsdup(value);
}
dictAdd(o->ptr, f, v);
}
}
else {
serverPanic("Unknown hash encoding");
}
if (flags & HASH_SET_TAKE_FIELD && field) sdsfree(field);
if (flags & HASH_SET_TAKE_VALUE && value) sdsfree(value);
return update;
}
(t_hash.c)
1.2 dict 结构体
(dict.h)
1.3 rehash
ht_table[0]; // 用户访问的数据结构
ht_table[1] = null; // 用于渐进式数据迁移(rehashidx)
渐进式迁移
- 法 1:分摊在增删改查中,每次操作就迁移一个槽位
- 法 2:redis 空闲时,迁移 1ms 的时间
操作完之后,释放 ht_table[0],将其指向数据迁移完毕的结构体,然后置 ht_table[1] 为 null
2. 字符串(int → embstr → raw)
- int 编码:数字型字符串直接存成整数,抛弃 SDS 结构,内存从'redisObject+SDS'(≥23 字节)降到仅
redisObject(16 字节),且整数运算无需解析字符串,效率拉满;
- embstr 编码:短字符串(≤44 字节)用连续内存存储,一次分配 / 释放,CPU 缓存命中率高(64 字节缓存行刚好装下),比 raw 少一次内存申请;
- raw 编码:长字符串无法塞进 64 字节缓存行,只能分离存储,牺牲一点内存效率,保证长字符串的读写性能。
设计目的:数字 / 短字符串极致省内存,长字符串保证功能兼容(最大 512MB)
2.1 string 对象的构建
robj *tryObjectEncoding(robj *o) {
long value;
sds s = o->ptr;
size_t len;
serverAssertWithInfo(NULL, o, o->type == OBJ_STRING);
if (!sdsEncodedObject(o)) return o;
if (o->refcount > 1) return o;
len = sdslen(s);
if (len <= 20 && string2l(s, len, &value)) {
if ((server.maxmemory == 0 || !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) && value >= 0 && value < OBJ_SHARED_INTEGERS)
{
decrRefCount(o);
incrRefCount(shared.integers[value]);
return shared.integers[value];
} else {
if (o->encoding == OBJ_ENCODING_RAW) {
sdsfree(o->ptr);
o->encoding = OBJ_ENCODING_INT;
o->ptr = (*) value;
o;
} (o->encoding == OBJ_ENCODING_EMBSTR) {
decrRefCount(o);
createStringObjectFromLongLongForValue(value);
}
}
}
(len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
robj *emb;
(o->encoding == OBJ_ENCODING_EMBSTR) o;
emb = createEmbeddedStringObject(s, sdslen(s));
decrRefCount(o);
emb;
}
trimStringObjectIfNeeded(o);
o;
}
(object.c)
2.2 redisObject 结构体
(server.h,LRU_BITS 为 24 : 结构体占 16 字节)
2.3 int 形式:当 string 为纯数字时(可以含+-号)且长度 <= 20 位。
64 位系统中 void *ptr 本身占 8 字节,但在 int 编码时,Redis 把 ptr 当作 long long 类型(也是 8 字节)来用,而非指针:long long 的取值范围是 -9223372036854775808 ~ 9223372036854775807;这个范围对应的十进制数字的最大长度是 19 位(比如 9223372036854775807 是 19 位),加上正负号就是 20 位
2.4 embstr 形式
(sds.h, 结构体占 3 字节)
2.5 raw 编码则是 redisObject 中成员 ptr 指向一块另外分配的内存空间
2.6 总结
小于 20 字节且为数字时,选择 int 编码。大于 20 字节小于 44 字节,选择 embstr 编码。大于 44 字节,选择 raw 编码
embstr:嵌入字符串,嵌入到 redisObject 中,而 raw 就是在 redisObject 中维持一个指向堆上的资源
cpu cache line 最小访问单位为 64 字节,所以选择 64 个字节作为分界线。20 字节为界的原因:long long int 表示的数字范围:-19 位 ~ +19 位,即长度为 20 的字符串
44 字节为界的原因:buf 可用空间为:64-16-3-1 = 44。(这个 1 作为'\0',以便兼容 c 的字符串库函数)
3. 列表(quicklist 封装 ziplist)
- 早期 Redis 列表用 ziplist(小列表)和 linkedlist(大列表),但 linkedlist 每个节点有指针开销,内存效率低;
- 现在改用 quicklist(双向链表 + 每个节点是 ziplist):
- 既保留了 ziplist 的内存紧凑性(每个节点内的元素连续存储);
- 又避免了纯 ziplist 重排内存的性能问题(增删只影响单个 ziplist 节点,而非整个列表)。
设计目的:平衡内存效率和增删性能,是对传统 ziplist/linkedlist 的优化升级。
核心说明 list 的底层编码是 quicklist(双向链表),而 quicklist 的每个节点又是一个 ziplist(压缩列表节点);因此,list 并不直接转换为 ziplist,而是 quicklist 内部间接使用 ziplist 来存储每个节点的数据。
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count;
unsigned long len;
signed int fill : QL_FILL_BITS;
unsigned int compress : QL_COMP_BITS;
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *entry;
sz;
count : ;
encoding : ;
container : ;
recompress : ;
attempted_compress : ;
extra : ;
} quicklistNode;
(quicklist.h)。每个 quicklistNode 内部包含一个 ziplist,用于存储实际数据,这就是'间接使用 ziplist'的含义。
4. 集合(intset → dict)
- intset(整数集合):
- 内存上:连续存储整数,无任何冗余(比如存储 1,2,3 只占 3×8=24 字节),而 dict 存储同样数据需要哈希表 + 指针,至少占上百字节;
- 限制:只能存整数,且增删需要重排内存;
- dict:
- 内存上:有哈希表开销,但支持任意字符串元素;
- 性能上:增删查 O (1),无类型限制。
设计目的:纯整数小集合用 intset 省内存,非整数 / 大集合用 dict 保证通用性和性能(比如用户标签集合,可能存字符串标签,只能用 dict)。
5. 有序集合
- ziplist(压缩列表):
- 内存上:所有键值对 / 元素连续存储,无冗余指针(dict 每个键值对要存两个指针,skiplist 每个节点有多层指针),内存密度是 dict 的 1/5 以上;
- 性能上:增删改需要重排整个列表(O (n)),但数据量小(≤512/128 个元素)时,n 很小,性能损耗可忽略;
- skiplist:
- 内存上:skiplist 有多层指针开销,内存占用是 ziplist 的数倍;
- 性能上:skiplist 查改 O (logN),数据量大时远快于 ziplist 的 O (n)。
设计目的 小有序集合用 ziplist 省内存 大有序集合用 skiplist 保证读写性能(比如电商场景的商品排行榜 zset,数据量变大后,skiplist 的排序效率远高于 ziplist)
int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore) {
int incr = (in_flags & ZADD_IN_INCR) != 0;
int nx = (in_flags & ZADD_IN_NX) != 0;
int xx = (in_flags & ZADD_IN_XX) != 0;
int gt = (in_flags & ZADD_IN_GT) != 0;
int lt = (in_flags & ZADD_IN_LT) != 0;
*out_flags = 0;
double curscore;
if (isnan(score)) {
*out_flags = ZADD_OUT_NAN;
return 0;
}
if (zobj->encoding == OBJ_ENCODING_LISTPACK) {
unsigned char *eptr;
if ((eptr = zzlFind(zobj->ptr, ele, &curscore)) != NULL) {
(nx) {
*out_flags |= ZADD_OUT_NOP;
;
}
(incr) {
score += curscore;
(isnan(score)) {
*out_flags |= ZADD_OUT_NAN;
;
}
}
((lt && score >= curscore) || (gt && score <= curscore)) {
*out_flags |= ZADD_OUT_NOP;
;
}
(newscore) *newscore = score;
(score != curscore) {
zobj->ptr = zzlDelete(zobj->ptr, eptr);
zobj->ptr = zzlInsert(zobj->ptr, ele, score);
*out_flags |= ZADD_OUT_UPDATED;
}
;
} (!xx) {
(zzlLength(zobj->ptr)+ > server.zset_max_listpack_entries || sdslen(ele) > server.zset_max_listpack_value || !lpSafeToAdd(zobj->ptr, sdslen(ele))) {
zsetConvert(zobj, OBJ_ENCODING_SKIPLIST);
} {
zobj->ptr = zzlInsert(zobj->ptr, ele, score);
(newscore) *newscore = score;
*out_flags |= ZADD_OUT_ADDED;
;
}
} {
*out_flags |= ZADD_OUT_NOP;
;
}
}
(zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplistNode *znode;
dictEntry *de;
de = dictFind(zs->dict, ele);
(de != ) {
(nx) {
*out_flags |= ZADD_OUT_NOP;
;
}
curscore = *(*)dictGetVal(de);
(incr) {
score += curscore;
(isnan(score)) {
*out_flags |= ZADD_OUT_NAN;
;
}
}
((lt && score >= curscore) || (gt && score <= curscore)) {
*out_flags |= ZADD_OUT_NOP;
;
}
(newscore) *newscore = score;
(score != curscore) {
znode = zslUpdateScore(zs->zsl, curscore, ele, score);
dictGetVal(de) = &znode->score;
*out_flags |= ZADD_OUT_UPDATED;
}
;
} (!xx) {
ele = sdsdup(ele);
znode = zslInsert(zs->zsl, score, ele);
serverAssert(dictAdd(zs->dict, ele, &znode->score) == DICT_OK);
*out_flags |= ZADD_OUT_ADDED;
(newscore) *newscore = score;
;
} {
*out_flags |= ZADD_OUT_NOP;
;
}
} {
serverPanic();
}
;
}
(t_zet.c)
6. 总结:跳表
redis 选择采用 32 层,兼顾内存占用和查找效率的综合考量。
| 数据类型 | 编码转换逻辑 | 核心配置宏定义 | 转换触发函数 | 主要实现文件 |
|---|
| zset | ziplist ↔ skiplist | zset_max_ziplist_entries, zset_max_ziplist_value | zsetConvert | t_zset.c |
| set | intset ↔ dict | set_max_intset_entries | setTypeConvert | t_set.c |
| hash | ziplist ↔ dict | hash_max_ziplist_entries, hash_max_ziplist_value | hashTypeConvert | t_hash.c |
| list | quicklist(内部 ziplist) | list-max-ziplist-size | 内部自动管理 | quicklist.c, t_list.c |
| string | int ↔ embstr ↔ raw | OBJ_ENCODING_EMBSTR_SIZE_LIMIT (44) | tryObjectEncoding | object.c |
五、IO 多进程的工作原理