Redis作为内存数据库,对内存使用效率有着极致追求。压缩列表(ziplist)正是这种理念下的产物——它通过精巧的结构设计和紧凑的内存布局,在特定场景下能大幅降低内存消耗。我在实际项目中曾对比过,当存储小型整数集合时,ziplist相比常规链表能节省40%以上的内存空间。
典型使用场景:
list-max-ziplist-entries配置值(默认512),且每个元素值大小小于list-max-ziplist-value(默认64字节)时hash-max-ziplist-entries(默认512),且每个字段名和值都小于hash-max-ziplist-value(默认64字节)时提示:可通过
redis-cli config get *max-ziplist*查看当前配置阈值,根据业务数据特征调整这些参数能获得最佳内存/性能平衡。
压缩列表本质上是一个字节数组,其物理结构如下所示:
c复制struct ziplist {
uint32_t zlbytes; // 整个压缩列表占用的字节数
uint32_t zltail; // 到尾节点的偏移量
uint16_t zllen; // 节点数量
entry[] entries; // 节点数组
uint8_t zlend; // 结束标记0xFF
};
关键字段作用:
zlbytes:快速获取列表总长度,无需遍历即可进行内存重分配zltail:实现O(1)时间复杂度访问尾节点,这对RPUSH等操作至关重要zllen:节点数量超过UINT16_MAX时需要全表遍历,这种情况在实际中极少出现每个节点由三部分组成:
c复制struct entry {
unsigned int prevlen; // 前驱节点长度
unsigned char encoding; // 数据类型标识
void *content; // 数据内容
};
prevlen的变长设计:
这种设计带来两个重要特性:
当存储字符串时,Redis使用三种编码方式:
| 编码格式 | 长度标识位 | 最大长度 | 适用场景 |
|---|---|---|---|
| 00xxxxxx | 6位 | 63字节 | 短字符串(如Redis键) |
| 01xxxxxx xxxxxxxx | 14位 | 16,383字节 | 中等长度字符串 |
| 10______ xxxxxxxx ... | 32位 | 4,294,967,295字节 | 长文本数据 |
实战案例:
存储字符串"hello"的编码过程:
Redis支持6种整数类型存储:
| 编码格式 | 数据类型 | 数值范围 |
|---|---|---|
| 11000000 | int16_t | -32,768 ~ 32,767 |
| 11010000 | int32_t | -2,147,483,648 ~ 2,147,483,647 |
| 11100000 | int64_t | -2^63 ~ 2^63-1 |
| 11110000 | 24位有符号整数 | -8,388,608 ~ 8,388,607 |
| 11111110 | 8位有符号整数 | -128 ~ 127 |
| 1111xxxx | 4位无符号整数 | 0 ~ 12(直接存于编码) |
优化技巧:
对于0-12的小整数,Redis直接将其存储在encoding字段的xxxx四位中,完全省略content部分。这种设计使得存储小整数时空间效率达到极致。
连锁更新本质上是prevlen字段长度变化引发的多米诺效应。典型场景:
时间复杂度对比:
通过压力测试发现:
注意事项:虽然理论最坏复杂度较高,但实际使用时无需过度担心。可通过监控
ziplist相关指标(如节点平均长度)来评估风险。
| 操作 | 平均复杂度 | 最坏复杂度 | 说明 |
|---|---|---|---|
| ziplistPush | O(N) | O(N^2) | 尾部插入效率高于头部 |
| ziplistInsert | O(N) | O(N^2) | 随机插入性能波动较大 |
| ziplistDelete | O(N) | O(N^2) | 删除后可能触发内存紧缩 |
| ziplistFind | O(N^2) | O(N^2) | 需要逐节点比较内容 |
Redis采用增量式内存分配:
优化建议:
对于已知会持续增长的列表,可预先通过redis-cli DEBUG ZIPLIST size估算预估所需空间,避免频繁重分配。
redis复制# 列表键配置
list-max-ziplist-entries 512 # 元素超过512转用linkedlist
list-max-ziplist-value 64 # 元素值超过64字节转码
# 哈希键配置
hash-max-ziplist-entries 512 # 字段超过512转用hashtable
hash-max-ziplist-value 64 # 字段值超过64字节转码
调优原则:
bash复制# 查看ziplist内存使用
redis-cli memory stats | grep ziplist
# 获取特定key的编码类型
redis-cli object encoding <key>
异常情况处理:
当发现ziplist编码的Key性能下降时:
| 特性 | ziplist | quicklist |
|---|---|---|
| 内存效率 | 极高 | 较高 |
| 插入性能 | 尾部快,头部慢 | 头尾都快 |
| 范围查询 | O(N) | O(N)但缓存友好 |
| 适用场景 | 小数据量 | 大数据量列表 |
| 特性 | ziplist | hashtable |
|---|---|---|
| 内存占用 | 极低 | 较高(指针开销) |
| 查找速度 | O(N) | O(1) |
| 字段数量 | <512 | 无限制 |
| 修改效率 | 可能触发连锁更新 | 稳定O(1) |
在实际项目中,我通常采用这样的策略:初期使用ziplist节省内存,当数据增长到阈值的80%时,通过Lua脚本自动转换为更合适的数据结构。这种渐进式策略在内存和性能之间取得了良好平衡。