跳到主要内容

Redis 性能优化

问题

Redis 性能优化有哪些方向?如何排查 Redis 慢查询?Pipeline 和 Lua 脚本如何提升性能?

答案

性能优化全景

避免慢命令

慢命令时间复杂度替代方案
KEYS *O(N)SCAN 游标遍历
HGETALL (大Hash)O(N)HSCANHMGET 指定字段
SMEMBERS (大Set)O(N)SSCAN 游标遍历
LRANGE 0 -1 (大List)O(N)分页取 LRANGE 0 99
DEL (大Key)O(N)UNLINK 异步删除
SORTO(N+M*log(M))业务层排序
生产环境禁用 KEYS 命令

KEYS * 会遍历所有 key,在数据量大时导致 Redis 阻塞。应使用 SCAN 替代,它基于游标增量迭代,不会阻塞服务。

# SCAN 遍历所有 key(每次返回一批 + 下一游标)
SCAN 0 MATCH user:* COUNT 100

Pipeline 批量执行

默认情况下,每条 Redis 命令都是一次网络往返(RTT)。Pipeline 将多条命令打包发送,减少网络开销。

Pipeline vs 逐条执行
// ❌ 逐条执行:100 次 RTT
for (int i = 0; i < 100; i++) {
jedis.set("key:" + i, "value:" + i);
}

// ✅ Pipeline:1 次 RTT
Pipeline pipe = jedis.pipelined();
for (int i = 0; i < 100; i++) {
pipe.set("key:" + i, "value:" + i);
}
pipe.sync(); // 一次性发送并接收所有响应
Pipeline 注意事项
  • Pipeline 不是原子操作,中间命令可能被其他客户端命令穿插
  • 命令数量不宜过多(通常 500-1000 条为一批),避免占用过多内存和服务端缓冲区
  • 需要原子性时使用 Lua 脚本或事务(MULTI/EXEC)

Lua 脚本

Lua 脚本在 Redis 服务端原子执行,适合需要多步操作保证原子性的场景。

Lua 脚本实现限流(固定窗口)
String script = """
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = tonumber(redis.call('GET', key) or '0')
if current < limit then
redis.call('INCR', key)
if current == 0 then
redis.call('EXPIRE', key, window)
end
return 1
end
return 0
""";

// EVALSHA 使用脚本的 SHA1 缓存,避免每次传输脚本内容
String sha = jedis.scriptLoad(script);
Long result = (Long) jedis.evalsha(sha, 1, "rate:user:1001", "100", "60");
Lua 脚本注意事项
  • Lua 执行期间 Redis 不处理其他命令(单线程),脚本应尽量简短
  • 避免在 Lua 中执行耗时操作(如大循环)
  • 脚本超时可通过 lua-time-limit 配置上限(默认 5 秒)

慢查询排查

慢查询配置与查看
# 调优配置(redis.conf 或 CONFIG SET)
CONFIG SET slowlog-log-slower-than 10000 # 超 10ms 记录(微秒)
CONFIG SET slowlog-max-len 128 # 最多保留 128 条

# 查看慢查询
SLOWLOG GET 10 # 最近 10 条慢查询
SLOWLOG LEN # 当前慢查询条数
SLOWLOG RESET # 清空慢查询日志

慢查询记录包含:ID、时间戳、执行耗时(微秒)、命令及参数。

大 Key 治理

大 Key 会导致:内存不均、网络拥塞、操作阻塞、主从同步延迟。

大 Key 扫描
# 使用 redis-cli 扫描各类型最大 key
redis-cli --bigkeys

# 也可以用 MEMORY USAGE 查看单个 key 占用
MEMORY USAGE mykey
类型大 Key 标准解决方案
String> 10KB压缩(gzip)、拆分
Hash> 5000 field分桶:hash:{key}:{N}
List> 5000 元素分段或改用 Stream
Set/ZSet> 5000 成员分片存储
Hash 分桶示例
// 将一个大 Hash 按 field 哈希分桶
public void hset(String key, String field, String value) {
int bucket = Math.abs(field.hashCode()) % 100;
redis.hset(key + ":" + bucket, field, value);
}

连接池优化

JedisPool 配置
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200); // 最大连接数
config.setMaxIdle(50); // 最大空闲连接
config.setMinIdle(10); // 最小空闲连接
config.setMaxWaitMillis(3000); // 获取连接最大等待时间
config.setTestOnBorrow(true); // 借用时检测连接有效性
config.setTestWhileIdle(true); // 空闲时检测

Redis 6.0 多线程模型

多线程 IO 说明

Redis 6.0 的多线程仅用于 网络 IO 的读写(socket 读取和响应写入),命令执行仍然是单线程。这样既利用了多核 CPU 加速网络处理,又保持了命令执行的线程安全。

# 开启多线程 IO(默认关闭)
io-threads 4 # IO 线程数(建议 CPU 核数 / 2)
io-threads-do-reads yes # 读也使用多线程

持久化对性能的影响

配置性能影响建议
save (RDB触发)BGSAVE 时 fork 导致短暂卡顿合理设置触发条件,避免频繁 fork
appendfsync always每次写入都 fsync,最慢仅数据零丢失场景使用
appendfsync everysec每秒 fsync,性能与安全平衡推荐
aof-rewrite-min-sizeAOF 重写触发阈值合理设置,避免频繁重写

常见面试问题

Q1: Redis 为什么快?

答案

  1. 纯内存操作:数据存储在内存中,读写速度达微秒级
  2. 单线程命令执行:避免上下文切换和锁竞争开销
  3. IO 多路复用:epoll/kqueue 模型,单线程高效处理数万连接
  4. 高效数据结构:SDS、跳表、压缩列表、quicklist 等针对性优化
  5. 6.0 多线程 IO:网络读写多线程,命令执行仍单线程
  6. C 语言实现:接近底层的执行效率

Q2: Pipeline 和 Lua 脚本的区别是什么?

答案

对比PipelineLua 脚本
原子性非原子,命令可被穿插原子执行,不被打断
网络开销一次 RTT 发送多条命令一次 RTT 发送脚本
服务端计算支持条件判断和循环
适用场景批量独立操作需要原子性或依赖中间结果

Q3: KEYS 命令为什么不能在生产环境使用?

答案

KEYS pattern 时间复杂度 O(N),会遍历整个 keyspace。在数据量大时(百万级 key),执行时间可达数秒甚至数十秒,期间 Redis 无法处理其他请求(单线程阻塞)。

替代方案:SCAN 0 MATCH pattern COUNT 100,基于游标增量遍历,每次只返回一小批结果,不会阻塞。

Q4: 如何排查 Redis 性能问题?

答案

# 1. 查看慢查询
SLOWLOG GET 20

# 2. 查看实时统计
INFO stats # 命令统计、命中率
INFO memory # 内存使用
INFO clients # 连接数

# 3. 监控延迟
redis-cli --latency # 实时延迟检测
redis-cli --latency-history # 延迟历史

# 4. 大 Key 扫描
redis-cli --bigkeys

# 5. 实时命令监控(调试用,生产慎用)
MONITOR

排查顺序:慢查询 → 大 Key → 内存/连接数 → 持久化配置 → 网络延迟。

相关链接