RedisObject 与五大数据类型

50 阅读18分钟

RedisObject 与五大数据类型

在上一篇文章中,我们深入剖析了 Redis 的 8 种底层数据结构。但用户在使用 Redis 时,并不直接操作这些底层结构,而是通过 RedisObject 对象系统 使用五大数据类型。本文将揭秘 Redis 如何将底层结构封装成用户友好的数据类型。

📖 目录


为什么需要对象系统?

Redis 设计对象系统有以下几个重要原因:

1️⃣ 类型检查

# 用户执行命令时,Redis 需要检查类型是否匹配
LPUSH mylist "value"    # ✅ List 命令
GET mylist              # ❌ 错误:WRONGTYPE Operation against a key holding the wrong kind of value

2️⃣ 多态

# 同一个命令,不同对象有不同的实现
LLEN mylist    # List 底层可能是 QuickList
LLEN mylist2   # 也可能是 ZipList(小数据)
# 用户无需关心底层实现

3️⃣ 内存优化

# Redis 根据数据量自动选择最优编码
HSET user:1 name "Alice"           # 小数据 → ListPack
HSET user:2 name "Very Long..."    # 大数据 → Dict

4️⃣ 引用计数与对象共享

# 小整数对象(0-9999)在 Redis 启动时预创建并共享
SET key1 100    # 共享对象
SET key2 100    # 引用同一个对象,节省内存

2.2.1 RedisObject 结构

所有 Redis 对象都是 redisObject 结构:

typedef struct redisObject {
    unsigned type:4;        // 类型(4 位,16 种)
    unsigned encoding:4;    // 编码(4 位,16 种)
    unsigned lru:LRU_BITS;  // LRU 时间或 LFU 数据(24 位)
    int refcount;           // 引用计数(4 字节)
    void *ptr;              // 指向实际数据的指针(8 字节)
} robj;

// 总大小:16 字节(64 位系统)

内存布局

┌────────────────────────────────────────────────┐
│              redisObject (16 字节)              │
├────────┬────────┬────────┬────────┬────────────┤
│ type   │encoding│  lru   │refcount│    ptr     │
│ 4 bits │ 4 bits │24 bits │4 bytes8 bytes   │
└────────┴────────┴────────┴────────┴────────────┘
                                         ↓
                                    ┌────────┐
                                    │ 实际数据│
                                    └────────┘

类型(Type)

// 5 种对象类型
#define OBJ_STRING 0    // 字符串
#define OBJ_LIST   1    // 列表
#define OBJ_SET    2    // 集合
#define OBJ_ZSET   3    // 有序集合
#define OBJ_HASH   4    // 哈希表

// 其他类型(Redis 5.0+)
#define OBJ_MODULE 5    // 模块类型
#define OBJ_STREAM 6    // 流类型

查看类型

127.0.0.1:6379> SET name "Redis"
OK
127.0.0.1:6379> TYPE name
string

127.0.0.1:6379> LPUSH mylist "a" "b" "c"
(integer) 3
127.0.0.1:6379> TYPE mylist
list

编码(Encoding)

每种类型可以有多种编码方式(底层实现):

// 编码方式
#define OBJ_ENCODING_RAW 0          // 简单动态字符串(SDS)
#define OBJ_ENCODING_INT 1          // long 类型的整数
#define OBJ_ENCODING_HT 2           // 字典(Dict)
#define OBJ_ENCODING_ZIPLIST 3      // 压缩列表(已废弃)
#define OBJ_ENCODING_INTSET 4       // 整数集合
#define OBJ_ENCODING_SKIPLIST 5     // 跳跃表
#define OBJ_ENCODING_EMBSTR 6       // embstr 编码的 SDS
#define OBJ_ENCODING_QUICKLIST 7    // 快速列表
#define OBJ_ENCODING_STREAM 8       // Stream
#define OBJ_ENCODING_LISTPACK 9     // 紧凑列表(Redis 7.0+)

查看编码

127.0.0.1:6379> SET num 123
OK
127.0.0.1:6379> OBJECT ENCODING num
"int"

127.0.0.1:6379> SET name "Redis"
OK
127.0.0.1:6379> OBJECT ENCODING name
"embstr"

127.0.0.1:6379> SET long_str "Very long string..."
OK
127.0.0.1:6379> OBJECT ENCODING long_str
"raw"

引用计数(refcount)

Redis 使用引用计数进行内存管理:

// 创建对象时,refcount = 1
robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = OBJ_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;  // 初始引用计数为 1
    o->lru = LRU_CLOCK();
    return o;
}

// 增加引用
void incrRefCount(robj *o) {
    o->refcount++;
}

// 减少引用
void decrRefCount(robj *o) {
    if (o->refcount == 1) {
        // 引用计数为 0,释放对象
        freeObject(o);
    } else {
        o->refcount--;
    }
}

查看引用计数

127.0.0.1:6379> SET key1 100
OK
127.0.0.1:6379> OBJECT REFCOUNT key1
(integer) 2147483647  # INT_MAX,表示这是共享对象

LRU/LFU 时间

用于内存淘汰策略:

// LRU(Least Recently Used)模式
// 记录对象最后一次被访问的时间

// LFU(Least Frequently Used)模式(Redis 4.0+)
// 记录对象的访问频率

查看空转时间

127.0.0.1:6379> SET key "value"
OK
127.0.0.1:6379> OBJECT IDLETIME key
(integer) 5  # 5 秒未被访问

2.2.2 String 对象

String 是 Redis 最基本的数据类型,可以存储:

  • 字符串
  • 整数
  • 浮点数
  • 二进制数据(图片、序列化对象等)

编码方式

String 对象有 3 种编码:

1️⃣ INT 编码

存储可以用 long 类型表示的整数(-2^63 ~ 2^63-1):

┌─────────────────────────────┐
│      RedisObject            │
├──────────┬──────────────────┤
│ type=0   │ encoding=1 (INT) │
│ (STRING) │                  │
├──────────┴──────────────────┤
│ ptr = 12345 (直接存储整数)   │
└─────────────────────────────┘

# 不需要额外分配内存
127.0.0.1:6379> SET num 123
OK
127.0.0.1:6379> OBJECT ENCODING num
"int"

127.0.0.1:6379> INCR num
(integer) 124
2️⃣ EMBSTR 编码

存储长度 ≤ 44 字节的短字符串:

┌──────────────────────────────────────────┐
│      RedisObject + SDS 连续分配           │
├──────────┬───────────────────────────────┤
│ RedisObj │        SDS                    │
│ (16B)    │ len|alloc|flags|buf (44B)    │
└──────────┴───────────────────────────────┘
    ↑______________|

# 只需一次内存分配
# 内存连续,缓存友好
# 只读,修改时转为 RAW
127.0.0.1:6379> SET short "Hello Redis"
OK
127.0.0.1:6379> OBJECT ENCODING short
"embstr"

127.0.0.1:6379> APPEND short " Cluster"
(integer) 19
127.0.0.1:6379> OBJECT ENCODING short
"raw"  # 修改后转为 RAW
3️⃣ RAW 编码

存储长度 > 44 字节的字符串:

┌────────────────┐
│  RedisObject   │
│                │
│  ptr ───────┐  │
└─────────────┼──┘
              ↓
         ┌─────────────┐
         │    SDS      │
         │ "Long str..." │
         └─────────────┘

# 需要两次内存分配
# RedisObject 和 SDS 分开存储
127.0.0.1:6379> SET long_str "Very long string that exceeds 44 bytes..."
OK
127.0.0.1:6379> OBJECT ENCODING long_str
"raw"

编码转换规则

创建 String 对象:
↓
是否是整数?
├─ 是 → INT 编码
└─ 否 → 长度 ≤ 44 字节?
        ├─ 是 → EMBSTR 编码
        └─ 否 → RAW 编码

修改 EMBSTR:
EMBSTR → RAW(EMBSTR 是只读的)

修改 INTINT → RAW(如果追加字符串)
INTINT(如果 INCR/DECR)

应用场景

1️⃣ 缓存
// 缓存用户信息
public User getUser(String userId) {
    String cacheKey = "user:" + userId;
    String userJson = redisTemplate.opsForValue().get(cacheKey);
    
    if (userJson != null) {
        return JSON.parseObject(userJson, User.class);
    }
    
    User user = userRepository.findById(userId);
    redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), 
        30, TimeUnit.MINUTES);
    return user;
}
2️⃣ 计数器
# 文章浏览量
INCR article:1001:views
GET article:1001:views

# 限流(每分钟最多 100 次)
SET rate:user:1001 1 EX 60 NX
INCR rate:user:1001
GET rate:user:1001  # 判断是否超过 100
3️⃣ 分布式锁
# 获取锁
SET lock:order:1001 uuid123 NX EX 10

# 释放锁(Lua 脚本保证原子性)
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
4️⃣ Session 存储
# 存储 Session
SET session:abc123 "user_id=1001,role=admin" EX 1800
GET session:abc123
5️⃣ 位图(Bitmap)
# 用户签到(每天一位)
SETBIT signin:user:1001:202510 26 1  # 10月26日签到

# 统计签到天数
BITCOUNT signin:user:1001:202510

# 判断是否签到
GETBIT signin:user:1001:202510 26

2.2.3 List 对象

List 是一个有序的字符串列表,可以从两端插入和弹出元素。

编码方式

List 对象只有 1 种编码(Redis 3.2+):

  • QUICKLIST:QuickList(ZipList + LinkedList 的混合结构)
127.0.0.1:6379> LPUSH mylist "a" "b" "c"
(integer) 3
127.0.0.1:6379> OBJECT ENCODING mylist
"quicklist"

编码转换

Redis 3.2 之前,List 有两种编码:

  • ZIPLIST:元素少且小
  • LINKEDLIST:元素多或大
# Redis 3.2 之前的配置
list-max-ziplist-entries 512  # 最多 512 个元素
list-max-ziplist-value 64     # 每个元素最大 64 字节

# 超过任一阈值,转换为 LINKEDLIST

Redis 3.2+ 统一使用 QuickList:

# redis.conf
list-max-ziplist-size -2      # 每个 ziplist 最多 8KB
list-compress-depth 0         # 压缩深度

常用命令

# 左侧插入/弹出
LPUSH mylist "a" "b" "c"    # 头部插入
LPOP mylist                 # 头部弹出

# 右侧插入/弹出
RPUSH mylist "x" "y" "z"    # 尾部插入
RPOP mylist                 # 尾部弹出

# 阻塞弹出
BLPOP mylist 5              # 阻塞 5 秒等待元素

# 范围查询
LRANGE mylist 0 -1          # 获取所有元素
LRANGE mylist 0 9           # 获取前 10 个

# 修改元素
LSET mylist 0 "new"         # 修改索引 0 的元素
LINSERT mylist BEFORE "a" "new"  # 在 "a" 前插入

# 其他操作
LLEN mylist                 # 获取长度
LINDEX mylist 0             # 获取索引 0 的元素
LTRIM mylist 0 99           # 只保留前 100 个

应用场景

1️⃣ 消息队列
# 生产者
RPUSH queue:tasks "task1" "task2" "task3"

# 消费者
BLPOP queue:tasks 5  # 阻塞等待任务
// Java 实现
public void produceTask(String task) {
    redisTemplate.opsForList().rightPush("queue:tasks", task);
}

public String consumeTask() {
    return redisTemplate.opsForList().leftPop("queue:tasks", 5, TimeUnit.SECONDS);
}
2️⃣ 最新动态列表
# 发布动态
LPUSH timeline:user:1001 "post123"
LTRIM timeline:user:1001 0 99  # 只保留最新 100 条

# 获取最新动态
LRANGE timeline:user:1001 0 9  # 获取最新 10 条
3️⃣ 评论列表
# 添加评论
RPUSH comments:article:1001 "comment1" "comment2"

# 分页查询
LRANGE comments:article:1001 0 19    # 第 1 页(0-19)
LRANGE comments:article:1001 20 39   # 第 2 页(20-39)

2.2.4 Hash 对象

Hash 是一个 field-value 的映射表,适合存储对象。

编码方式

Hash 对象有 2 种编码:

1️⃣ LISTPACK 编码(Redis 7.0+)

当 Hash 满足以下条件时使用:

  • 字段数量 ≤ 512(hash-max-listpack-entries
  • 每个字段和值的长度 ≤ 64 字节(hash-max-listpack-value
ListPack 存储:
┌─────────────────────────────────────┐
│ field1 | value1 | field2 | value2   │
└─────────────────────────────────────┘

# 内存紧凑,连续存储
127.0.0.1:6379> HSET user:1001 name "Alice" age "25"
(integer) 2
127.0.0.1:6379> OBJECT ENCODING user:1001
"listpack"
2️⃣ HT 编码(HashTable)

当 Hash 不满足 ListPack 条件时使用:

Dict 结构:
┌──────────────────────────┐
│      Hash Table          │
├──────────────────────────┤
│ field1 → value1          │
│ field2 → value2          │
│ ...                      │
└──────────────────────────┘

# 查询快,内存开销大
127.0.0.1:6379> HSET user:1002 bio "Very long biography..."
(integer) 1
127.0.0.1:6379> OBJECT ENCODING user:1002
"hashtable"

编码转换

# 初始使用 ListPack
HSET user:1 name "Alice"
OBJECT ENCODING user:1  # "listpack"

# 添加很多字段,超过 512 个
HSET user:1 field001 "v1" field002 "v2" ... field600 "v600"
OBJECT ENCODING user:1  # "hashtable"

# 或者某个值超过 64 字节
HSET user:1 bio "Very long string exceeds 64 bytes..."
OBJECT ENCODING user:1  # "hashtable"

内存优化

对比 String vs Hash 存储对象

# 方案 1:使用 String 存储 JSON
SET user:1001 '{"name":"Alice","age":25,"city":"Beijing"}'
# 优点:简单
# 缺点:修改单个字段需要反序列化整个对象

# 方案 2:使用 Hash 存储字段
HSET user:1001 name "Alice"
HSET user:1001 age 25
HSET user:1001 city "Beijing"
# 优点:可以单独修改字段,内存更省
# 缺点:稍复杂

内存对比(存储 100 万用户,每个用户 3 个字段):

方案内存占用说明
String (JSON)~200 MB每个 key 一个 RedisObject
Hash (分散)~180 MB每个 field 无 RedisObject
Hash (优化)~70 MB使用 ListPack 编码

优化技巧

# 将用户 ID 分片,减少 key 数量
# user:1001 → user:shard:1 field=1001

# redis.conf
hash-max-listpack-entries 512
hash-max-listpack-value 64

常用命令

# 设置字段
HSET user:1001 name "Alice"
HSET user:1001 age 25 city "Beijing"  # 批量设置
HMSET user:1001 name "Alice" age 25   # 批量设置(旧命令)

# 获取字段
HGET user:1001 name
HMGET user:1001 name age city         # 批量获取
HGETALL user:1001                     # 获取所有

# 判断字段存在
HEXISTS user:1001 name

# 删除字段
HDEL user:1001 age

# 自增
HINCRBY user:1001 age 1
HINCRBYFLOAT user:1001 score 1.5

# 其他
HLEN user:1001                        # 字段数量
HKEYS user:1001                       # 所有字段名
HVALS user:1001                       # 所有值

实战案例

1️⃣ 存储用户信息
public void saveUser(User user) {
    String key = "user:" + user.getId();
    Map<String, String> map = new HashMap<>();
    map.put("name", user.getName());
    map.put("age", String.valueOf(user.getAge()));
    map.put("city", user.getCity());
    
    redisTemplate.opsForHash().putAll(key, map);
}

public User getUser(String userId) {
    String key = "user:" + userId;
    Map<Object, Object> map = redisTemplate.opsForHash().entries(key);
    
    User user = new User();
    user.setId(userId);
    user.setName((String) map.get("name"));
    user.setAge(Integer.parseInt((String) map.get("age")));
    user.setCity((String) map.get("city"));
    return user;
}
2️⃣ 购物车
# 添加商品到购物车
HSET cart:user:1001 product:1001 2   # 商品 1001,数量 2
HSET cart:user:1001 product:1002 1

# 修改数量
HINCRBY cart:user:1001 product:1001 1  # 数量 +1

# 查看购物车
HGETALL cart:user:1001

# 删除商品
HDEL cart:user:1001 product:1001
3️⃣ 统计信息
# 统计网站访问量
HINCRBY stats:website pv 1           # PV +1
HINCRBY stats:website uv 1           # UV +1
HGET stats:website pv                # 查询 PV

2.2.5 Set 对象

Set 是无序的字符串集合,自动去重。

编码方式

Set 对象有 2 种编码:

1️⃣ INTSET 编码

当 Set 满足以下条件时使用:

  • 所有元素都是整数
  • 元素数量 ≤ 512(set-max-intset-entries
127.0.0.1:6379> SADD numbers 1 3 5 7 9
(integer) 5
127.0.0.1:6379> OBJECT ENCODING numbers
"intset"
2️⃣ HT 编码(HashTable)

当 Set 不满足 IntSet 条件时使用:

127.0.0.1:6379> SADD tags "redis" "cache" "database"
(integer) 3
127.0.0.1:6379> OBJECT ENCODING tags
"hashtable"

编码转换

# 初始使用 IntSet
SADD numbers 1 2 3
OBJECT ENCODING numbers  # "intset"

# 添加非整数元素
SADD numbers "abc"
OBJECT ENCODING numbers  # "hashtable"

# 或者超过 512 个元素

常用命令

# 添加元素
SADD tags "redis" "cache"

# 删除元素
SREM tags "cache"

# 判断存在
SISMEMBER tags "redis"

# 查看所有元素
SMEMBERS tags

# 随机获取
SRANDMEMBER tags 3       # 随机获取 3 个(不删除)
SPOP tags 2              # 随机弹出 2 个(删除)

# 集合运算
SINTER set1 set2         # 交集
SUNION set1 set2         # 并集
SDIFF set1 set2          # 差集

# 其他
SCARD tags               # 元素数量
SMOVE src dest member    # 移动元素

集合运算

# 共同好友
SADD user:1001:friends "u1" "u2" "u3" "u4"
SADD user:1002:friends "u2" "u3" "u5" "u6"
SINTER user:1001:friends user:1002:friends  # ["u2", "u3"]

# 推荐好友(好友的好友,排除已有好友)
SDIFF user:1002:friends user:1001:friends  # ["u5", "u6"]

# 标签匹配
SADD article:1001:tags "redis" "cache" "nosql"
SADD article:1002:tags "mysql" "sql" "database"
SADD search:tags "redis" "mysql"
SINTER article:1001:tags search:tags  # ["redis"]

应用场景

1️⃣ 标签系统
# 给文章打标签
SADD article:1001:tags "Redis" "NoSQL" "Cache"

# 查询带某个标签的文章
SADD tag:Redis:articles "1001" "1002" "1003"
SMEMBERS tag:Redis:articles
2️⃣ 点赞/关注
# 用户点赞文章
SADD article:1001:likes "user:1001" "user:1002"

# 判断是否点赞
SISMEMBER article:1001:likes "user:1001"

# 点赞数
SCARD article:1001:likes

# 取消点赞
SREM article:1001:likes "user:1001"
3️⃣ 抽奖系统
# 参与抽奖
SADD lottery:20251026 "user:1001" "user:1002" "user:1003"

# 随机抽取 3 个中奖者
SRANDMEMBER lottery:20251026 3

# 或者抽取后移除
SPOP lottery:20251026 3

2.2.6 ZSet 对象

ZSet(Sorted Set)是有序集合,每个元素关联一个分数(score),按分数排序。

编码方式

ZSet 对象有 2 种编码:

1️⃣ LISTPACK 编码(Redis 7.0+)

当 ZSet 满足以下条件时使用:

  • 元素数量 ≤ 128(zset-max-listpack-entries
  • 每个元素长度 ≤ 64 字节(zset-max-listpack-value
ListPack 存储:
┌────────────────────────────────────────┐
│ member1 | score1 | member2 | score2    │
└────────────────────────────────────────┘

# 按 score 排序存储
127.0.0.1:6379> ZADD rank 100 "user1" 95 "user2" 90 "user3"
(integer) 3
127.0.0.1:6379> OBJECT ENCODING rank
"listpack"
2️⃣ SKIPLIST 编码

当 ZSet 不满足 ListPack 条件时使用:

// ZSet 同时使用两种数据结构
typedef struct zset {
    dict *dict;          // 字典:member → score(O(1) 查找分数)
    zskiplist *zsl;      // 跳跃表:score → member(O(log n) 范围查询)
} zset;

为什么同时使用两种结构?

Dict: 快速查找 member 的 score
ZSCORE rank "user1"  # O(1)

SkipList: 快速范围查询
ZRANGE rank 0 9      # O(log n + m)
ZRANGEBYSCORE rank 80 100  # O(log n + m)
127.0.0.1:6379> ZADD leaderboard 1000 "user1" 950 "user2" ... (超过 128 个)
127.0.0.1:6379> OBJECT ENCODING leaderboard
"skiplist"

双重数据结构

┌─────────────────────────────────────────┐
│              ZSet                        │
├─────────────────┬───────────────────────┤
│     DictSkipList         │
├─────────────────┼───────────────────────┤
│ "user1" → 100L3: Header ─→ NULL   │
│ "user2" → 95L2: Header ─→ [100]  │
│ "user3" → 90L1: Header[90] →  │
│                 │       [95][100]    │
└─────────────────┴───────────────────────┘

# DictSkipList 共享 memberscore
# 不会重复存储

常用命令

# 添加元素
ZADD rank 100 "user1" 95 "user2" 90 "user3"

# 获取分数
ZSCORE rank "user1"

# 增加分数
ZINCRBY rank 10 "user1"

# 获取排名(从小到大,从 0 开始)
ZRANK rank "user1"           # 正序排名
ZREVRANK rank "user1"        # 逆序排名

# 范围查询(按排名)
ZRANGE rank 0 9              # 前 10 名(分数从小到大)
ZREVRANGE rank 0 9           # 前 10 名(分数从大到小)
ZRANGE rank 0 9 WITHSCORES   # 带分数

# 范围查询(按分数)
ZRANGEBYSCORE rank 80 100
ZREVRANGEBYSCORE rank 100 80

# 统计
ZCARD rank                   # 元素数量
ZCOUNT rank 80 100           # 分数在 [80, 100] 的元素数

# 删除
ZREM rank "user1"            # 删除指定元素
ZREMRANGEBYRANK rank 0 9     # 删除排名 [0, 9] 的元素
ZREMRANGEBYSCORE rank 0 60   # 删除分数 [0, 60] 的元素

排行榜实现

1️⃣ 游戏排行榜
// 更新玩家分数
public void updateScore(String userId, double score) {
    redisTemplate.opsForZSet().add("game:leaderboard", userId, score);
}

// 获取前 10 名
public List<String> getTopPlayers(int topN) {
    Set<String> players = redisTemplate.opsForZSet()
        .reverseRange("game:leaderboard", 0, topN - 1);
    return new ArrayList<>(players);
}

// 获取玩家排名
public Long getPlayerRank(String userId) {
    return redisTemplate.opsForZSet()
        .reverseRank("game:leaderboard", userId);
}

// 获取玩家分数
public Double getPlayerScore(String userId) {
    return redisTemplate.opsForZSet()
        .score("game:leaderboard", userId);
}

// 获取周围玩家(前后各 5 名)
public List<String> getNearbyPlayers(String userId) {
    Long rank = getPlayerRank(userId);
    if (rank == null) return Collections.emptyList();
    
    long start = Math.max(0, rank - 5);
    long end = rank + 5;
    
    return new ArrayList<>(redisTemplate.opsForZSet()
        .reverseRange("game:leaderboard", start, end));
}
2️⃣ 热搜榜
# 搜索关键词,增加热度
ZINCRBY hotsearch:20251026 1 "Redis"
ZINCRBY hotsearch:20251026 1 "Spring Boot"

# 获取热搜榜 Top 10
ZREVRANGE hotsearch:20251026 0 9 WITHSCORES

# 定时任务:清理过期数据
EXPIRE hotsearch:20251026 86400  # 24 小时后过期
3️⃣ 延迟队列
# 添加延迟任务(score = 执行时间戳)
ZADD delay:queue 1698307200 "task1"  # 2023-10-26 12:00:00
ZADD delay:queue 1698310800 "task2"  # 2023-10-26 13:00:00

# 获取到期任务
ZRANGEBYSCORE delay:queue 0 <当前时间戳>

# 删除已执行的任务
ZREM delay:queue "task1"

2.2.7 对象共享

Redis 在启动时会创建一些常用对象,供多个键共享,节省内存。

共享对象

// Redis 启动时创建 0-9999 的整数对象
#define OBJ_SHARED_INTEGERS 10000

void createSharedObjects(void) {
    for (int i = 0; i < OBJ_SHARED_INTEGERS; i++) {
        shared.integers[i] = createObject(OBJ_STRING, (void*)(long)i);
        shared.integers[i]->encoding = OBJ_ENCODING_INT;
        shared.integers[i]->refcount = INT_MAX;  // 永不释放
    }
}

示例

127.0.0.1:6379> SET key1 100
OK
127.0.0.1:6379> SET key2 100
OK
127.0.0.1:6379> OBJECT REFCOUNT key1
(integer) 2147483647  # INT_MAX,表示共享对象

127.0.0.1:6379> SET key3 10000  # 超出共享范围
OK
127.0.0.1:6379> OBJECT REFCOUNT key3
(integer) 1  # 独立对象

为什么只共享整数?

// 共享字符串对象需要比较内容
// 时间复杂度 O(n),得不偿失

// 共享整数只需比较指针
// 时间复杂度 O(1)

maxmemory-policy 的影响

# 如果使用 LFU 淘汰策略,会禁用对象共享
# 因为 LFU 需要记录每个对象的访问频率

maxmemory-policy allkeys-lfu  # 禁用对象共享
maxmemory-policy allkeys-lru  # 允许对象共享

2.2.8 对象的生命周期

创建对象

robj *createStringObject(char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        // EMBSTR 编码
        return createEmbeddedStringObject(ptr, len);
    } else {
        // RAW 编码
        return createRawStringObject(ptr, len);
    }
}

访问对象

// 每次访问对象时,更新 LRU 时间
robj *lookupKey(redisDb *db, robj *key) {
    dictEntry *de = dictFind(db->dict, key->ptr);
    if (de) {
        robj *val = dictGetVal(de);
        
        // 更新 LRU 时间
        if (!server.loading) {
            val->lru = LRU_CLOCK();
        }
        
        return val;
    }
    return NULL;
}

释放对象

void decrRefCount(robj *o) {
    if (o->refcount == 1) {
        switch(o->type) {
        case OBJ_STRING: freeStringObject(o); break;
        case OBJ_LIST: freeListObject(o); break;
        case OBJ_SET: freeSetObject(o); break;
        case OBJ_ZSET: freeZsetObject(o); break;
        case OBJ_HASH: freeHashObject(o); break;
        }
        zfree(o);
    } else {
        o->refcount--;
    }
}

内存优化最佳实践

1️⃣ 选择合适的数据类型

# ❌ 不好的做法
SET user:1001:name "Alice"
SET user:1001:age "25"
SET user:1001:city "Beijing"
# 3 个 RedisObject,内存开销大

# ✅ 推荐做法
HSET user:1001 name "Alice" age "25" city "Beijing"
# 1 个 RedisObject,节省内存

2️⃣ 控制编码阈值

# redis.conf
hash-max-listpack-entries 512
hash-max-listpack-value 64

zset-max-listpack-entries 128
zset-max-listpack-value 64

set-max-intset-entries 512

3️⃣ 使用合适的 Key 命名

# ❌ Key 太长
SET user:profile:information:name:fullname "Alice"

# ✅ 简洁的 Key
SET u:1001:n "Alice"

4️⃣ 避免 BigKey

# 查找大键
redis-cli --bigkeys

# 大键的危害
# - 内存占用高
# - 删除耗时长
# - 网络传输慢

5️⃣ 使用对象共享

# 尽量使用 0-9999 的整数
SET key 100  # 共享对象
SET key "100"  # 独立对象(字符串)

常见问题解答

Q1: String 为什么有 3 种编码?

A: 内存优化。

  • INT:整数直接存在 ptr 中,省内存
  • EMBSTR:短字符串一次分配,缓存友好
  • RAW:长字符串独立分配,灵活修改

Q2: Hash 什么时候用 ListPack,什么时候用 Dict?

A: 根据数据量自动选择。

  • ListPack:字段少(< 512)且小(< 64B),内存紧凑
  • Dict:字段多或大,查询快

Q3: ZSet 为什么同时使用 Dict 和 SkipList?

A: 性能平衡。

  • Dict:O(1) 查找 member 的 score
  • SkipList:O(log n) 范围查询和排名

Q4: 如何查看对象的编码?

A: 使用 OBJECT ENCODING 命令。

OBJECT ENCODING key
OBJECT REFCOUNT key
OBJECT IDLETIME key

Q5: 对象共享有什么限制?

A:

  • 只共享 0-9999 的整数
  • LFU 淘汰策略会禁用共享
  • 共享对象 refcount = INT_MAX

总结

本文深入剖析了 Redis 对象系统:

  1. RedisObject:统一的对象结构,16 字节
  2. String:3 种编码(INT、EMBSTR、RAW)
  3. List:QuickList 编码,混合结构
  4. Hash:ListPack 或 Dict,适合存储对象
  5. Set:IntSet 或 Dict,自动去重
  6. ZSet:ListPack 或 SkipList+Dict,有序集合

核心设计思想

  • ✅ 类型安全:编译时检查类型
  • ✅ 多态:同一命令,不同实现
  • ✅ 内存优化:根据数据量自动选择编码
  • ✅ 对象共享:小整数共享,节省内存

理解对象系统,能帮助你:

  • ✅ 选择最优数据类型
  • ✅ 优化内存使用
  • ✅ 提升查询性能
  • ✅ 避免常见陷阱

💡 下一篇预告:《Redis 持久化机制详解:RDB vs AOF vs 混合持久化》

敬请期待!