Redis Hash与Ziplist优化
Redis 的五种数据结构各有特色,用对了才能发挥它的优势。很多人只用到了 String 和 Hash,却不知道 List、Set、ZSet 在特定场景下更合适。本文从应用场景出发,讲什么时候用什么类型。
一、Hash的编码方式
Redis Hash有两种底层编码:
| 编码 |
结构 |
适用场景 |
| ziplist |
压缩列表 |
字段少、值小的Hash |
| hashtable |
哈希表 |
字段多或值大的Hash |
#
1.1 编码转换条件
默认配置(redis.conf): hash-max-ziplist-entries 512 # ziplist最多512个entry hash-max-ziplist-value 64 # 每个value最大64字节
|
转换规则:
- Hash满足条件:使用ziplist
- 任一条件不满足:转换为hashtable
- 转换是单向的:ziplist → hashtable后不会回退
二、Ziplist详解
#
2.1 Ziplist结构
Ziplist是紧凑的连续内存结构:
整体布局: ┌─────────┬─────────┬─────────────────────┬─────────┐ │ zlbytes │ zltail │ zllen │ entries │ zlend │ │ 4字节 │ 4字节 │ 2字节 │ 变长 │ 1字节 │ └─────────┴─────────┴─────────┴───────────┴─────────┘
各字段说明: - zlbytes: 整个ziplist占用的字节数 - zltail: 到最后一个entry的偏移量(方便快速定位尾部) - zllen: entry数量(超过65535时需遍历统计) - entries: 实际的entry列表 - zlend: 结束标记,固定为0xFF
|
#
2.2 Entry结构
每个Entry的格式: ┌─────────────────┬─────────────────┬─────────────────┐ │ prevlen │ encoding │ content │ │ 1或5字节 │ 1或2或5字节 │ 变长 │ └─────────────────┴─────────────────┴─────────────────┘
prevlen: 前一个entry的长度(方便倒序遍历) encoding: 编码信息,包含content类型和长度 content: 实际数据(整数或字符串)
|
prevlen的连锁更新问题:
当前entry的prevlen是1字节(记录前entry长度<254) 如果前entry增大到>=254字节 → 当前entry的prevlen需从1字节扩展到5字节 → 可能导致当前entry也变长 → 下一个entry的prevlen可能也要扩展 → 连锁反应!
|
这是ziplist的已知缺陷,在大量entry且值大小波动时可能触发。
#
2.3 Ziplist的优缺点
优点:
- 内存紧凑,无额外指针开销
- 适合小数据量场景
- CPU缓存友好(连续内存)
缺点:
- 插入/删除可能需要重新分配内存
- 连锁更新问题
- 查找特定字段O(n)(线性查找)
三、Hashtable详解
#
3.1 字典结构
typedef struct dict { dictType *type; void *privdata; dictht ht[2]; int rehashidx; } dict;
typedef struct dictht { dictEntry **table; unsigned long size; unsigned long sizemask; unsigned long used; } dictht;
typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; } v; struct dictEntry *next; } dictEntry;
|
#
3.2 哈希算法
uint64_t dictGenHashFunction(const void *key, int len) { }
idx = hash(key) & d->ht[0].sizemask;
|
#
3.3 渐进式Rehash
当哈希表负载因子超过阈值时,需要扩容:
触发条件: - 没有执行BGSAVE/BGREWRITEAOF时,负载因子 >= 1 - 执行BGSAVE/BGREWRITEAOF时,负载因子 >= 5
负载因子 = used / size
|
渐进式Rehash流程:
初始状态: ht[0]: 大小4,已用3 ht[1]: 空 rehashidx: -1
开始rehash: 1. 为ht[1]分配大小为8的空间 2. rehashidx = 0 增删查时渐进迁移: - 每次操作将ht[0]中rehashidx位置的桶迁移到ht[1] - rehashidx++ 完成时: - ht[0] = ht[1] - ht[1] 清空 - rehashidx = -1
|
优势:
- 避免一次性rehash的长时间阻塞
- 分摊到每次操作中
四、Hash命令的实现
#
4.1 HSET的实现
void hsetCommand(client *c) { robj *o; if ((o = hashTypeLookupWriteOrCreate(c, c->argv[1])) == NULL) return; hashTypeTryConversion(o, c->argv, 2, 3); hashTypeSet(o, c->argv[2]->ptr, c->argv[3]->ptr, HASH_SET_COPY); addReply(c, shared.ok); }
|
#
4.2 编码转换检查
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) { if (o->encoding != OBJ_ENCODING_ZIPLIST) return; for (int i = start; i <= end; i++) { if (sdslen(argv[i]->ptr) > server.hash_max_ziplist_value) { hashTypeConvert(o, OBJ_ENCODING_HT); break; } } }
|
#
4.3 HGETALL的实现
void hgetallCommand(client *c) { robj *o = lookupKeyReadOrReply(c, c->argv[1], shared.null[c->resp]); if (o == NULL || checkType(c, o, OBJ_HASH)) return; addReplyArrayLen(c, hashTypeLength(o) * 2); hashTypeIterator *hi = hashTypeInitIterator(o); while (hashTypeNext(hi) != C_ERR) { addReplyBulkCBuffer(c, hi->zk, hi->klen); addReplyBulkCBuffer(c, hi->zv, hi->vlen); } hashTypeReleaseIterator(hi); }
|
五、内存优化实践
#
5.1 合理设置ziplist阈值
# redis.conf
# 如果Hash字段多但值小,可以增大entries限制 hash-max-ziplist-entries 1000 hash-max-ziplist-value 128
# 如果Hash字段少但值大,保持默认值或减小
|
#
5.2 控制Hash的字段数量
for (int i = 0; i < 100000; i++) { redis.hset("big:hash", "field" + i, "value" + i); }
int shardCount = 100; for (int i = 0; i < 100000; i++) { int shard = i % shardCount; redis.hset("hash:shard:" + shard, "field" + i, "value" + i); }
|
#
5.3 使用Hash替代String存储对象
#
5.4 查看编码信息
OBJECT ENCODING user:1001
MEMORY USAGE user:1001
DEBUG OBJECT user:1001
|
六、性能测试对比
#
6.1 Ziplist vs Hashtable
redis-benchmark -n 100000 HSET small:hash field value
redis-benchmark -n 100000 HSET big:hash field1000 value1000
redis-benchmark -n 100000 HGET small:hash field
|
#
6.2 编码转换的影响
七、Hash应用优化
#
7.1 购物车优化
@Service public class CartService { private static final int CART_EXPIRE_DAYS = 7; public void addToCart(Long userId, Long skuId, Integer quantity) { String key = "cart:" + userId; String field = String.valueOf(skuId); Long newQty = redis.hincrBy(key, field, quantity); if (newQty <= 0) { redis.hdel(key, field); } redis.expire(key, CART_EXPIRE_DAYS * 86400); } public Map<Long, Integer> getCart(Long userId) { String key = "cart:" + userId; Map<String, String> entries = redis.hgetAll(key); Map<Long, Integer> cart = new HashMap<>(); entries.forEach((k, v) -> cart.put(Long.valueOf(k), Integer.valueOf(v))); return cart; } public void clearCart(Long userId) { redis.del("cart:" + userId); } }
|
#
7.2 配置中心优化
@Component public class ConfigManager { private static final String CONFIG_KEY = "app:config"; @PostConstruct public void init() { Map<String, String> configs = loadFromDB(); redis.hsetAll(CONFIG_KEY, configs); redis.expire(CONFIG_KEY, 3600); } public String getConfig(String key) { String value = redis.hget(CONFIG_KEY, key); if (value == null) { value = db.queryConfig(key); if (value != null) { redis.hset(CONFIG_KEY, key, value); } } return value; } public void updateConfig(String key, String value) { db.updateConfig(key, value); redis.hset(CONFIG_KEY, key, value); } }
|
八、常见问题
#
8.1 Hash的字段数量限制
理论限制:约40亿个字段(2^32 - 1) 实际限制:内存大小
建议:单个Hash不超过1000个字段(保持ziplist编码)
|
#
8.2 HGETALL的性能问题
Map<String, String> all = redis.hgetAll("big:hash");
Cursor<Map.Entry<String, String>> cursor = redis.opsForHash() .scan("big:hash", ScanOptions.scanOptions().count(100).build());
while (cursor.hasNext()) { Map.Entry<String, String> entry = cursor.next(); processEntry(entry); }
|
#
8.3 Hash过期问题
九、总结
| 特性 |
Ziplist |
Hashtable |
| 内存占用 |
低 |
高 |
| 读写性能 |
小数据量优 |
大数据量优 |
| 查找复杂度 |
O(n) |
O(1) |
| 插入开销 |
可能需要内存重分配 |
可能需要rehash |
| 适用场景 |
字段少、值小 |
字段多、值大 |
Hash优化核心要点:
- 保持ziplist编码:控制字段数量和值大小
- 避免大Hash:单个Hash不超过1000字段
- 分片存储:大数据量分多个Hash
- 使用HSCAN:避免HGETALL大Hash
- 合理设置阈值:根据实际数据调整hash-max-ziplist-*
核心要点
String:简单的键值对,适合缓存、计数器
Hash:存储对象属性,适合用户信息、配置
List:有序列表,适合消息队列、最新列表
Set:无序去重,适合共同好友、抽奖
ZSet:有序集合,适合排行榜、积分系统
总结
选择合适的数据结构是使用 Redis 的关键。在实际项目中,根据业务需求选择合适的类型,可以提升性能和开发效率。