好的,我们开始吧。
Redis慢查询与集群延迟抖动:命令黑名单与结构优化
大家好,今天我们来聊聊Redis慢查询以及它如何导致集群的延迟抖动,并重点讨论命令黑名单和结构优化这两种应对策略。
慢查询的根源与影响
Redis以其高性能著称,但随着数据量、并发量的增加,或代码设计不合理,慢查询问题便会浮出水面。慢查询是指执行时间超过预设阈值的命令。
慢查询的常见原因:
- 复杂度高的命令: 比如
KEYS *、SMEMBERS在大数据量集合上的操作,以及SORT未加LIMIT等。这些命令的时间复杂度较高,容易阻塞Redis进程。 - 网络延迟: 客户端与Redis服务器之间的网络延迟,尤其是在跨地域部署的情况下。
- 大 Value 操作: 获取或设置过大的 Value,导致序列化/反序列化耗时增加,网络传输时间变长。
- CPU 瓶颈: Redis单线程模型下,CPU占用率过高会导致所有命令的执行速度下降。
- 内存瓶颈: 内存不足会导致频繁的swap操作,严重影响性能。
- 持久化阻塞: RDB或AOF的同步操作可能会阻塞Redis主进程。
- 不合理的Lua脚本: 执行时间过长的Lua脚本会阻塞Redis。
慢查询的影响:
- 延迟增加: 最直接的影响是客户端请求的响应时间变长。
- 吞吐量下降: Redis服务器处理请求的能力降低。
- 集群延迟抖动: 在Redis集群中,单个节点的慢查询会影响整个集群的性能。因为客户端可能会将请求发送到慢查询节点,导致请求排队,甚至超时。
- 雪崩效应: 如果慢查询导致大量请求堆积,可能会引发系统崩溃,导致服务不可用。
慢查询日志分析
Redis 提供了慢查询日志功能,可以记录执行时间超过指定阈值的命令。 通过分析慢查询日志,我们可以找到导致性能问题的根源。
配置慢查询日志:
在 redis.conf 中设置以下参数:
slowlog-log-slower-than 1000 # 单位微秒,表示执行时间超过 1000 微秒的命令会被记录
slowlog-max-len 128 # 慢查询日志的最大长度,超过这个长度旧的日志会被覆盖
修改配置后,可以使用 CONFIG REWRITE 命令将配置写入配置文件。
查看慢查询日志:
使用 SLOWLOG GET [number] 命令查看慢查询日志,例如 SLOWLOG GET 10 查看最近 10 条慢查询日志。
慢查询日志示例:
1) 1) (integer) 1
2) (integer) 1678886400
3) (integer) 2000
4) 1) "KEYS"
2) "*"
5) "127.0.0.1:6379"
6) ""
2) 1) (integer) 2
2) (integer) 1678886300
3) (integer) 1500
4) 1) "SMEMBERS"
2) "my_set"
5) "127.0.0.1:6379"
6) ""
1):日志条目的序号。2):命令执行的 Unix 时间戳。3):命令执行的时长,单位为微秒。4):执行的命令及其参数。5):客户端的 IP 地址和端口号。6):客户端的名字 (如果设置了)。
利用 RedisInsight 分析慢查询日志
RedisInsight是Redis官方提供的可视化管理工具,它可以方便地分析慢查询日志,找出性能瓶颈。它可以按照执行时间、命令类型等维度对慢查询日志进行排序和过滤,帮助我们快速定位问题。
命令黑名单策略
找到了慢查询的根源之后,如果某些命令是业务不需要的,或者可以通过其他方式替代,我们可以考虑使用命令黑名单策略来禁用这些命令。
命令重命名:
Redis 提供了 RENAME 命令可以将危险命令重命名,从而防止误用。
CONFIG SET rename-command "KEYS" "" # 禁用 KEYS 命令
CONFIG SET rename-command "FLUSHALL" "" # 禁用 FLUSHALL 命令
CONFIG SET rename-command "FLUSHDB" "dangerous_flushdb" # 将 FLUSHDB 重命名为 dangerous_flushdb
使用 ACL 控制命令访问权限:
Redis 6.0 引入了 ACL (Access Control List) 功能,可以细粒度地控制用户对命令的访问权限。
-
创建用户:
ACL SETUSER myuser on >mypassword这会创建一个名为
myuser的用户,密码为mypassword。on表示启用用户。 -
设置命令权限:
ACL SETUSER myuser -@all +get +set +del ~prefix:*这条命令表示
myuser用户可以执行GET、SET、DEL命令,并且只能访问以prefix:开头的 key。-@all表示移除所有权限,+get +set +del表示添加GET、SET、DEL命令的权限。~prefix:*表示允许访问以prefix:开头的 key。 -
禁用危险命令:
ACL SETUSER myuser -flushall -flushdb这条命令禁止
myuser用户执行FLUSHALL和FLUSHDB命令。
Lua 脚本控制:
可以使用 Lua 脚本来控制命令的执行。 例如,可以编写一个 Lua 脚本来限制 KEYS 命令的匹配模式,或者限制 SMEMBERS 命令返回的元素数量。
示例 Lua 脚本:
-- 限制 KEYS 命令的匹配模式
local pattern = ARGV[1]
if string.find(pattern, "*") then
return redis.error_reply("KEYS command with wildcard pattern is not allowed")
end
return redis.call("KEYS", pattern)
在 Redis 中执行 Lua 脚本:
EVAL "local pattern = ARGV[1]nif string.find(pattern, "*") thenn return redis.error_reply("KEYS command with wildcard pattern is not allowed")nendnreturn redis.call("KEYS", pattern)" 0 "mykey"
这个脚本会检查 KEYS 命令的匹配模式是否包含 *,如果包含则返回错误,否则执行 KEYS 命令。
命令黑名单的局限性:
- 需要重启 Redis 服务器: 修改
redis.conf文件需要重启 Redis 服务器才能生效。而使用ACL只需要重新加载ACL规则。 - 可能会影响业务功能: 禁用某些命令可能会导致业务功能无法正常工作。
- 无法完全避免慢查询: 命令黑名单只能防止某些特定命令导致慢查询,但无法完全避免慢查询问题。
数据结构优化策略
除了命令黑名单之外,更重要的是优化数据结构,减少命令的复杂度。
1. 使用 Hash 替代 String 存储对象:
如果需要存储一个对象,不要将对象的每个属性都存储为一个 String 类型的 key,而是应该使用 Hash 类型。
反例:
SET user:1:name "John"
SET user:1:age "30"
SET user:1:email "[email protected]"
正例:
HSET user:1 name "John" age "30" email "[email protected]"
使用 Hash 类型可以减少 key 的数量,降低内存占用,并且可以更方便地获取对象的属性。
2. 使用 Set 存储集合数据:
如果需要存储一个集合数据,可以使用 Set 类型。 Set 类型提供了高效的添加、删除、判断元素是否存在等操作。
反例:
LPUSH user:1:friends "user:2"
LPUSH user:1:friends "user:3"
LPUSH user:1:friends "user:4"
正例:
SADD user:1:friends "user:2"
SADD user:1:friends "user:3"
SADD user:1:friends "user:4"
使用 Set 类型可以避免 List 类型中可能出现的重复元素问题,并且可以更高效地进行集合操作。
3. 使用 ZSet 存储有序集合数据:
如果需要存储一个有序集合数据,可以使用 ZSet 类型。 ZSet 类型可以根据 score 对元素进行排序,并且可以高效地进行范围查询。
反例:
使用 List + Sort 实现排序功能,效率较低。
正例:
ZADD leaderboard 100 "user:1"
ZADD leaderboard 90 "user:2"
ZADD leaderboard 80 "user:3"
使用 ZSet 类型可以方便地实现排行榜、时间序列等功能。
4. 合理使用 List:
List 适合存储具有先后顺序的数据,例如消息队列、任务队列等。 但是,List 的操作复杂度较高,尤其是 LINDEX、LINSERT 等命令。
- 避免使用
LINDEX命令: 如果需要根据索引访问 List 中的元素,可以考虑使用其他数据结构,例如 Hash。 - 避免使用
LINSERT命令: 在 List 中间插入元素的效率较低,可以考虑使用其他数据结构,或者先将 List 中的元素取出,插入新元素后再重新存储。 - 使用
LRANGE命令分页获取数据: 使用LRANGE命令可以高效地分页获取 List 中的数据。
5. 避免存储过大的 Value:
过大的 Value 会导致序列化/反序列化耗时增加,网络传输时间变长,并且容易导致内存碎片。
- 拆分 Value: 如果 Value 过大,可以考虑将其拆分成多个小的 Value,然后使用多个 key 存储。
- 压缩 Value: 可以使用压缩算法对 Value 进行压缩,减少存储空间和网络传输时间。
6. Key 的设计规范:
- Key 的长度: Key 的长度应该尽可能短,但也要具有可读性。
- Key 的命名: Key 的命名应该具有一定的规范,例如使用冒号分隔不同的字段。
- 避免使用过长的 Key 前缀: 过长的 Key 前缀会增加内存占用。
7. 批量操作:
使用 MGET、MSET 等批量操作命令可以减少网络开销,提高性能。
反例:
SET key1 value1
SET key2 value2
SET key3 value3
正例:
MSET key1 value1 key2 value2 key3 value3
8. Pipeline:
Pipeline 可以将多个命令打包发送到 Redis 服务器,减少网络开销。
示例代码 (Python):
import redis
r = redis.Redis(host='localhost', port=6379)
pipe = r.pipeline()
pipe.set('foo', 'bar')
pipe.get('foo')
result = pipe.execute()
print(result) # [True, b'bar']
9. 避免使用阻塞命令:
避免使用 BLPOP、BRPOP 等阻塞命令,这些命令会阻塞 Redis 进程,影响性能。 可以使用非阻塞命令 + 轮询的方式替代。
10. 合理使用过期时间:
为 Key 设置合理的过期时间可以避免内存占用过多。 可以使用 EXPIRE 命令设置 Key 的过期时间,或者使用 SETEX 命令设置 Key 的值和过期时间。
11. 内存优化:
- 使用
INFO memory命令查看内存使用情况: 可以了解 Redis 的内存使用情况,例如内存碎片率、used_memory 等。 - 调整
maxmemory参数: 设置 Redis 的最大内存使用量,防止 Redis 占用过多内存。 - 开启
lazyfree-lazy-eviction参数: 开启 lazyfree 功能可以异步释放内存,减少阻塞。 - 定期清理过期 Key: 定期清理过期 Key 可以释放内存。
结构优化总结表格
| 优化方向 | 具体策略 |
| 数据类型优化 | 使用 Hash 替代 String 存储对象,使用 Set 存储集合数据,使用 ZSet 存储有序集合数据,合理使用 List。