核心结论:Redis Pub/Sub 天然不适合异步任务处理——它缺乏确认机制、持久化能力、消费者组支持以及积压缓冲。不要将其用作任务队列。如果需要可靠的任务队列,应使用 LPUSH+BRPOP 或 XADD+XREADGROUP(Stream)方案。

然而,Pub/Sub 并非毫无价值。它非常适合轻量级远程指令下发——例如重启服务、触发备份、清理缓存等“发送即忘”的运维操作。但前提是必须了解其局限性:它不保证消息送达、不支持应答确认、不保存历史消息,因此切勿用于需要强一致性或结果反馈的任务。
为什么不能直接用 redis-cli 做生产级远程执行
很多人图省事,直接把 redis-cli SUBSCRIBE 当成守护进程养在目标机器上。结果呢?网络抖动、终端被关闭、shell 脚本意外退出——连接说断就断,而且没有任何重连逻辑。更坑的是,SUBSCRIBE 是个阻塞命令,一旦进入监听状态,后续的 shell 命令全被堵住,整个脚本直接卡死。
- 使用
redis-cli SUBSCRIBE channel时,若收到 Ctrl-C 或连接中断,不会自动重试 - 缺乏心跳保活机制,当 TCP 空闲超时(
timeout配置),Redis 会悄然断开连接,导致订阅丢失 - 无法区分消息来源,且无签名校验——任何能连接 Redis 的客户端均可向频道发送指令(安全隐患极大)
- 消息体仅为原始字符串,缺乏
target、ttl、signature等结构化字段,容易误执行其他指令
Python 订阅端必须处理的三个关键点
如果用 redis-py 写订阅脚本,有个容易踩的坑:pubsub.get_message() 默认是非阻塞的,没消息就返回 None。你要是直接上 while True 空转,CPU 直接起飞。同时还得防着网络闪断导致整个进程挂掉。
- 务必为
pubsub.get_message()设置timeout=1参数,避免 CPU 空转 - 捕获
redis.ConnectionError和redis.TimeoutError异常,并在异常发生时重建pubsub实例并重新subscribe - 收到
message['data']后,先进行基础校验:判断是否为合法 JSON?是否包含cmd字段?是否携带时间戳以防止重放攻击(例如检查ts > time.time() - 30) - 执行指令时,建议使用
shlex.split()解析命令,而非直接传入os.system()——否则类似data: "reboot; rm -rf /"的恶意指令可能造成严重后果
发布端如何避免指令被误刷或重复执行
运维指令不是聊天消息,发错一次可能直接导致服务中断。所以发布端得自带约束,不能指望下游来做判断。
- 指令必须序列化为字典格式,至少包含
{"cmd": "systemctl restart nginx", "target": "web-01", "nonce": "abc123"},然后使用json.dumps()发送 - 在调用
redis.Redis().publish()前,先通过PUBSUB NUMSUB channel查看当前订阅者数量。若为 0,则表明目标机器离线或未启动监听,应停止发送 - 对于敏感操作(如
reboot、drop database),发布前需增加二次确认,或要求携带auth_token字段并与白名单进行比对 - 避免使用通配符频道(如
PSUBSCRIBE ops.*)接收指令,模式匹配可能导致跨环境指令混淆,存在较大隐患
真正上线前必须关掉的 Redis 默认配置
默认的 redis.conf 是给本地开发用的,要想安全地做远程指令下发,必须显式放开并加固配置:
bind不应仅设置为127.0.0.1,应明确绑定内网 IP(例如bind 192.168.10.5),或注释该行(监听所有接口,但不推荐)protected-mode yes需改为no,否则非本地连接将被拒绝(仅限内网环境使用)requirepass必须设置强密码,发布端和订阅端均需传递password=xxx,否则指令通道缺乏保护- 建议将
tcp-keepalive设置为 60,避免 NAT 设备或防火墙将长连接视为僵尸连接而断开
还有一个极易被忽略的细节:订阅脚本启动后,Redis 连接对象(redis.Redis())与 pubsub 对象是独立的。断连时若仅重建 pubsub 而底层连接未重连,get_message() 将持续抛出 ConnectionError,无法自动恢复——必须同时重建整个连接对象才有效。
