Redis 性能优化
问题
Redis 性能优化有哪些方向?如何排查 Redis 慢查询?Pipeline 和 Lua 脚本如何提升性能?
答案
性能优化全景
避免慢命令
| 慢命令 | 时间复杂度 | 替代方案 |
|---|---|---|
KEYS * | O(N) | SCAN 游标遍历 |
HGETALL (大Hash) | O(N) | HSCAN 或 HMGET 指定字段 |
SMEMBERS (大Set) | O(N) | SSCAN 游标遍历 |
LRANGE 0 -1 (大List) | O(N) | 分页取 LRANGE 0 99 |
DEL (大Key) | O(N) | UNLINK 异步删除 |
SORT | O(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-size | AOF 重写触发阈值 | 合理设置,避免频繁重写 |
常见面试问题
Q1: Redis 为什么快?
答案:
- 纯内存操作:数据存储在内存中,读写速度达微秒级
- 单线程命令执行:避免上下文切换和锁竞争开销
- IO 多路复用:epoll/kqueue 模型,单线程高效处理数万连接
- 高效数据结构:SDS、跳表、压缩列表、quicklist 等针对性优化
- 6.0 多线程 IO:网络读写多线程,命令执行仍单线程
- C 语言实现:接近底层的执行效率
Q2: Pipeline 和 Lua 脚本的区别是什么?
答案:
| 对比 | Pipeline | Lua 脚本 |
|---|---|---|
| 原子性 | 非原子,命令可被穿插 | 原子执行,不被打断 |
| 网络开销 | 一次 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 → 内存/连接数 → 持久化配置 → 网络延迟。