Redis持久化、内存管理、慢查询与发布订阅 -- pd的后端笔记
文章目录
-
- [Redis持久化、内存管理、慢查询与发布订阅 -- pd的后端笔记](#Redis持久化、内存管理、慢查询与发布订阅 -- pd的后端笔记)
- [持久化机制(RDB vs AOF)------ 如何防止 Redis 数据丢失?](#持久化机制(RDB vs AOF)—— 如何防止 Redis 数据丢失?)
-
- RDB:定时快照(全量备份)
- AOF:操作日志(增量备份)
- 配置设置
- [🎯 总结](#🎯 总结)
- [内存管理与淘汰策略(Eviction Policy) ------ Redis 满了怎么办?](#内存管理与淘汰策略(Eviction Policy) —— Redis 满了怎么办?)
-
- [当内存达到上限:淘汰策略(Eviction Policy)](#当内存达到上限:淘汰策略(Eviction Policy))
-
- [🗑️ 类型 1:针对"设置了过期时间"的 key(volatile)](#🗑️ 类型 1:针对“设置了过期时间”的 key(volatile))
- [🗑️ 类型 2:针对"所有 key"(allkeys)](#🗑️ 类型 2:针对“所有 key”(allkeys))
- [🚫 类型 3:不淘汰(危险!)](#🚫 类型 3:不淘汰(危险!))
- [🛠️ 生产环境推荐配置](#🛠️ 生产环境推荐配置)
- [Redis 事务(MULTI/EXEC)------ 原子执行一组命令](#Redis 事务(MULTI/EXEC)—— 原子执行一组命令)
-
- [🌰 实战:安全转账(模拟)](#🌰 实战:安全转账(模拟))
- [🔍 WATCH:实现乐观锁(Optimistic Locking)](#🔍 WATCH:实现乐观锁(Optimistic Locking))
- [🆚 事务 vs Lua 脚本 vs Pipeline](#🆚 事务 vs Lua 脚本 vs Pipeline)
- [📖 Redis 发布/订阅 ------ 轻量级消息广播系统](#📖 Redis 发布/订阅 —— 轻量级消息广播系统)
-
-
- [🌰 场景 1:实时通知(如订单状态更新)](#🌰 场景 1:实时通知(如订单状态更新))
- [🌰 场景 2:多服务解耦(WebSocket 集群广播)](#🌰 场景 2:多服务解耦(WebSocket 集群广播))
- [🎯 总结](#🎯 总结)
-
- [📖 Redis 慢查询 ------ 找出拖慢系统的"罪魁祸首"](#📖 Redis 慢查询 —— 找出拖慢系统的“罪魁祸首”)
-
- [🌰 常见"慢命令"黑名单(务必警惕!)](#🌰 常见“慢命令”黑名单(务必警惕!))
持久化机制(RDB vs AOF)------ 如何防止 Redis 数据丢失?
Redis 是内存数据库,默认情况下:
- 所有数据都在 RAM 中
- 如果 Redis 进程崩溃或服务器断电 → 所有数据丢失!
持久化(Persistence) 就是把内存中的数据保存到磁盘,以便重启后恢复。
Redis 提供两种主流持久化方式:
- RDB(Redis Database):快照(Snapshot)
- AOF(Append-Only File):日志(Log)
也可以同时开启两者(推荐用于关键业务)。
RDB:定时快照(全量备份)
✅ 原理
- 在指定时间点,将整个内存数据集写入一个二进制文件(默认 dump.rdb)
- 类似"拍照":某一刻的完整状态
⚙️ 触发方式
| 方式 | 说明 |
|---|---|
| 自动触发 | 通过 save 配置(如 save 900 1 表示 900 秒内至少 1 次修改就保存) |
| 手动触发 | SAVE(阻塞主线程)或 BGSAVE(后台 fork 子进程,推荐) |
| 主从同步 | 从节点全量同步时,主节点会自动生成 RDB |
📄 默认配置(redis.conf)
conf
save 900 1 # 15分钟内至少1次修改
save 300 10 # 5分钟内至少10次修改
save 60 10000 # 1分钟内至少10000次修改
✅ 优点
- 文件紧凑:单个二进制文件,适合备份、灾难恢复
- 恢复速度快:直接加载 RDB 文件到内存
- 性能影响小:BGSAVE 在子进程中执行,主进程继续处理请求
❌ 缺点
- 可能丢数据:如果 Redis 在两次快照之间崩溃,最近一次快照之后的数据全部丢失
- 例如:配置 save 60 10000,但只写了 5000 次就宕机 → 全丢!
- 大数据集 fork 耗时:如果内存很大(几十 GB),fork() 子进程可能阻塞主线程几百毫秒
AOF:操作日志(增量备份)
✅ 原理
- 记录每一个写命令到日志文件(如 appendonly.aof)
- 重启时,重放(replay)所有命令来重建数据集
- 类似"录像":记录每一步操作
⚙️ 同步策略(fsync)
| 配置 | 说明 | 安全性 | 性能 |
|---|---|---|---|
appendfsync always |
每个写命令都同步到磁盘 | ✅ 最安全(最多丢 1 条) | ❌ 最慢(受磁盘 I/O 限制) |
appendfsync everysec |
每秒同步一次(默认) | ⚠️ 最多丢 1 秒数据 | ✅ 平衡 |
appendfsync no |
由操作系统决定何时刷盘 | ❌ 可能丢几秒~几分钟 | ✅ 最快 |
🛠️ AOF 重写(Rewrite)
- 问题:AOF 文件会不断增长(即使 SET key 1 执行 100 次,只需最后 1 次)
- 解决:BGREWRITEAOF 命令生成精简版 AOF(只保留最终状态)
- 自动触发:可通过 auto-aof-rewrite-percentage 配置
✅ 优点
- 数据更安全:配合 everysec,最多丢 1 秒数据
- 可读性强:AOF 是文本格式,可用 redis-check-aof 修复
❌ 缺点
- 文件大:比 RDB 大很多(尤其高频写入)
- 恢复慢:需重放所有命令(数据量大时启动慢)
- I/O 压力:频繁 fsync 可能成为瓶颈
🔍 RDB vs AOF 对比总结
| 特性 | RDB | AOF |
|---|---|---|
| 数据完整性 | 可能丢多次快照间的数据 | 最多丢 1 秒(everysec) |
| 文件大小 | 小(二进制压缩) | 大(命令日志) |
| 恢复速度 | 快 | 慢(需重放) |
| 备份便利性 | ✅ 单文件,易拷贝 | ❌ 文件大,需重写 |
| 对性能影响 | 低(BGSAVE) |
中(everysec 可接受) |
| 适用场景 | 灾备、冷数据、分析 | 实时性要求高的业务 |
配置设置
🛠️ 生产环境推荐配置
✅ 场景 1:既要速度又要安全(推荐大多数业务)
conf
# 同时开启 RDB + AOF
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
# AOF 重写
; 当 AOF 文件比上次重写后的大小增长了 100%(即翻倍),并且当前 AOF 文件至少有 64MB 时,自动触发 AOF 重写。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
💡 重启时,Redis 优先使用 AOF 恢复(因为更完整)!
✅ 场景 2:纯缓存,允许丢数据: 适合临时 session、排行榜等可重建数据。
conf
save "" # 禁用 RDB
appendonly no # 禁用 AOF
✅ 场景 3:极致性能,容忍少量丢失
conf
save 60 10000
appendonly yes
appendfsync everysec
🔧 Python 中如何验证持久化是否生效?
python
r = redis.Redis()
print(r.config_get("save")) # 查看 RDB 规则
print(r.config_get("appendonly")) # 查看 AOF 是否开启
# 手动触发保存:
r.bgsave() # 触发 RDB
r.bgrewriteaof() # 触发 AOF 重写
模拟重启:
- 停止 Redis:
redis-cli shutdown - 启动 Redis:
redis-server
⚠️ 注意:SHUTDOWN 命令会强制保存(即使没到 RDB 条件),所以测试时要用 kill -9 模拟崩溃。
🎯 总结
- RDB = 快照,适合备份、恢复快
- AOF = 日志,适合数据安全、可读性强
- 生产环境建议两者都开,AOF 为主,RDB 为辅
- 没有 100% 不丢数据的方案,但合理配置可将风险降到极低
内存管理与淘汰策略(Eviction Policy) ------ Redis 满了怎么办?
🧠 为什么需要内存限制?
Redis 默认不限制内存使用------它会一直吃内存,直到:
- 系统内存耗尽 → 触发 Linux OOM Killer → Redis 进程被强制杀死!
- 或者系统开始大量 swap(交换到磁盘)→ 性能暴跌 100 倍!
所以,生产环境必须设置 maxmemory!
一、设置最大内存:maxmemory
在 redis.conf 中配置:
conf
maxmemory 2gb
或者运行时动态设置:
shell
CONFIG SET maxmemory 2147483648 # 2GB in bytes
# 1gb = 1,073,741,824 字节
当内存达到上限:淘汰策略(Eviction Policy)
光设 maxmemory 不够!你还得告诉 Redis:
- "内存满了,该删哪些 key?"
这就是 maxmemory-policy 的作用。
Redis 提供 8 种策略,分为三类:
🗑️ 类型 1:针对"设置了过期时间"的 key(volatile)
| 策略 | 行为 | 适用场景 |
|---|---|---|
volatile-lru |
从有 TTL 的 key中,淘汰最近最少使用的 | ✅ 缓存场景(如 session、临时 token) |
volatile-lfu |
从有 TTL 的 key中,淘汰最不经常使用的 | 高频访问 + 低频垃圾共存 |
volatile-random |
从有 TTL 的 key中,随机淘汰 | 简单场景,不关心访问模式 |
volatile-ttl |
从有 TTL 的 key中,淘汰剩余时间最短的 | "快过期的先删" |
🗑️ 类型 2:针对"所有 key"(allkeys)
| 策略 | 行为 | 适用场景 |
|---|---|---|
allkeys-lru |
从所有 key中,淘汰最近最少使用的 | ✅ 最常用!通用缓存(如页面缓存) |
allkeys-lfu |
从所有 key中,淘汰最不经常使用的 | 访问模式稳定(如热门商品缓存) |
allkeys-random |
从所有 key中,随机淘汰 | 测试或完全无状态缓存 |
✅ 优势:不管有没有 TTL,都能淘汰,不会写满报错。
🚫 类型 3:不淘汰(危险!)
| 策略 | 行为 | 风险 |
|---|---|---|
noeviction |
拒绝所有写入命令(返回 error),只读操作允许 | ❌ 默认策略!内存满后服务不可写 |
⚠️ Redis 默认策略是 noeviction!
这意味着:如果你不配置,内存一满,所有 SET/INCR/HSET 等写操作都会失败!
🔍 LRU vs LFU:到底选哪个?
| 算法 | 全称 | 特点 | 适合场景 |
|---|---|---|---|
| LRU | Least Recently Used | "最近没用的删" | 访问模式随时间变化(如新闻热点) |
| LFU | Least Frequently Used | "总用得少的删" | 访问模式稳定(如首页 banner) |
🛠️ 生产环境推荐配置
✅ 场景 1:纯缓存(如 API 响应缓存)
conf
maxmemory 4gb
maxmemory-policy allkeys-lru
✅ 场景 2:混合存储(部分 key 永久,部分带 TTL)
conf
maxmemory 4gb
maxmemory-policy volatile-lru
✅ 场景 3:严格禁止丢数据(如计数器、排行榜)
maxmemory 16gb
maxmemory-policy noeviction
🔧 Python 中如何查看和设置?
python
import redis
r = redis.Redis()
# 查看当前内存和策略
info = r.info("memory")
print("Used memory:", info["used_memory_human"])
print("Max memory:", info["maxmemory_human"])
print("Policy:", info["maxmemory_policy"])
# 动态设置(谨慎!)
r.config_set("maxmemory", "2147483648") # 2GB
r.config_set("maxmemory-policy", "allkeys-lru")
⚠️ 注意:动态设置 maxmemory 后,如果当前内存已超限,Redis 会立即触发一次淘汰!
| 误区 | 正确做法 |
|---|---|
| "不设 maxmemory,让系统管" | ❌ 必须设!否则 OOM 崩溃 |
| "用了 AOF 就不怕丢数据,随便淘汰" | ❌ 淘汰 = 永久删除,AOF 也救不回 |
| "LFU 一定比 LRU 好" | ❌ 热点突变场景 LRU 更优(如突发新闻) |
| "设置了 policy 就万事大吉" | ❌ 必须配合内存监控(如 Prometheus + Grafana) |
✅ 最佳实践清单:
- 永远设置 maxmemory(建议物理内存的 50%~80%)
- 根据业务选策略:缓存用 allkeys-lru,临时数据用 volatile-lru
- 监控内存使用率:超过 80% 就告警
- 避免大 Key:单个 key 几百 MB 会导致淘汰时卡顿
- 定期分析热点:用 redis-cli --hotkeys(需开启 maxmemory-samples)
Redis 事务(MULTI/EXEC)------ 原子执行一组命令
🧠 Redis 事务是什么?
Redis 的事务通过 MULTI / EXEC 实现:
- 将多个命令打包,然后一次性、按顺序、不被其他客户端打断地执行
- 但它 ≠ ACID 事务!没有回滚(Rollback)机制
🔑 核心特点:
- ✅ 原子性(Atomicity):事务中的命令要么全部执行,要么一个都不执行(在 EXEC 阶段)
- ❌ 不支持回滚:如果某个命令出错,之前的命令不会撤销(这是设计选择,非 bug!)
- ✅ 隔离性(Isolation):事务执行期间,其他客户端看不到中间状态
- ❌ 不保证持久性:除非配合 AOF + appendfsync always
类似于mysql的 start transaction; 写入、删除、更新逻辑 commit;
python
# 方式 1:使用 pipeline(推荐!)
pipe = r.pipeline()
pipe.incr("user:1001:posts")
pipe.incr("stats:total_posts")
results = pipe.execute() # 触发 MULTI/EXEC
print(results) # [1, 1]
# 方式 2:显式事务(较少用)
with r.pipeline() as pipe:
pipe.multi() # 显式开启事务(其实默认就是)
pipe.incr("a")
pipe.incr("b")
res = pipe.execute()
⚠️ 关键限制:Redis 事务不支持回滚!
场景:命令语法错误 vs 运行时错误
❌ 情况 1:语法错误(在 EXEC 前检测到)
bash
MULTI
INCR user:1001:posts
INCRBY stats:total_posts abc # ← 错误!abc 不是数字
EXEC
→ 整个事务被拒绝,两个命令都不执行。✅ 安全!
⚠️ 情况 2:运行时错误(EXEC 后才暴露)
bash
MULTI
INCR user:1001:posts # 正常
HSET user:1001 name Alice # 但 user:1001 是 String,不是 Hash!
EXEC
→ 结果:
- NCR 成功执行(值 +1)
- SET 失败(返回 error)
- 但 INCR 不会回滚!
📌 Redis 的哲学:
"错误通常是程序 bug,应该在开发阶段发现。生产环境不应依赖回滚。"
------ 所以它选择简单高效,而非复杂回滚。
💡 最佳实践:
- 简单原子操作 → 用 事务(pipeline)
- 复杂逻辑(带条件判断、回滚需求)→ 用 Lua 脚本
🌰 实战:安全转账(模拟)
假设我们要从用户 A 转 100 元给用户 B:
python
def transfer_money(pipe, from_user, to_user, amount):
# 注意:这里不能直接用 r.get(),必须在 pipe 中读!
balance_a = int(pipe.get(f"balance:{from_user}") or 0)
if balance_a < amount:
raise ValueError("余额不足")
pipe.multi() # 确保后续命令在事务中
pipe.decr(f"balance:{from_user}", amount)
pipe.incr(f"balance:{to_user}", amount)
# 使用 watch 监控 key(防并发修改)
def safe_transfer(from_user, to_user, amount):
with r.pipeline() as pipe:
while True:
try:
# 监听双方余额,防止中间被修改
pipe.watch(f"balance:{from_user}", f"balance:{to_user}")
transfer_money(pipe, from_user, to_user, amount)
break # 成功则退出
except redis.WatchError:
continue # 重试
pipe.execute() 是在 safe_transfer() 函数的上下文(即 with r.pipeline() as pipe: 块结束时)自动调用的!
🔍 WATCH:实现乐观锁(Optimistic Locking)
- WATCH key:监控 key,如果在 EXEC 前被其他客户端修改,则事务失败(返回 None)
- 适用于:先读 → 判断 → 写 的场景(如上面的转账)
python
pipe = r.pipeline()
pipe.watch("stock:iphone")
current = int(pipe.get("stock:iphone"))
if current > 0:
pipe.multi()
pipe.decr("stock:iphone")
result = pipe.execute()
if result is None:
print("库存被别人抢了!")
else:
print("购买成功!")
else:
print("库存不足")
✅ 这比"先查后改"更安全,但仍不如 Lua 脚本原子(因为 read 和 multi 之间有 gap)。
🆚 事务 vs Lua 脚本 vs Pipeline
| 特性 | 事务(MULTI/EXEC) | Lua 脚本 | Pipeline(非事务) |
|---|---|---|---|
| 原子性 | ✅(命令序列) | ✅(整个脚本) | ❌ |
| 支持条件逻辑 | ❌ | ✅(if/for) | ❌ |
| 支持回滚 | ❌ | ❌(但可自己写逻辑) | ❌ |
| 性能 | 高 | 极高(服务端执行) | 最高(仅批处理) |
| 适用场景 | 简单原子操作 | 复杂业务逻辑 | 批量写入/读取 |
💡 黄金法则:
- 能用 单个命令 解决?→ 优先用(如 HINCRBY)
- 需要 条件判断 + 原子性?→ 用 Lua 脚本
- 只是 批量操作?→ 用 Pipeline
📖 Redis 发布/订阅 ------ 轻量级消息广播系统
Redis 的 发布/订阅(Pub/Sub) 是一个轻量级、高性能的消息通信机制,非常适合用于实时通知、事件广播、解耦系统模块等场景。
虽然它不像 RabbitMQ 或 Kafka 那样功能全面(比如不支持持久化、ACK、重试),但在"简单、快、够用"的场景下,它是极佳的选择!
🧠 核心概念:发布者 vs 订阅者
- 发布者(Publisher):往某个 频道(channel) 发送消息
- 订阅者(Subscriber):订阅一个或多个频道,接收消息
- 消息只发给"当前在线"的订阅者 → 不持久化!离线就丢!
🔍 类比:
就像微信群------你发一条消息,所有群成员(订阅者)都能看到。
但如果某人退出了群(断开连接),他再进来就看不到历史消息了。
| 模式 | 命令 | 说明 |
|---|---|---|
| 频道订阅 | SUBSCRIBE channel1 |
精确匹配频道名 |
| 模式订阅 | PSUBSCRIBE news.* |
通配符匹配(*、?) |
🛠️ 核心命令速览(CLI vs Python)
| 功能 | Redis CLI | Python (redis-py) |
|---|---|---|
| 订阅频道 | SUBSCRIBE news |
r.pubsub().subscribe("news") |
| 订阅模式 | PSUBSCRIBE news.* |
r.pubsub().psubscribe("news.*") |
| 发布消息 | PUBLISH news "hello" |
r.publish("news", "hello") |
| 查看订阅数 | PUBSUB NUMSUB news |
r.pubsub_numsub("news") |
🌰 场景 1:实时通知(如订单状态更新)
发布者(订单服务)
py
# 订单支付成功
order_id = "order_123"
r.publish(f"order:{order_id}:status", "paid")
订阅者(通知服务)
py
pubsub = r.pubsub()
# 正确做法:用模式订阅
pubsub.psubscribe("order:*:status")
for message in pubsub.listen():
if message["type"] == "pmessage": # 模式订阅的消息类型是 pmessage
channel = message["channel"].decode()
data = message["data"].decode()
print(f"收到通知: {channel} → {data}")
# 输出: 收到通知: order:order_123:status → paid
注意for message in pubsub.listen():是一个同步阻塞(synchronous blocking)的
它会阻塞当前线程,直到有新消息到达。pubsub.listen() 的本质
- 它是一个 Python 生成器(generator)
- 内部调用
socket.recv()等待 Redis 服务器推送消息 - 没有消息时,线程会卡在
recv()上 → 同步 I/O 阻塞
可以用 threading 把 listen() 放到后台线程:
python
# 启动后台线程
thread = threading.Thread(target=listen_to_pubsub, daemon=True)
thread.start()
# 主线程继续做其他事
while True:
print("主线程在工作...")
time.sleep(2)
⚠️ 重要区别:
- subscribe() → 消息类型是 "message"
- psubscribe() → 消息类型是 "pmessage"
🌰 场景 2:多服务解耦(WebSocket 集群广播)
在 WebSocket 集群中,每个节点只维护部分连接。要广播消息给所有用户,可以用 Redis Pub/Sub 中转:
python
# 任意节点收到消息
def on_websocket_message(user_id, msg):
# 先处理本地逻辑
process_locally(user_id, msg)
# 再广播给其他节点
r.publish("ws:broadcast", json.dumps({"from": user_id, "msg": msg}))
# 所有节点都订阅这个频道
pubsub = r.pubsub()
pubsub.subscribe("ws:broadcast")
for msg in pubsub.listen():
if msg["type"] == "message":
payload = json.loads(msg["data"])
# 把消息推送给本机的所有 WebSocket 客户端
broadcast_to_local_clients(payload)
✅ 这就是很多开源项目(如 Django Channels)的底层实现原理!
🔍 底层原理:字典 + 链表
Redis 服务器内部维护两个关键结构:
- pubsub_channels:字典
- key = 频道名(如 "news")
- value = 订阅该频道的客户端列表(链表)
- pubsub_patterns:列表
- 存储所有模式订阅(如 "news.*")
- 每个元素包含 pattern + 客户端
当执行 PUBLISH news "hello":
- 查 pubsub_channels["news"] → 获取所有精确订阅者
- 遍历 pubsub_patterns → 匹配 "news" 是否符合某个 pattern
- 把消息推送给所有匹配的客户端
📌 所以:模式订阅比频道订阅稍慢(需要遍历匹配)。
| 问题 | 说明 |
|---|---|
| 消息不持久化 | 订阅者离线期间的消息全部丢失 |
| 无 ACK 机制 | 无法确认消息是否被成功处理 |
| 无消息堆积 | 如果订阅者处理慢,Redis 会缓冲消息,但可能 OOM |
| 不支持历史回溯 | 不能像 Kafka 那样"从头消费" |
| 连接必须保持 | 一旦 TCP 断开,自动取消订阅 |
✅ 最佳实践
- 命名规范:用冒号分隔,如 event:user:login、cache:invalidate:product
- 避免长连接中断:在客户端实现自动重连 + 重新订阅
- 监控订阅者数量:用 PUBSUB NUMSUB channel 防止"假广播"(没人订阅)
- 不要传大消息:单条消息建议 < 1MB,避免阻塞 Redis
🎯 总结
| 特性 | Pub/Sub |
|---|---|
| 消息模型 | 广播(一对多) |
| 持久化 | ❌ 不支持 |
| 可靠性 | ❌ 最多一次(at-most-once) |
| 性能 | ✅ 极高(纯内存,无磁盘 IO) |
| 适用场景 | 实时通知、事件驱动、解耦 |
💡 记住:Pub/Sub 是"火警广播"------响一声就没了,听到算你幸运,没听到也不补。
📖 Redis 慢查询 ------ 找出拖慢系统的"罪魁祸首"
很多线上 Redis 性能问题,其实都是因为一条 KEYS * 或一个巨大的 SMEMBERS 引起的。而慢查询日志,就是你的"黑匣子记录仪"。
🧠 什么是慢查询?
Redis 会自动记录执行时间超过阈值的命令到一个内存中的 FIFO 队列(先进先出),这就是 慢查询日志(Slow Log)。
- ✅ 只记录命令本身(不含网络传输时间)
- ✅ 包含执行耗时、发生时间、客户端信息
- ❌ 不记录读取的数据内容(保护隐私)
🔍 类比:
就像汽车的行车记录仪------只记"什么时候、发生了什么操作、花了多久",不记车内对话。
⚙️ 核心配置(redis.conf)
Redis 通过两个参数控制慢查询:
conf
# 1. 超时阈值(单位:微秒,1 秒 = 1,000,000 微秒)
slowlog-log-slower-than 10000 # 默认 10ms
# 2. 日志队列最大长度(FIFO,超出则丢弃最旧的)
slowlog-max-len 128 # 默认保留最近 128 条
💡 建议生产环境配置:
conf
slowlog-log-slower-than 5000 # 记录 >5ms 的命令(更敏感)
slowlog-max-len 1000 # 保留更多历史
🔧 如何查看慢查询日志?
python
# 如果使用redis终端
# 127.0.0.1:6379> SLOWLOG GET 10
import redis
r = redis.Redis()
# 获取所有慢日志(最多返回 slowlog-max-len 条)
slowlogs = r.slowlog_get(10)
for log in slowlogs:
print(f"ID: {log['id']}")
print(f"Time: {log['time']}")
print(f"Duration: {log['duration'] / 1000:.2f} ms")
# log['command'] 是 bytes,需要 decode
print(f"Command: {' '.join(log['command'].decode().split())}")
print("---")
🌰 常见"慢命令"黑名单(务必警惕!)
| 命令 | 为什么慢? | 替代方案 |
|---|---|---|
KEYS * |
遍历所有 key,O(N) | 用 SCAN 渐进式迭代 |
SMEMBERS big_set |
返回整个集合(可能百万级) | 用 SSCAN 分批取 |
HGETALL big_hash |
返回整个 Hash | 用 HSCAN 或只取需要的字段 |
LRANGE list 0 -1 |
返回整个 List | 用 LRANGE list 0 99 分页 |
FLUSHDB / FLUSHALL |
清空数据库(阻塞) | 避免在生产使用 |
大 Value 的 GET/SET |
网络 + 内存拷贝开销大 | 拆分 Value,或用压缩 |
Redis 计算耗时的方式:
- ✅ 包含:命令解析、数据查找、修改、返回构建
- ❌ 不包含:网络 I/O、排队等待时间
所以:
- 如果客户端网络慢,不会被记入慢查询
- 如果 Redis 正在 BGSAVE,命令排队很久,排队时间不算,只算实际执行时间
📌 这意味着:慢查询反映的是 Redis 自身的 CPU/内存瓶颈,不是网络问题。
✅ 推荐监控策略
- 设置合理阈值:slowlog-log-slower-than 5000(5ms)
- 定期巡检:每天跑脚本检查是否有新慢日志
- 告警集成:如果慢日志数量突增,触发企业微信/钉钉告警
- 压测时开启:性能测试期间密切观察慢日志