RedisObject 与五大数据类型
在上一篇文章中,我们深入剖析了 Redis 的 8 种底层数据结构。但用户在使用 Redis 时,并不直接操作这些底层结构,而是通过 RedisObject 对象系统 使用五大数据类型。本文将揭秘 Redis 如何将底层结构封装成用户友好的数据类型。
📖 目录
- 为什么需要对象系统?
- 2.2.1 RedisObject 结构
- 2.2.2 String 对象
- 2.2.3 List 对象
- 2.2.4 Hash 对象
- 2.2.5 Set 对象
- 2.2.6 ZSet 对象
- 2.2.7 对象共享
- 2.2.8 对象的生命周期
- 内存优化最佳实践
- 常见问题解答
为什么需要对象系统?
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 bytes │ 8 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 是只读的)
修改 INT:
INT → RAW(如果追加字符串)
INT → INT(如果 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 │
├─────────────────┬───────────────────────┤
│ Dict │ SkipList │
├─────────────────┼───────────────────────┤
│ "user1" → 100 │ L3: Header ─→ NULL │
│ "user2" → 95 │ L2: Header ─→ [100] │
│ "user3" → 90 │ L1: Header → [90] → │
│ │ [95] → [100] │
└─────────────────┴───────────────────────┘
# Dict 和 SkipList 共享 member 和 score
# 不会重复存储
常用命令
# 添加元素
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 对象系统:
- RedisObject:统一的对象结构,16 字节
- String:3 种编码(INT、EMBSTR、RAW)
- List:QuickList 编码,混合结构
- Hash:ListPack 或 Dict,适合存储对象
- Set:IntSet 或 Dict,自动去重
- ZSet:ListPack 或 SkipList+Dict,有序集合
核心设计思想:
- ✅ 类型安全:编译时检查类型
- ✅ 多态:同一命令,不同实现
- ✅ 内存优化:根据数据量自动选择编码
- ✅ 对象共享:小整数共享,节省内存
理解对象系统,能帮助你:
- ✅ 选择最优数据类型
- ✅ 优化内存使用
- ✅ 提升查询性能
- ✅ 避免常见陷阱
💡 下一篇预告:《Redis 持久化机制详解:RDB vs AOF vs 混合持久化》
敬请期待!