Redis 性能调优与生产监控

深入 Redis 性能调优:内存优化、大 Key 治理、慢查询诊断、延迟分析、基准测试与生产监控指标体系建设。

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^63int (16B)embstr / raw
String短字符串且 ≤ 44Bembstr (一次分配)raw
List元素数 ≤ 512 且元素 ≤ 64Blistpack (紧凑列表)quicklist
Set整数元素且数量 ≤ 512intset (有序整数数组)hashtable
Hash字段数 ≤ 512 且字段/值 ≤ 64Blistpack (紧凑哈希)hashtable
ZSet元素数 ≤ 128 且成员 ≤ 64Blistpack (紧凑有序集)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 个字段 或 总大小 > 10MBHGETALL/HLEN 需要遍历全部元素
List> 10000 个元素 或 总大小 > 10MBLRANGE 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_memoryINFO memory> maxmemory × 80%内存使用率
mem_fragmentation_ratioINFO memory> 1.5 或 < 1.0碎片率(RSS/used_memory)
connected_clientsINFO clients> maxclients × 80%连接数
rejected_connectionsINFO stats> 0拒绝连接数(超过 maxclients)
keyspace_hits_ratioINFO stats< 90%命中率(命中/(命中+未命中))
instantaneous_ops_per_secINFO stats突增/突降实时 QPS
master_link_down_since_secondsINFO replication> 0主从断连秒数
rdb_last_bgsave_statusINFO persistence!= okRDB 快照状态
aof_last_write_statusINFO persistence!= okAOF 写入状态
blocked_clientsINFO clients> 0阻塞客户端数(BLPOP/BRPOP 等)
evicted_keysINFO stats观察趋势每秒淘汰键数
expired_keysINFO stats观察趋势每秒过期键数
pubsub_channelsINFO stats> 1000发布订阅通道数
tracking_total_keysINFO 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 fsyncLATENCY 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   # 最小重写大小

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「redis」更多文章

  1. 多租户隔离与资源配额:共享 Redis 的边界设计
  2. Key 设计与命名规范:Redis 里唯一的结构约束
  3. 代理与路由方案:客户端直连之外的另一种选择