1. Redis 内存模型与优化策略
1.1 Redis 对象结构
Redis 使用 Redis Object(robj) 作为所有数据类型的统一包装结构,每个键值对都对应一个 robj 和一个 dictEntry:
┌───────────────────────────────────────────┐
│ dictEntry (哈希表节点) │
├──────────────┬────────────────────────────┤
│ key │ robj → 字符串 "user:1001" │
│ value │ robj → 实际数据结构 │
│ next │ 链表指针(处理哈希冲突) │
└──────────────┴────────────────────────────┘
┌───────────────────────────────────────────┐
│ robj (Redis Object) │
├──────────────┬────────────────────────────┤
│ type │ 数据类型(string/list/hash/ │
│ │ set/zset/sstream等) │
│ encoding │ 内部编码(raw/int/hashtable │
│ │ /ziplist/skiplist等) │
│ lru │ 最后访问时间(LRU/LFU用) │
│ refcount │ 引用计数(共享对象复用) │
│ *ptr │ 指向实际数据结构的指针 │
└──────────────┴────────────────────────────┘
编码方式自动选择:
Redis 会根据数据量和元素大小自动选择最紧凑的编码:
| 数据类型 | 条件 | 紧凑编码(省内存) | 普通编码 |
|---|---|---|---|
| String | 整数值且 < 2^63 | int (16B) | embstr / raw |
| String | 短字符串且 ≤ 44B | embstr (一次分配) | raw |
| List | 元素数 ≤ 512 且元素 ≤ 64B | listpack (紧凑列表) | quicklist |
| Set | 整数元素且数量 ≤ 512 | intset (有序整数数组) | hashtable |
| Hash | 字段数 ≤ 512 且字段/值 ≤ 64B | listpack (紧凑哈希) | hashtable |
| ZSet | 元素数 ≤ 128 且成员 ≤ 64B | listpack (紧凑有序集) | skiplist |
内存占用的一个重要影响因素是数据结构开销。以 Hash 类型为例,当使用 hashtable 编码时:
hashtable 编码的 Hash 内存开销估算:
所需内存 ≈ dictEntry × n + robj(键) × n + robj(值) × n
≈ (key指针 + value指针 + next指针 + 对齐) × n
≈ 32B × n (每个键值对) + 键和值的实际内容
对比 listpack 编码:
listpack 将多个字段-值对紧凑排列在一段连续内存中
开销 ≈ 1-2B × n 的紧凑格式元数据 + 实际内容
节省约 80-90% 的内存!
1.2 查看内存使用
# 内存统计
INFO memory
# used_memory: 1073741824 # Redis 分配的总内存(字节)
# used_memory_rss: 1572864000 # OS 视角 RSS(含碎片、库等)
# mem_fragmentation_ratio: 1.46 # 碎片率 = RSS / used_memory
# used_memory_dataset: 805306368 # 纯数据占用
# used_memory_overhead: 268435456 # 内部数据结构开销
1.2 减少内存占用
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 哈希化小对象 | Hash 存储 > 10 个字段的对象 | 用户资料、配置 |
| 整数编码 | String 存整数用 int 编码 | 计数器、ID |
| ziplist/listpack | 小集合用紧凑编码 | 列表元素 < 512,单个 < 64B |
| 共享对象 | 0-9999 整数复用 | 计数器值 |
| 持久化压缩 | RDB 启用压缩 | 备份文件 |
# 查看某个 key 的内存占用
MEMORY USAGE user:1001
# 查看编码方式
OBJECT ENCODING user:1001:name # "embstr" / "raw" / "int"
1.3 内存优化实战技巧
哈希化对象(Hashing):
将原本用多个 String 存储的对象字段合并为 Hash:
# ❌ 低效:每个字段一个 String(10 个字段 = 10 个 dictEntry + 10 个 robj)
SET user:1001:name "Alice"
SET user:1001:age 25
SET user:1001:email "alice@example.com"
# ... 10 个键
# ✅ 高效:使用 Hash(1 个 dictEntry + 1 个 robj,内部紧凑编码)
HSET user:1001 name "Alice" age 25 email "alice@example.com"
# 内存节省 80-90%
当 Hash 字段超过 512 或单个字段/值超过 64B 时,Redis 会自动从 listpack 切换到 hashtable 编码。因此对于超大对象,可以手动分桶:
# 1000 个字段的对象 → 分 10 个桶,每桶 <= 100 个字段
HSET user:1001:bucket0 field1 val1 ... field100 val100
HSET user:1001:bucket1 field101 val101 ... field200 val200
# 查询时根据字段哈希值定位到对应桶
Ziplist/Listpack 配置调优:
# 控制 listpack 编码的阈值(Redis 7.0+)
# list-max-listpack-size: 单个 listpack 的最大元素数(默认 -2 = 8KB)
# 正值 = 元素个数上限;负值 = 以 KB 为单位的大小上限
list-max-listpack-size -2 # 8KB
hash-max-listpack-entries 512 # Hash 元素数上限
hash-max-listpack-value 64 # 单个字段/值的大小上限(字节)
set-max-intset-entries 512 # Set 使用 intset 的元素数上限
zset-max-listpack-entries 128 # ZSet 使用 listpack 的元素数上限
zset-max-listpack-value 64 # ZSet 元素值大小上限
调大这些阈值让更多数据使用紧凑编码,但会增加单个操作的延迟(因为需要遍历更多元素)。对于读多写少的小对象,增大这些值能显著降低内存占用。
共享对象池:
Redis 内部共享 0-9999 的整数对象。当 String 存储的整数在这个范围内时,多个键共享同一个 robj,大幅降低内存:
# 共享对象(0-9999)
SET counter:page_view 1000 # 引用计数共享的 int robj
SET user:1001:login_count 1000 # 同样引用共享对象
# 超出共享范围(> 9999)
SET big:value 100000000 # 创建新的 int robj,不共享
### 1.4 内存上限与淘汰策略
```bash
## 配置最大内存
maxmemory 2gb
## 淘汰策略
maxmemory-policy allkeys-lru # 所有 key LRU 淘汰
## volatile-lru: 只淘汰有过期时间的 key(LRU)
## allkeys-lfu: 所有 key LFU 淘汰
## volatile-ttl: 淘汰即将过期的 key
## noeviction: 不淘汰,新写入报错(默认)
2. 大 Key 治理
2.1 大 Key 的危害与定义
什么是"大 Key"?Redis 单线程模型下,大 Key 的定义取决于数据类型和操作方式:
| 数据类型 | 大 Key 标准 | 原因 |
|---|---|---|
| String | > 10KB | 单 KV 过大,GET/SET 阻塞时间长 |
| Hash | > 1000 个字段 或 总大小 > 10MB | HGETALL/HLEN 需要遍历全部元素 |
| List | > 10000 个元素 或 总大小 > 10MB | LRANGE 0 -1 返回全部,阻塞并占用带宽 |
| Set | > 10000 个元素 | SMEMBERS 全部返回 |
| ZSet | > 10000 个元素 | ZRANGE 范围查询需要遍历跳表和哈希表 |
| Stream | 消息堆积 | XREADGROUP 消费组阻塞 |
大 Key 的危害不仅限于单次操作的阻塞时间,还包括:
主从同步延迟:主节点执行大 Key 写入后,需要将整个 Key 的序列化数据同步给从节点。如果 Key 大小为 100MB,同步过程会阻塞主从复制通道,导致所有后续写的复制延迟。
持久化影响:
- RDB: Fork 子进程执行快照时,大 Key 的存在会增加 Copy-on-Write 页面数量,导致内存瞬时翻倍。
- AOF:如果 AOF 处于
everysec模式,大 Key 的写入会导致单次 fsync 的数据量剧增。
集群模式热 Key:Cluster 模式下,大 Key 所在的节点会成为热点,槽位重分配时需要迁移整个 Key。
2.2 大 Key 的危害
| 影响 | 说明 |
|---|---|
| 阻塞 | 单线程 Redis,大 Key 操作阻塞其他请求 |
| 网络瓶颈 | 传输大 Key 占用带宽 |
| 复制延迟 | 主从同步时大 Key 造成延迟 |
| 内存不均衡 | Cluster 模式下某节点内存突增 |
2.2 检测大 Key
## 扫描大 Key(线上慎用,会阻塞)
redis-cli --bigkeys
## 内存采样分析
redis-cli --memkeys
## 查看具体 key 大小
MEMORY USAGE mykey SAMPLES 50
2.3 大 Key 拆分
## 大 Hash 拆分:按字段前缀分桶
## 原: HGETALL article:123 → 10000 个字段
## 拆: HGETALL article:123:content
## HGETALL article:123:metadata
## 大 List 拆分:按时间窗口
## 原: LPUSH log → 1000 万条
## 拆: LPUSH log:202401
## LPUSH log:202402
## 大 ZSet 拆分:按分数范围
## 原: ZADD timeline → 100 万条
## 拆: ZADD timeline:2024Q1
## ZADD timeline:2024Q2
## 渐进式删除大 Key(非阻塞)
UNLINK bigkey # 异步删除(Redis 4.0+)
## 或分批次删除
LRANGE biglist 0 99
LTRIM biglist 100 -1 # 循环直到删完
3. 慢查询诊断
3.1 慢查询日志
## 配置慢查询阈值(微秒)
CONFIG SET slowlog-log-slower-than 10000 # > 10ms
CONFIG SET slowlog-max-len 128
## 查看慢查询
SLOWLOG GET 10
SLOWLOG LEN
SLOWLOG RESET
3.2 延迟监控
## 内置延迟监控
CONFIG SET latency-monitor-threshold 10 # > 10ms 记录
LATENCY LATEST # 最新延迟事件
LATENCY HISTORY command # 命令延迟历史
LATENCY GRAPH command # 命令延迟分布图
LATENCY GRAPH fork # RDB/AOF fork 延迟
LATENCY DOCTOR # 延迟诊断建议
LATENCY RESET # 重置统计
3.3 常见延迟原因
| 原因 | 现象 | 解决 |
|---|---|---|
| 大 Key 操作 | 某命令耗时数百 ms | 拆分大 Key |
| AOF 同步 | fsync 阻塞 | 改为 everysec 或 no |
| RDB Fork | 内存大时 fork 耗时 | 禁用/增大内存/使用 AOF |
| 内存交换 | 内存不足触发 swap | 增大物理内存或限制 maxmemory |
| 网络延迟 | 跨可用区/地域 | 就近部署或使用连接池 |
4. 内存碎片与碎片整理
内存碎片是 Redis 长期运行后内存利用率下降的主要原因。碎片率 mem_fragmentation_ratio = RSS / used_memory,正常值在 1.0 ~ 1.5 之间。
碎片产生原因:
碎片来源:
1. 键值对删除差异:反复创建和删除不同大小的键,内存分配器(jemalloc)
将内存划分为不同大小的 slab,小对象删除后留下无法复用的碎片
2. 编码升级:Hash 从 listpack 升级到 hashtable 时,新分配更大块
3. AOF 重写:AOF rewrite 期间创建新文件,旧文件释放后不连续
4. 淘汰策略:频繁淘汰导致内存分配模式不连续
碎片整理(碎片率 > 1.5 时):
## 查看碎片率
redis-cli INFO memory | grep mem_fragmentation_ratio
## Redis 4.0+ 支持在线碎片整理(实验性功能)
redis-cli --eval -
> redis.replicate_commands() -- 启用脚本复制模式
## 或配置文件中启用
activedefrag yes # 开启自动碎片整理
active-defrag-threshold-lower 10 # 碎片率 > 10% 开始整理
active-defrag-threshold-upper 100 # 碎片率 < 100% 停止整理
active-defrag-ignore-bytes 100mb # 最小整理大小
active-defrag-cycle-min 5 # CPU 最低占用 5%
active-defrag-cycle-max 75 # CPU 最高占用 75%
⚠️ 碎片整理是 CPU 密集型操作,在写入量大的时段应避免。建议通过
active-defrag-cycle-min/max限制 CPU 使用率,在低峰期加大整理力度。
替代方案——重启迁移:
对于碎片率极高的实例,重启是彻底清除碎片的有效手段:
## 1. 配置主从,让从节点追上
## 2. 在从节点上验证数据一致性
## 3. 主从切换(Sentinel 或手动 failover)
## 4. 重启原主节点(从 RDB 恢复,内存完全重新分配)
## 5. 验证碎片率降至 ~1.0-1.2
5. Pipeline 与事务优化
5.1 Pipeline 批量操作
Redis 单线程处理命令,网络往返(RTT)是吞吐量的主要瓶颈之一:
无 Pipeline(100 条命令,RTT = 1ms):
发送 → 等待 → 返回 × 100 = 100ms 总耗时
有 Pipeline(100 条命令一起发送):
发送(100条) → 服务端批量处理 → 返回(100条) = 1ms RTT + 处理时间 ≈ 2ms
吞吐量提升约 50 倍!
import redis
r = redis.Redis()
## ❌ 低效:每次往返
for key in keys:
r.get(key) # 100 次 RTT
## ✅ 高效:Pipeline 批量
pipe = r.pipeline()
for key in keys:
pipe.get(key)
results = pipe.execute() # 1 次 RTT
pipeline() 的注意事项:
- 服务端缓冲所有命令结果,不要一次发太多(建议 <= 1000 条)
- Pipeline 的命令失败不会导致后续命令回滚(非事务)
- 使用
transaction=True时,Pipeline 包裹在 MULTI/EXEC 中,但牺牲了性能
5.2 Lua 脚本原子性执行
当多个命令之间存在数据依赖时,Pipeline 无法保证原子性。Lua 脚本在 Redis 中以原子方式执行:
-- 原子性扣减库存(检查→扣减→返回新值)
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) > 0 then
local remain = redis.call('DECR', KEYS[1])
return remain
else
return -1 -- 库存不足
end
script = r.register_script("""
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) > 0 then
return redis.call('DECR', KEYS[1])
else
return -1
end
""")
result = script(keys=['stock:item:001'])
Lua 脚本的特点:
- 执行期间不会插入其他命令(原子性)
- 脚本执行时间应尽可能短(避免长时间阻塞)
- Redis 5.0 引入 Redis Functions(持久化的 Lua 函数),比临时脚本更推荐用于常驻业务逻辑
5.3 事务(MULTI/EXEC)的局限与替代
## Redis 事务:不支持回滚,只保证原子性打包执行
MULTI
SET balance:alice 100
SET balance:bob 200
EXEC
## 乐观锁:WATCH 监控键,被修改则 EXEC 失败
WATCH balance:alice
MULTI
DECRBY balance:alice 50
INCRBY balance:bob 50
EXEC # 如果 balance:alice 被其他客户端修改,EXEC 返回 nil(失败)
Redis 事务的局限:
- 不支持回滚:语法错误导致整个事务丢弃,运行时错误不排除已完成命令
- 无隔离级别:WATCH 提供的乐观锁适合冲突率低的场景
- 阻塞问题:EXEC 执行期间其他命令排队等待
对于需要事务特性的场景,建议优先使用 Lua 脚本或Redisson 等客户端库提供的分布式锁。
6. 主从复制优化
6.1 复制原理与延迟诊断
主从复制流程:
主节点 (Master) 从节点 (Replica)
┌──────────────┐ ┌──────────────┐
│ 写入命令 │──→ 复制缓冲区 ───→│ 接收并执行 │
│ │ │ │
│ offset 10000 │ │ offset 10000 │ ← 同步
│ offset 10001 │──→ CMD "SET" ──→ │ offset 10001 │
│ offset 10002 │──→ CMD "HSET" ─→ │ offset 10002 │
│ ... │ │ ... │
└──────────────┘ └──────────────┘
复制缓冲区 (Replication Backlog):
- 环形缓冲区,保存最近 N 字节(默认 1MB)的命令
- 从节点断线重连后,通过 PSYNC + offset 部分重同步
- 如果 offset 差值超过 backlog 大小,触发全量同步 (FULLRESYNC)
复制延迟诊断:
## 查看复制延迟(主从 offset 差值)
redis-cli INFO replication
## 输出关键指标:
## master_repl_offset: 9876543210 <- 主节点当前 offset
## slave_repl_offset: 9876543208 <- 从节点当前 offset
## 差值 = 2 字节(几乎无延迟)
## 实际延迟 = (主 offset - 从 offset) / 每秒写入量
降低复制延迟:
| 策略 | 配置 | 说明 |
|---|---|---|
| 增大复制缓冲区 | repl-backlog-size 64mb | 减少因缓冲区满导致的全量同步 |
| 禁用大键传播 | repl-disable-tcp-nodelay no | 合并小数据包减少网络开销 |
| 使用专线/同机房 | 网络拓扑 | 跨可用区延迟比同机房高 1-5ms |
| 限制慢查询 | slowlog-log-slower-than | 主节点执行慢查询会阻塞复制 |
6.2 Cluster 集群调优
Redis Cluster 使用 CRC16(key) % 16384 将键映射到 16384 个槽位(slot),每个节点负责一部分槽位。
Cluster 请求路由:
客户端 → 节点 A (slot 0-5460)
└─ 键 "user:1001" → CRC16("user:1001") % 16384 = 1234
└─ 1234 在 0-5460 范围内 → 由节点 A 直接处理
客户端 → 节点 A (slot 0-5460)
└─ 键 "order:2002" → CRC16("order:2002") % 16384 = 7890
└─ 7890 不在节点 A 的范围 → 返回 MOVED 重定向
└─ 客户端重新请求节点 B (slot 5461-10922)
智能客户端:大多数 Redis 客户端支持 MOVED/ASK 重定向和槽位缓存,首次连接时通过 CLUSTER SLOTS 获取槽位映射,后续直接路由到正确节点。
Cross-Slot 操作限制:
Redis Cluster 中,涉及多个 Key 的命令要求所有 Key 在同一个槽位:
## ❌ 失败:keys 落在不同 slot
MGET user:1001 user:1002
## ✅ 解决:使用 Hash Tag {user} 强制同 slot
## CRC16 只计算 {} 内的内容
MGET {user}:1001 {user}:1002
## 其他受限命令:
## MSET, MGET, SUNION, SINTER, SDIFF, ZUNIONSTORE, 事务, Lua 多 Key
Cluster 扩容(重分片):
## 1. 添加新节点到集群
redis-cli --cluster add-node new_host:6379 existing_host:6379
## 2. 将部分 slot 从旧节点迁移到新节点
redis-cli --cluster reshard existing_host:6379 \
--cluster-from old_node_id \
--cluster-to new_node_id \
--cluster-slots 1000 \
--cluster-yes
## 迁移过程:
## - 对于每个 key:MIGRATE 到目标节点(原子性移动)
## - 迁移期间该 key 短暂不可写(ASK 重定向)
## - 迁移完成后更新槽位映射
7. 基准测试
## Redis 自带基准测试工具
redis-benchmark -h localhost -p 6379 -n 100000 -c 50
## 参数说明
## -n: 总请求数
## -c: 并发连接数
## -d: 数据大小(字节)
## -t: 测试的命令集
## -P: Pipeline 批量数
## 测试 Pipeline 性能
redis-benchmark -n 100000 -c 50 -P 16 -t set,get
## 测试特定命令
redis-benchmark -n 10000 -t lpush,lpop
8. 生产监控指标体系
8.1 核心监控指标
| 指标 | 获取命令 | 告警阈值 | 说明 |
|---|---|---|---|
| used_memory | INFO memory | > maxmemory × 80% | 内存使用率 |
| mem_fragmentation_ratio | INFO memory | > 1.5 或 < 1.0 | 碎片率(RSS/used_memory) |
| connected_clients | INFO clients | > maxclients × 80% | 连接数 |
| rejected_connections | INFO stats | > 0 | 拒绝连接数(超过 maxclients) |
| keyspace_hits_ratio | INFO stats | < 90% | 命中率(命中/(命中+未命中)) |
| instantaneous_ops_per_sec | INFO stats | 突增/突降 | 实时 QPS |
| master_link_down_since_seconds | INFO replication | > 0 | 主从断连秒数 |
| rdb_last_bgsave_status | INFO persistence | != ok | RDB 快照状态 |
| aof_last_write_status | INFO persistence | != ok | AOF 写入状态 |
| blocked_clients | INFO clients | > 0 | 阻塞客户端数(BLPOP/BRPOP 等) |
| evicted_keys | INFO stats | 观察趋势 | 每秒淘汰键数 |
| expired_keys | INFO stats | 观察趋势 | 每秒过期键数 |
| pubsub_channels | INFO stats | > 1000 | 发布订阅通道数 |
| tracking_total_keys | INFO tracking | 观察趋势 | 客户端缓存追踪的键数 |
8.2 延迟百分位监控
平均延迟(avg latency)会掩盖长尾问题,生产环境应使用 P99/P99.9 延迟:
## Redis 6.2+ 内置延迟直方图
redis-cli LATENCY HISTOGRAM
## 输出示例(command 事件的延迟分布):
## command : 12345 ops/sec (p50=0.5ms p99=2.1ms p99.9=5.8ms)
## 自定义延迟追踪
redis-cli LATENCY TRACKER ADD "hgetall-big-hash" "HGETALL" 100
## 监控特定命令超过阈值的次数
redis-cli LATENCY HISTORY hgetall-big-hash
延迟异常排查矩阵:
| 延迟现象 | 根因 | 排查命令 | 解决措施 |
|---|---|---|---|
| 所有命令 P99 飙升 | CPU 饱和 / 系统态高 | top, mpstat -P ALL 1 | 升级 CPU 或拆分实例 |
| 偶发尖刺 | RDB Fork / AOF fsync | LATENCY GRAPH fork, LATENCY GRAPH aof-fsync-always | 调整持久化策略 |
| 写入延迟正常,读取延迟高 | 大 Key 读取 | redis-cli --bigkeys, SLOWLOG GET | 拆分大 Key |
| 连接建立慢 | 连接数过多/三次握手慢 | ss -s, tcpdump | 增大连接池预热 |
| Cluster 模式偶发高延迟 | MOVED 重定向风暴 | INFO commandstats | 检查客户端槽位缓存 |
8.3 持久化策略选择与权衡
Redis 提供 RDB(快照)和 AOF(追加日志)两种持久化方式,各有优劣:
RDB 持久化:
优点:
- 恢复速度快(直接加载二进制文件)
- 文件紧凑(经过压缩)
- 适合灾难恢复和备份
缺点:
- 两次快照之间可能丢失数据(默认 5 分钟一次)
- Fork 操作时内存瞬时翻倍
- 大数据集 Fork 耗时增加
AOF 持久化:
优点:
- 数据安全性高(每条命令追加)
- 可配置 fsync 策略(always/everysec/no)
- AOF 重写时不会丢失旧数据
缺点:
- 文件体积大(记录所有命令,尤其是重复 SET)
- 恢复速度慢(需要重放所有命令)
- 重写期间 Fork 同样存在内存问题
混合模式(Redis 4.0+,推荐):
- AOF 文件包含 RDB 前缀(全量数据)+ 增量命令
- 重写时生成 RDB 格式的基础数据 + 增量 AOF
- 恢复时先加载 RDB 部分,再重放增量 AOF
- 兼顾恢复速度和数据安全
## 推荐生产配置
save 900 1 # 15 分钟内 1 次变更触发 RDB
save 300 10 # 5 分钟内 10 次变更触发 RDB
save 60 10000 # 1 分钟内 10000 次变更触发 RDB
appendonly yes
appendfsync everysec # 每秒 fsync(性能和安全的折中)
aof-use-rdb-preamble yes # 开启混合模式(RDB + AOF)
auto-aof-rewrite-percentage 100 # AOF 增长 100% 时触发重写
auto-aof-rewrite-min-size 64mb # 最小重写大小
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。