刚开始学 Redis 的时候记的是五种数据类型SET存字符串、LPUSH存列表、ZADD存排行榜命令能用起来就算过了。第一次敲OBJECT ENCODING会看到几个不认识的返回值127.0.0.1:6379SET name tom OK127.0.0.1:6379OBJECT ENCODING nameembstr127.0.0.1:6379SADD tags redis mysql(integer)2127.0.0.1:6379OBJECT ENCODING tagslistpackembstr和listpack这两个名字五种类型里都没有命令文档里也基本不出现。它们回答的是另一个问题这份数据在内存里到底是怎么摆的。五种类型先把五种类型过一遍。这里的类型是给使用者看的接口决定这个 key 能用哪些命令。类型常用命令典型场景StringSETGETINCRSETNX缓存对象、计数器、分布式锁、SessionListLPUSHRPUSHLPOPLRANGE消息队列、最新动态列表HashHSETHGETHINCRBYHGETALL存对象、购物车SetSADDSISMEMBERSINTERSUNION去重、标签、共同好友、抽奖ZSetZADDZRANGEZRANKZINCRBY排行榜、延时队列几个不那么显然的点String 的INCR是原子的计数器就靠它。分布式锁一般写SET key val NX EX 10而不是SETNX再EXPIRE后者两条命令之间如果进程挂了锁就永远不解开了List 做队列是LPUSH进、BRPOP出BRPOP可以一次传多个 key按顺序找第一个有数据的相当于优先级。但 List 没有 ACK元素弹出去就没了可靠性不够真要当 MQ 得看 StreamHash 相比把整个对象序列化成 String 存好处是能只改一个字段。String 存对象要读出来、改完再整个写回去Hash 一条HSET就够了Set 的价值在SINTER、SUNION这些集合运算比如两个人关注的共同好友ZSet 按 score 排序把 score 设成时间戳ZRANGEBYSCORE就是延时队列除了这五种还有 Bitmap、HyperLogLog、GEO、Stream 这几个常被叫做扩展类型的东西。但严格说它们不都是新类型Bitmap 和 HyperLogLog 的TYPE返回的是stringGEO 返回的是zset底层直接复用了 String 和 ZSet只是命令帮你把数据按位或者按坐标解释了一遍。真正新加的只有 Stream5.0 才引入TYPE返回stream。这几个放在后面 Bitmap、HyperLogLog 和 GEO 怎么选和 Redis Stream 入门里单独讲。类型、编码、底层结构这三个词经常被混着说其实是三层东西类型type逻辑上的接口。TYPE key返回它只有stringlisthashsetzsetstream六种。编码encoding存储方式。OBJECT ENCODING key返回它同一个类型下会看到好几种。底层结构data structure编码对应的具体实现。listpack对应 listpackskiplist对应跳表加哈希表hashtable对应dict。一个类型可以对应多个编码一个底层结构也可以被多个类型共用。跳表被 ZSet 用dict被 Hash 和 Set 用SDS 被 String用也被 Bitmap、HyperLogLog 借去当字节数组。TYPE告诉我们这个 key 能干什么至于它在内存里长什么样得看OBJECT ENCODING。ZADD出来的 ZSet元素少的时候底层根本没有跳表就是一块 listpack 数组跳表要等元素多到一定程度才会出现。redisObject同样一个 keyvalue 不是一个裸的指针而是外面包了一层redisObjecttypedefstructredisObject{unsignedtype:4;// 逻辑类型TYPE 返回的就是它unsignedencoding:4;// 物理编码决定 ptr 指向什么unsignedlru:LRU_BITS;// LRU 时间或 LFU 计数24 bitintrefcount;// 引用计数void*ptr;// 指向真正的底层结构}robj;type和encoding各占 4 个 bit最多表示 16 个值塞下六种类型和十几种编码绰绰有余。ptr指向什么完全由encoding决定同一个字段encoding 是hashtable时指向一个dict是listpack时指向一块 listpack 内存。有一种情况例外String 的int编码不去分配 SDS直接把整数值本身塞进ptr写出来是(void*)(long)value。指针本来就是 8 字节装个long long刚好。refcount是做对象共享的。预建 0 到 9999 这 10000 个整数的对象所有存这个范围内整数的 key 都指向同一份127.0.0.1:6379SET a100OK127.0.0.1:6379OBJECT REFCOUNT a(integer)21474836472147483647就是INT_MAX表示共享对象永远不会被释放。注意这个共享只在maxmemory-policy不是 LRU/LFU 的时候才启用因为 LRU 需要在对象上记访问时间共享对象记不了。编码对照下面这张表是现代版本7.2 之后的情况同一份代码在老版本上看到的编码名会不一样版本差异放到最后一节。类型编码底层结构什么时候是这种编码Stringint无整数存在ptr里值是整数且长度不超过 20 字节StringembstrSDS和 robj 一起分配长度不超过 44 字节的字符串StringrawSDS长度超过 44 字节Listquicklistlistpack 节点组成的双向链表3.2 之后只有这一种Hashlistpacklistpack字段数 ≤hash-max-listpack-entries( 默认 128 ) 且每个 field、value ≤ 64 字节Hashhashtabledict上面任一条件被打破Setintset整数集合全是整数且元素数 ≤set-max-intset-entries( 默认 512 )Setlistpacklistpack用不了intset但元素数 ≤ 128 且每个元素 ≤ 64 字节Sethashtabledict上面任一条件被打破ZSetlistpacklistpack元素数 ≤ 128 且每个 member ≤ 64 字节ZSetskiplist跳表 dict上面任一条件被打破Streamstreamrax listpack固定没有别的编码Set 是三种编码里唯一有一条完整升级链的选编码的顺序是intset → listpack → hashtable先看能不能用intset全是整数、不超过 512 个用不了就退一步试listpack不超过 128 个、每个不超过 64 字节两个条件都过不去才落到hashtable。所以一个存了 600 个连续整数的 Set在 7.2 之后不是 hashtable而是 listpack。那张表里listpack出现的次数最多它是 Redis 里最通用的紧凑结构Hash、ZSet、Set 的小对象还有 List 的每个节点用的都是它。它的具体设计在 List 那篇里讲这里只要知道它是一块连续的、不存指针的内存。观察编码切换编码不是定死的同一个 key 随着数据变多、变大编码会在运行时切过去。用 redis-cli 一组一组看最直观。String 的三种编码127.0.0.1:6379SET counter100OK127.0.0.1:6379OBJECT ENCODING counterint127.0.0.1:6379SET greetinghello redisOK127.0.0.1:6379OBJECT ENCODING greetingembstr127.0.0.1:6379SET longaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaOK127.0.0.1:6379OBJECT ENCODING longraw还有一个细节embstr是只读的。往一个 embstr 上APPEND哪怕只加一个字节也会变成 raw127.0.0.1:6379APPEND greeting!(integer)12127.0.0.1:6379OBJECT ENCODING greetingraw原因是 embstr 把 robj 和 SDS 连着分配在同一块内存里一次分配搞定代价是它被标成只读。只读对象没法原地改所以任何修改操作都会先把整块内存重新分配成可变的 raw再改。Hash 从 listpack 掉到 hashtable只要有一个 field 或 value 超过 64 字节就够127.0.0.1:6379HSET user:1 name tom age18(integer)2127.0.0.1:6379OBJECT ENCODING user:1listpack127.0.0.1:6379HSET user:1 bioaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(integer)1127.0.0.1:6379OBJECT ENCODING user:1hashtableSet 有三种编码一路升上去127.0.0.1:6379SADD ids123(integer)3127.0.0.1:6379OBJECT ENCODING idsintset127.0.0.1:6379SADD ids tom(integer)1127.0.0.1:6379OBJECT ENCODING idslistpackZSet 同理一个超长的 member 就能把它推下去127.0.0.1:6379ZADD rank90tom85jerry(integer)2127.0.0.1:6379OBJECT ENCODING ranklistpack127.0.0.1:6379ZADD rank60aaaa...一个超过 64 字节的 member(integer)1127.0.0.1:6379OBJECT ENCODING rankskiplistList 因为没有紧凑和分散两种形态OBJECT ENCODING永远只返回quicklist127.0.0.1:6379RPUSH queue a b c(integer)3127.0.0.1:6379OBJECT ENCODING queuequicklist想看更细的比如 quicklist 分了几个节点、序列化后多大用DEBUG OBJECT127.0.0.1:6379DEBUG OBJECT rank Value at:0x7f... refcount:1 encoding:skiplist serializedlength:31 lru:0 lru_seconds_idle:0DEBUG OBJECT是调试命令线上一般禁掉看编码用OBJECT ENCODING就够了它本身是 O(1)没有遍历。编码转换是单向的上面这些转换只会往更宽松的方向走listpack掉到hashtableintset掉到listpack都回不去。把数据删回去编码也不会退回来127.0.0.1:6379HSET user:1 bio短的(integer)0127.0.0.1:6379OBJECT ENCODING user:1hashtable原因是转换的检查只在写入路径上做。HSET进来的时候Redis 看一眼这个字段长度超没超 64 字节“字段数有没有超过 128”超了就调用一次hashTypeConvert换成 hashtable。删字段走的是另一条路径那条路上没有要不要换回去的判断。这个设计是有意的。紧凑编码和哈希表之间的转换要重新分配内存、搬数据成本不低而且真实场景里数据只会越攒越多来回抖动的概率很小加反向检查反而多一份开销。临界值附近的抖动正因为转换是一次性的数据刚好卡在临界值上就会有问题。hash-max-listpack-entries默认 128如果一个 Hash 稳定在 129、130 个字段它就一直停在 hashtable 上明明是接近小对象的数据量却要付哈希表的内存开销。这种情况要么把阈值调大要么把对象拆成多个 key。反过来阈值调得太大也不行listpack 是 O(n) 查找字段多了单次HGET会变慢。默认的 128 / 64 是走了很多年测出来的折中没特殊需求别动。和版本有关的坑编码最坑的地方是它跟版本绑得很紧同一份数据、同一段代码换个 Redis 版本OBJECT ENCODING看到的就不一样。ziplist 被 listpack 取代。6.2 及之前Hash、ZSet 的小对象编码叫ziplistList 的 quicklist 节点也是 ziplist。7.0 之后全部换成listpack。原因是 ziplist 有个连锁更新cascade update的问题改一个元素可能导致后面一连串元素都要搬动listpack 从设计上避免了它这部分在 List 那篇细讲。配置项跟着改名。编码名一改控制它的配置也改了名6.2 及之前 7.0 之后 hash-max-ziplist-entries → hash-max-listpack-entries hash-max-ziplist-value → hash-max-listpack-value zset-max-ziplist-entries → zset-max-listpack-entries zset-max-ziplist-value → zset-max-listpack-value list-max-ziplist-size → list-max-listpack-size从 6.x 升到 7.x 的时候老配置文件里写的是旧名字Redis 7 会直接报这个参数不认识但不会拦住启动结果是这些配置静默失效、全部走默认值。升级时配置文件里搜一遍ziplist是必须的动作。Set 的 listpack 是 7.2 才有的。7.2 之前Set 只有intset和hashtable两种编码一旦有非整数元素就直接是 hashtable没有中间的紧凑形态。7.2 加了 listpack 之后才有了intset → listpack → hashtable这条链对应配置set-max-listpack-entries和set-max-listpack-value。所以看别人的文章或者排查线上问题时先确认版本。OBJECT ENCODING返回ziplist说明是 6.2 或更早返回listpack说明是 7.0 以后Set 返回listpack说明是 7.2 以后光看编码名就能大致判断出版本。