Redis CoreDataTypes And Commands
Redis 命令不区分大小写,基础命令:
ping # 测试链接是否 OK(PONG 为成功) echo redis # 输出 redis quit # 退出客户端 save # 持久化到本地 bgsave dbsize # 查看当前键空间中数据量 select 0 # 切换键空间,默认为 0 move key db # 数据移动 flushdb # 清除当前键空间数据(info 中的 Keyspace) flushall # 清除所有键空间数据 auth 密码 # 密码认证 help @list # 查看帮助命令 help @string help @set help @zset help @hash help @generic info # 已连上 Redis,查看系统信息(Keyspace 显示每个键空间中的数据数量) redis-server -v # 查看版本信息 redis-cli --memkeys redis-cli --bigkeys
存入 Redis 的 key 区分大小写,Key 通用命令:
set a abc # 添加字符串类型 a-abc hset hsah hahaha zhangsan # 添加哈希类型 a-abc del a # 删除 a-abc,返回 1 成功,0 失败 # * 匹配任意数量的任意符号,? 配合-个任意符号,[] 匹配一个指定符号 keys * # 查询所有,尽量避免使用,会锁住数据库,阻塞其它请求,可使用 scan 代替 keys it* # 查询所有以 it 开头 keys *hm # 查询所有以 hm 结尾 keys ??hm # 查询所有前面两个字符任意,后面以 hma 结尾 keys user:? # 查询所有以 user: 开头,最后一个字符任意 keys u[st]er:1 # 查询所有以 u 开头,以 er:1 结尾,中间包含一个字母,s 或 t exists a # 判断是否存在,返回 1 为存在,0 为不存在 expire a 10 # 设置 a 的生命周期为 10 秒 pexpire a 10 expireat a 10 pexpireat a 10 persist key # 切换 key 从时效性转换为永久性 ttl a # 查看这条数据的生存时间,单位秒,返回 -1 为不过期,-2 表示 key 不存在 pttl a type a # 查看键 a 对应 value 的数据类型 randomkey # 随机获取 key 的值 rename a b # 重命名 key,把 a 命名为 b,会覆盖已存在的 key renamenx a b # 重命名 key,如果 b 存在会重命名失败 sort # 对所有 key 排序
Redis 中所有 key-value 都以 redisObject(robj) 结构组织:
#define OBJ_STRING 0 /* String object. */ #define OBJ_LIST 1 /* List object. */ #define OBJ_SET 2 /* Set object. */ #define OBJ_ZSET 3 /* Sorted set object. */ #define OBJ_HASH 4 /* Hash object. */ #define OBJ_TYPE_BASIC_MAX 5 /* Max number of basic object types. */ #define OBJ_MODULE 5 /* Module object. */ #define OBJ_STREAM 6 /* Stream object. */ #define OBJ_TYPE_MAX 7 /* Maximum number of object types */ #define OBJ_ENCODING_RAW 0 /* Raw representation */ #define OBJ_ENCODING_INT 1 /* Encoded as integer */ #define OBJ_ENCODING_HT 2 /* Encoded as hash table */ #define OBJ_ENCODING_ZIPMAP 3 /* No longer used: old hash encoding. */ #define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */ #define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */ #define OBJ_ENCODING_INTSET 6 /* Encoded as intset */ #define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ #define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */ #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */ #define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */ #define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */ #define OBJ_ENCODING_LISTPACK_EX 12 /* Encoded as listpack, extended with metadata */ #define LRU_BITS 24 struct redisObject { // 16 byte unsigned type:4; // 4 bit,数据类型:String(字符串)、Hash(散列表)、List(有序可重复集合)、Set(无序不可重复集合)、SortedSet(zset,有序不可重复集合)、Bitmap(位图)、HyperLogLog(统计)、GEO(地理)等 unsigned encoding:4; // 4 bit,编码方式:raw、int、ht、zipmap、linkedlist、ziplist、intset unsigned lru:LRU_BITS; /* 24 bit,LRU time (relative to global lru_clock) or LFU data (least significant 8 bits frequency and most significant 16 bits access time). */ int refcount; // 4 byte,引用计数 void *ptr; // 8 bye,数据指针 };
type
String(OBJ_STRING)
set a abc # 添加字符串类型 a-abc,成功返回 OK setex a 100 abc # 添加字符串类型 a-abc,存活时间 100秒 psetex a 10000 abc # 添加字符串类型 a-abc,存活时间 10000 毫秒(10秒) getrange a 0 2 # 获取指定下表字符串---(abc),闭合空间,包前后边界 getset a zxc # 设置新值,返回旧值---(abc) mset a abc z zxc # 设置多个值 a-abc,z-zxc mget a z # 获取多个值 setnx a abc # 添加字符串类型 a-abc,键 a 存在会设置失败 msetnx a abc z zxc # 添加字符串类型 a-abc,其中一个 key 存在就会全部设置失败(有原子性) strlen a # 获取字符串长度---(3) set s 21 # key 对应 value 为数值,value 会加一 incr s set s 21 # key 对应 value 为数值,value 会加制定数量 incrby s 100 decr # 同上为减操作 decrby append a xyz # 在 key 对应 value 的末尾添加字符串,会返回添加后的字符串 STRALGO # 处理大字符串 MEMORY STATS # 显示 SDS 内存细节
encoding 规则:2.6 及之前:OBJ_ENCODING_INT、OBJ_ENCODING_RAW。3.0 引入 OBJ_ENCODING_EMBSTR。3.2 调整 EMBSTR 长度阈值(从 39 字节扩展至 44 字节(因 jemalloc 内存分配器优化))
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44 robj *createStringObject(const char *ptr, size_t len) { if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) return createEmbeddedStringObject(ptr,len); // 一次内存分配 else return createRawStringObject(ptr,len); // 两次内存分配 } int isSdsRepresentableAsLongLong(sds s, long long *llval) { return string2ll(s,sdslen(s),llval) ? C_OK : C_ERR; }
List(OBJ_LIST)
lpush list 1 2 3 4 5 6 7 8 9 10 # 向列表左边添加元素 rpush list 1 2 3 4 5 6 7 8 9 10 # 向列表右边添加元素 llen list # 获取长度 lrange list 0 2 # 查看列表,栈("10" "9" " 8"),0 -1 可以查看全部 lset list 0 100 # 设置指定下标元素 (10->100) lindex list 5 # 获取指定下标元素 ("5") lpop list # 移除第一个元素,并返回(100) rpop list # 移除最后一个元素,并返回(1)
encoding 规则:3.2 之前:OBJ_ENCODING_ZIPLIST、OBJ_ENCODING_LINKEDLIST。3.2 及之后只有 OBJ_ENCODING_QUICKLIST
robj *createQuicklistObject(int fill, int compress) { quicklist *l = quicklistNew(fill, compress); robj *o = createObject(OBJ_LIST,l); o->encoding = OBJ_ENCODING_QUICKLIST; return o; }
Set(OBJ_SET)
sadd set a b c d # 添加 sadd set a # 会失败,set 是不可重复集合 scard set # 获取集合长度 4 smembers set # 获取集合中所有元素 srandmember set 2 # 随机获取指定个数的元素,不会删除元素 sismember set a # 判断元素是否在集合中,返回 1 是,返回 0 不是 srem set a b # 移除指定元素 srandmember set # 从集合中随机弹出一个元素,元素不删除 spop set # 从集合中随机弹出一个元素,出一个删一个 sadd set1 c d e f # 获取差集 sdiff set set1 # ("b" "a") sdiff set1 set # ("f" "e") sadd set1 c d e f # 获取交集 sinter set set1 # ("c" "d") sadd set1 c d e f # 获取并集 sunion set set1 # ("d" "f" "b" "a" "c" "e")
encoding 规则:7.0 及之前 OBJ_ENCODING_INTSET、OBJ_ENCODING_HT。7.2 新增 OBJ_ENCODING_LISTPACK
robj *createSetObject(void) { dict *d = dictCreate(&setDictType); robj *o = createObject(OBJ_SET,d); o->encoding = OBJ_ENCODING_HT; return o; } robj *setTypeCreate(sds value, size_t size_hint) { // https://github.com/redis/redis/blob/7.4.2/src/t_set.c#L25 if (isSdsRepresentableAsLongLong(value,NULL) == C_OK && size_hint <= server.set_max_intset_entries) return createIntsetObject(); // 用 OBJ_ENCODING_INTSET if (size_hint <= server.set_max_listpack_entries) return createSetListpackObject(); // 用 OBJ_ENCODING_LISTPACK /* We may oversize the set by using the hint if the hint is not accurate, but we will assume this is acceptable to maximize performance. */ robj *o = createSetObject(); // 用 OBJ_ENCODING_HT dictExpand(o->ptr, size_hint); return o; }
验证
redis-server.exe --enable-debug-command yes redis-cli SADD int_set 1 2 3 redis-cli SADD small_str_set a b c redis-cli SADD large_set $(seq 1 200) redis-cli DEBUG OBJECT int_set redis-cli DEBUG OBJECT small_str_set redis-cli DEBUG OBJECT large_set
SortedSet(OBJ_ZSET)
zadd sortedset 100 a 200 b 300 c # 添加 zrem sortedset a # 删除 zremrangebyrank sortedset 0 220 # 按照排名范围删除元素 zcard sortedset # 元素个数---(3) zcount sortedset 0 220 # 获得指定分数范围内的元素个数---(2) zscore sortedset a # 获取元素分数---(100) zcount sortedset 0 220 # 查看分数区间的元素个数---(2) zrank sortedset a # 获取元素排名,从小到大---(0) zrank sortedset b # ---(1) zrank sortedset c # ---(2) zrevrank sortedset a # 获取元素排名,从大到小---(2) zincrby sortedset 1000 a # 增加元素分数 # 按照元素分数从小到大的顺序 返回索引从 start 到 stop 之间的所有元素---("b" "c" "a") zrange sortedset 0 10 # 同上,会带上分数---("b" 200 "c" 300 "a" 1100) zrange sortedset 0 10 withscores # 获取指定分数范围的元素 zrangebyscore sortedset 0 220 [WITHSCORES] [LIMIT offset count]
encoding 规则:Redis 7.0 之前:OBJ_ENCODING_ZIPLIST、OBJ_ENCODING_SKIPLIST。Redis 7.0 及之后用 OBJ_ENCODING_LISTPACK 替代了 OBJ_ENCODING_ZIPLIST
robj *createZsetObject(void) { zset *zs = zmalloc(sizeof(*zs)); robj *o; zs->dict = dictCreate(&zsetDictType); zs->zsl = zslCreate(); o = createObject(OBJ_ZSET,zs); o->encoding = OBJ_ENCODING_SKIPLIST; return o; } robj *zsetTypeCreate(size_t size_hint, size_t val_len_hint) { // https://github.com/redis/redis/blob/7.4.2/src/t_zset.c#L1239 if (size_hint <= server.zset_max_listpack_entries && val_len_hint <= server.zset_max_listpack_value) { return createZsetListpackObject(); // 用 OBJ_ENCODING_LISTPACK } robj *zobj = createZsetObject(); // 用 OBJ_ENCODING_SKIPLIST zset *zs = zobj->ptr; dictExpand(zs->dict, size_hint); return zobj; }
Hash(OBJ_HASH)
hset map name jim # 给 key 为 map 的键值设置键为 name 值为 jim
hset map age 18
hincrby map age 1 # 年龄加一
hexisit map name # 判断 key 对应 value 是否有存在的 key,有返回 1,无返回 0
hget map name # 获取 key 对应 value 中的 key 对应的值,不存在返回 nil
hgetall map # 获取 key 对应 value 中的所有键值对
hkeys map # 获取 key 对应 value 中的所有 key
hvals map # 获取 key 对应 value 中的所有 value
hlen map # 获取 key 对应 value 中的键值对数量
hmget map name age # 获取多个
hmset map sex nan phone 1234568798 # 设置多个
hdel map phone sex # 删除 key 对应 value 中的键值对
hsetnx map name newjim # 若 map 中存在 name 会添加失败
encoding 规则:7.0 之前:OBJ_ENCODING_ZIPLIST、OBJ_ENCODING_HT。7.0 及之后用 OBJ_ENCODING_LISTPACK 替代了 OBJ_ENCODING_ZIPLIST
robj *createHashObject(void) { unsigned char *zl = lpNew(0); robj *o = createObject(OBJ_HASH, zl); o->encoding = OBJ_ENCODING_LISTPACK; return o; } void hashTypeTryConversion(redisDb *db, robj *o, robj **argv, int start, int end) { // https://github.com/redis/redis/blob/7.4.2/src/t_hash.c int i; size_t sum = 0; if (o->encoding != OBJ_ENCODING_LISTPACK && o->encoding != OBJ_ENCODING_LISTPACK_EX) return; /* We guess that most of the values in the input are unique, so if there are enough arguments we create a pre-sized hash, which might over allocate memory if there are duplicates. */ size_t new_fields = (end - start + 1) / 2; if (new_fields > server.hash_max_listpack_entries) { hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires); // 转为 OBJ_ENCODING_HT dictExpand(o->ptr, new_fields); return; } for (i = start; i <= end; i++) { if (!sdsEncodedObject(argv[i])) continue; size_t len = sdslen(argv[i]->ptr); if (len > server.hash_max_listpack_value) { hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires); // 转为 OBJ_ENCODING_HT return; } sum += len; } if (!lpSafeToAdd(hashTypeListpackGetLp(o), sum)) hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires); }
BitMaps(OBJ_STRING)
setbit bits 888 1 # 设置第 888 位为 1,之前位上的数据会都补成 0 getbit bits 888 1 # 获取指定位上的值,不存在会返回 0 # 对指定key按位进行交并、非异或操作,并将结果保存到 destKey 中 # 位运算:and:交、or:并、not:非、xor:异或 bitop op destKey key1 [key2...] bitcount key [start end] # 统计指定 key 中 1 的数量
encoding
OBJ_ENCODING_RAW(SDS(Simple Dynamic String))
SET long_str "This is a very long string that exceeds 44 bytes..." OBJECT ENCODING long_str # 输出 raw
当字符串长度 > 44 byte 或 包含二进制数据,redisObject 与 sds 字符串会分两次分配内存。支持原地修改(COW(copy-on-write,写时复制)机制)
struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* used */ uint64_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; };
flags:sdshdr5(长度 < 32)、sdshdr8(32 ≤ 长度 < 0xFF)、sdshdr16(0xFF ≤ 长度 < 0xFFFF)、sdshdr32(0xFFFF ≤ 长度 < 0xFFFFFFFF)、sdshdr64(超大长度)
OBJ_ENCODING_INT(整数编码)
当字符串值可表示为 long 类型整数时,会直接存储 64 位有符号整数
SET num 12345 OBJECT ENCODING num # 输出 int
OBJ_ENCODING_HT(哈希表)
struct dict { dictType *type; // 类型特定函数(哈希计算、键值复制/销毁等),实现多态 dictEntry **ht_table[2]; // 两个哈希表数组(通常只用 ht_table[0],Rehash 时同时使用) unsigned long ht_used[2]; // 两个哈希表已用槽位数(ht_used[0] + ht_used[1] = 总元素数) long rehashidx; /* rehashing not in progress if rehashidx == -1 */ // Rehash 进度索引:-1 = 未进行 Rehash,>=0 = 当前迁移的桶索引下标(渐进式 Rehash 标志) /* Keep small vars at end for optimal (minimal) struct padding */ unsigned pauserehash : 15; /* If >0 rehashing is paused */ // Rehash 暂停计数(用 15 位位域计数)器:>0 时暂停 Rehash(用于 Lua 脚本执行等原子性场景),支持嵌套暂停(如 Lua 脚本执行期间多次触发 RDB 保存) unsigned useStoredKeyApi : 1; /* See comment of storedHashFunction above */ // 启用存储键 API 优化标志:当键为 SDS 类型且无需销毁时,可复用内存(详见 storedHashFunction) signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */ // 哈希表容量指数(实际容量 = 1 << ht_size_exp[N]):用指数存储减少内存占用,同时支持快速扩容计算 int16_t pauseAutoResize; /* If >0 automatic resizing is disallowed (<0 indicates coding error) */ // 自动扩容控制:>0 时禁止自动扩容(如 RDB/AOF 加载时,避免写时复制(Copy-on-Write)内存膨胀),<0 表示编码错误(断言用) void *metadata[]; // 元数据扩展槽(用于特殊场景的附加数据,如 Streams 结构) }; struct dictEntry { // https://github.com/redis/redis/blob/7.4.2/src/dict.c#L45 void *key; // 存储键(通常为 SDS 类型) union { // 值存储联合体(支持多种数据类型) void *val; // 通用指针(用于复杂类型) uint64_t u64; // 无符号 64 位整型(用于整数存储优化) int64_t s64; // 有符号 64 位整型(如过期时间) double d; // 双精度浮点型(SortedSet 分数存储) } v; struct dictEntry *next; /* Next entry in the same hash bucket. */ // 链式哈希冲突解决 }; typedef struct { // 无值版哈希节点(节省 val 字段的 8 字节内存):专为仅需存储键的场景优化(如 Redis Set 类型) void *key; dictEntry *next; } dictEntryNoValue;
渐进式 Rehash(数据逐步增多,会触发 rehash 操作) 双哈希表:通过 ht_table[2] 和 rehashidx 实现平滑迁移,避免单次 Rehash 导致的延迟抖动。每次增删改查操作时迁移 1 个桶(_dictRehashStep),保证高性能
负载因子 = ht_used[0] / (1 << ht_size_exp[0]),即负载因子 = 已用槽位数 / 哈希表总槽位数。触发扩容的两个关键阈值:
- 阈值 1(默认 1):当 负载因子 ≥ 1 且 dict_can_resize 为真(允许自动扩容),触发扩容
- 阈值 2(强制 5):当 负载因子 ≥ 5 无论是否允许自动扩容,强制扩容(避免哈希冲突恶化)
static void _dictExpandIfNeeded(dict *d) { /* Automatic resizing is disallowed. Return */ if (d->pauseAutoResize > 0) return; // 如果自动扩容被暂停(例如在RDB/AOF操作中),直接返回 dictExpandIfNeeded(d); } int dictExpandIfNeeded(dict *d) { /* Incremental rehashing already in progress. Return. */ if (dictIsRehashing(d)) return DICT_OK; // 如果当前正在执行渐进式 Rehash,直接返回 OK /* If the hash table is empty expand it to the initial size. */ if (DICTHT_SIZE(d->ht_size_exp[0]) == 0) { // 如果哈希表为空(初始化场景),扩展为默认初始大小:4 dictExpand(d, DICT_HT_INITIAL_SIZE); return DICT_OK; } /* 扩容条件判断: * 1. 当负载因子 ≥1(used/size ≥1)且允许扩容(dict_can_resize 为 DICT_RESIZE_ENABLE) * 2. 或负载因子 ≥ 强制扩容阈值(默认5),即使全局禁止扩容(dict_can_resize != DICT_RESIZE_FORBID) */ /* If we reached the 1:1 ratio, and we are allowed to resize the hash table (global setting) or we should avoid it but the ratio between * elements/buckets is over the "safe" threshold, we resize doubling the number of buckets. */ if ((dict_can_resize == DICT_RESIZE_ENABLE && d->ht_used[0] >= DICTHT_SIZE(d->ht_size_exp[0])) || (dict_can_resize != DICT_RESIZE_FORBID && d->ht_used[0] >= dict_force_resize_ratio * DICTHT_SIZE(d->ht_size_exp[0])) ) { if (dictTypeResizeAllowed(d, d->ht_used[0] + 1)) // 检查类型特定限制(例如某些场景禁止扩容) dictExpand(d, d->ht_used[0] + 1); // 触发扩容:新大小为 used+1(实际会向上取整到 2^n) return DICT_OK; } return DICT_ERR; } #define DICT_HT_INITIAL_EXP 2 #define DICT_HT_INITIAL_SIZE (1<<(DICT_HT_INITIAL_EXP)) // https://github.com/redis/redis/blob/7.4.2/src/dict.h#L146
OBJ_ENCODING_LINKEDLIST(双向链表(大列表))
内存碎片化
typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; } listNode; typedef struct list { listNode *head; listNode *tail; void *(*dup)(void *ptr); // 节点值复制函数指针,参数:ptr - 待复制的值指针,返回值:新分配的值指针副本 void (*free)(void *ptr); // 节点值释放函数指针,参数:ptr - 待释放的值指针 int (*match)(void *ptr, void *key); // 节点值匹配函数指针,参数:ptr - 链表存储的值指针,key - 待匹配的键指针,返回值:匹配成功返回1,失败返回0 unsigned long len; // 链表长度计数器(O(1)时间复杂度获取长度) } list;
支持泛型数据存储,通过函数指针实现多态
OBJ_ENCODING_ZIPLIST(压缩列表(小列表优化))
扩展性/插入/删除效率低、级联更新问题。元素少且成员较小时使用
// <zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend> // 注意:如果没有另外指定,所有字段都以 little endian 保存 // <uint32_t zlbytes> 保存 ziplist 占用的字节数,包括 zlbytes 字段本身的 4 个字节。需要存储该值,以便能够调整整个结构的大小,而无需先遍历该结构 // <uint32_t zltail> ziplist 中最后一个 entry 的偏移量。这样就可以在列表的远端进行 pop 操作,而无需遍历整个列表 // <uint16_t zllen> entry 的数量。当条目数超过 2^16-2 时,该值将设置为 2^16-1,需要遍历整个 ziplist 才能知道它有多少个 entyr // <uint8_t zlend> 一个特殊的 entry,表示 ziplist 的结尾。编码为固定等于 255(0xFF) 的单字节。其它正常 entry 不会以 0xFF 开头
entry 完整结构:<prevlen> <encoding> <entry-data>,prevlen 以便能从后向前遍历。encoding 表示条目类型(整数或字符串),如果是字符串,它还表示字符串长度。有时 encoding 也存储数据(小整数)。这种情况下 entry 就没有 <entry-data> 部分
prevlen 编码:
- 前一个 entry 小于 254 byte,prevlen 用 1 byte 表示(unsigned 8 bit integer):<prevlen from 0 to 253> <encoding> <entry>
- 前一个 entry 大于 253 byte,pervlen 用 5 byte 表示(第 1 个 byte 设为 254 (0xFE),后 4 个 byte 保存前一个 entry 的长度):0xFE <4 bytes unsigned little endian prevlen> <encoding> <entry>
encoding 编码:第一个 byte 始终是确定条目类型
- 当 entry 是字符串,encoding 第 1 个 byte 的前 2 bit 保存字符串长度的编码类型,后面是字符串的实际长度
- 当 entry 是整数,encoding 第 1 个 byte 的前 2 bit 设为 1。随后的 2 bit 用于指定此标头后将存储的整数类型
unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) { size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen, newlen; // 当前 ziplist 总字节数 unsigned int prevlensize, prevlen = 0; // 前驱节点的长度 size_t offset; int nextdiff = 0; unsigned char encoding = 0; // 数据编码类型(ZIP_STR_* 或 ZIP_INT_*) long long value = 123456789; /* initialized to avoid warning. Using a value that is easy to see if for some reason we use it uninitialized. */ zlentry tail; /* Find out prevlen for the entry that is inserted. */ if (p[0] != ZIP_END) { // 非尾部插入时需处理前驱长度 ZIP_DECODE_PREVLEN(p, prevlensize, prevlen); // 解码前驱长度 } else { unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl); if (ptail[0] != ZIP_END) { prevlen = zipRawEntryLengthSafe(zl, curlen, ptail); // 尾部插入时获取最后一个元素的长度 } } /* See if the entry can be encoded */ if (zipTryEncoding(s,slen,&value,&encoding)) { // 尝试整数编码 /* 'encoding' is set to the appropriate integer encoding */ reqlen = zipIntSize(encoding); } else { /* 'encoding' is untouched, however zipStoreEntryEncoding will use the string length to figure out how to encode it. */ reqlen = slen; // 字符串直接使用长度 } /* We need space for both the length of the previous entry and the length of the payload. */ reqlen += zipStorePrevEntryLength(NULL,prevlen); // 添加 prevlen 存储空间 reqlen += zipStoreEntryEncoding(NULL,encoding,slen); // 添加 encoding 头 /* When the insert position is not equal to the tail, we need to make sure that the next entry can hold this entry's length in its prevlen field. */ int forcelarge = 0; nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0; if (nextdiff == -4 && reqlen < 4) { // 处理级联更新 nextdiff = 0; // 强制使用大长度编码 forcelarge = 1; } /* Store offset because a realloc may change the address of zl. */ offset = p-zl; newlen = curlen+reqlen+nextdiff; zl = ziplistResize(zl,newlen); // 重新分配内存 p = zl+offset; /* Apply memory move when necessary and update tail offset. */ if (p[0] != ZIP_END) { /* Subtract one because of the ZIP_END bytes */ memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff); // 移动后续数据 /* Encode this entry's raw length in the next entry. */ if (forcelarge) zipStorePrevEntryLengthLarge(p+reqlen,reqlen); else zipStorePrevEntryLength(p+reqlen,reqlen); /* Update offset for tail */ ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen); /* When the tail contains more than one entry, we need to take "nextdiff" in account as well. Otherwise, a change in the size of prevlen doesn't have an effect on the *tail* offset. */ assert(zipEntrySafe(zl, newlen, p+reqlen, &tail, 1)); // 安全写入检查 if (p[reqlen+tail.headersize+tail.len] != ZIP_END) { ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff); } } else { /* This element will be the new tail. */ ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl); } /* When nextdiff != 0, the raw length of the next entry has changed, so we need to cascade the update throughout the ziplist */ if (nextdiff != 0) { offset = p-zl; zl = __ziplistCascadeUpdate(zl,p+reqlen); // 级联更新 p = zl+offset; } /* Write the entry */ p += zipStorePrevEntryLength(p,prevlen); // 写入 prevlen p += zipStoreEntryEncoding(p,encoding,slen); // 写入 encoding if (ZIP_IS_STR(encoding)) { memcpy(p,s,slen); // 写入字符串 } else { zipSaveInteger(p,value,encoding); // 写入整数 } ZIPLIST_INCR_LENGTH(zl,1); return zl; }
OBJ_ENCODING_INTSET(整数集合)
typedef struct intset { uint32_t encoding; // 编码方式(INTSET_ENC_INT16/INTSET_ENC_INT32/INTSET_ENC_INT64) uint32_t length; // 集合元素数量(非字节数) int8_t contents[]; // 柔性数组(实际存储元素的字节数组,类型由 encoding 决定) } intset;
第一个元素为 int16_t,第二个元素为 int32_t,这时需要升级,即改变 contents 数组的类型
柔性数组(flexible array member)是 C99 标准特性,允许结构体最后一个成员是未知大小的数组。实际内存分配时通过malloc动态控制数组大小。访问时通过指针运算实现类型转换
OBJ_ENCODING_SKIPLIST(跳表)
typedef struct zskiplistNode { // 跳跃表节点结构 sds ele; // 成员字符串 double score; // 排序分值 struct zskiplistNode *backward; // 后向指针 struct zskiplistLevel { struct zskiplistNode *forward; // 前向指针 unsigned long span; // 跨度(用于排名) } level[]; // 柔性数组实现多层级 } zskiplistNode; typedef struct zskiplist { // 跳跃表结构 struct zskiplistNode *header, *tail; unsigned long length; int level; } zskiplist; typedef struct zset { dict *dict; // 哈希表:存储成员到分值的映射(O(1)查找) zskiplist *zsl; // 跳跃表:维护有序结构 } zset;
Skip Lists(跳表): A Probabilistic Alternative to Balanced Trees(平衡树):https://github.com/redis/redis/blob/7.4.2/src/t_zset.c#L50 & https://news.ycombinator.com/item?id=1171423
OBJ_ENCODING_EMBSTR(嵌入式字符串)
SET short_str Hello
OBJECT ENCODING short_str # 输出 embstr
当字符串长度 ≤ 44 byte (Redis 5.0+),redisObject 与字符串数据会连续存储(所以小 Key 效率更高),减少内存碎片。应用了 L1 缓存行对齐,但只读(修改需转 raw 编码)
robj *createEmbeddedStringObject(const char *ptr, size_t len) { robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1); // 单次内存分配:robj(16 byte) + sdshdr8(17 byte) + data(44 byte) + 结束符\0(1 byte) struct sdshdr8 *sh = (void*)(o+1); // robj 头部初始化,内存连续 o->type = OBJ_STRING; o->encoding = OBJ_ENCODING_EMBSTR; o->ptr = sh+1; o->refcount = 1; o->lru = 0; sh->len = len; sh->alloc = len; sh->flags = SDS_TYPE_8; if (ptr == SDS_NOINIT) sh->buf[len] = '\0'; else if (ptr) { memcpy(sh->buf,ptr,len); // 数据拷贝 sh->buf[len] = '\0'; } else { memset(sh->buf,0,len+1); } return o; }
OBJ_ENCODING_QUICKLIST(快速列表)
5.0 至 6.2:使用 ZIPLIST 作为底层节点。7.0 及之后换为 LISTPACK 作为底层节点
将多个 listpack 节点通过双向链表连接,结合了 listpack 的内存紧凑性和 linkedlist 的插入效率。解决了内存碎片化,同时优化了大规模数据的操作性能
typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* total count of all entries in all listpacks */ unsigned long len; /* number of quicklistNodes */ signed int fill : QL_FILL_BITS; /* fill factor for individual nodes */ unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */ unsigned int bookmark_count: QL_BM_BITS; quicklistBookmark bookmarks[]; } quicklist; typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *entry; // 指向 listpack(紧凑链表) size_t sz; /* entry size in bytes */ unsigned int count : 16; /* count of items in listpack */ unsigned int encoding : 2; /* RAW==1 or LZF==2 */ unsigned int container : 2; /* PLAIN==1 or PACKED==2 */ // PLAIN=1 表示节点存储的是单个元素(直接存储值不压缩)。PACKED=2 表示节点存储的是 listpack(替代了旧版的 ziplist) unsigned int recompress : 1; /* was this node previous compressed? */ unsigned int attempted_compress : 1; /* node can't compress; too small */ unsigned int dont_compress : 1; /* prevent compression of entry that will be used later */ unsigned int extra : 9; /* more bits to steal for future usage */ } quicklistNode;
OBJ_ENCODING_LISTPACK(紧凑列表)
避免了 ZIPLIST 的级联更新
typedef struct { // https://github.com/redis/redis/blob/7.4.2/src/listpack.h#L28 /* When string is used, it is provided with the length (slen). */ unsigned char *sval; // 存储字符串时使用 uint32_t slen; // 字符串长度 /* When integer is used, 'sval' is NULL, and lval holds the value. */ long long lval; // 存储整数时使用 } listpackEntry; #define LP_HDR_SIZE 6 /* 32 bit total len + 16 bit number of elements. */ #define LP_EOF 0xFF unsigned char *lpNew(size_t capacity) { // https://github.com/redis/redis/blob/7.4.2/src/listpack.c#L220 unsigned char *lp = lp_malloc(capacity > LP_HDR_SIZE+1 ? capacity : LP_HDR_SIZE+1); if (lp == NULL) return NULL; lpSetTotalBytes(lp,LP_HDR_SIZE+1); // listpack 总字节数 lpSetNumElements(lp,0); // listpack 元素总数 lp[LP_HDR_SIZE] = LP_EOF; // 结尾标识 return lp; } // <tot-bytes> <num-elements> <element-1> ... <element-N> <listpack-end-byte> // General structure // <encoding-type><element-data><element-tot-len> // This is an element
https://redis.io/topics/data-types-intro

浙公网安备 33010602011771号