不能只靠Redis SET查黑名单,因会导致Redis高负载、实例间缓存不一致及新节点冷启动空白期;须结合Pub/Sub广播变更,按zone模式订阅,消息带version/zone/ttl字段,并实现连接保活与轮询降级。

为什么不能只靠Redis SET查黑名单
每个网关实例直连 Redis 查 SMISMEMBER ip_blacklist 192.168.1.100,看似简单,但实际会暴露三个硬伤:一是所有实例重复查同一 key,QPS 上万时 Redis CPU 和网络带宽立刻打满;二是新增/删除黑名单后,各实例缓存不一致,有的还在放行、有的已拦截,最长延迟可达共享字典的 TTL(比如 30 秒);三是扩容新网关节点时,冷启动期间完全无黑名单数据,存在数秒空白期。Pub/Sub 不是替代 SET,而是补上「变更广播」这一环。
必须用 PSUBSCRIBE 而不是 SUBSCRIBE
企业网关通常按业务 zone 隔离(如 api_v1、admin、mobile),黑名单也需分域管理。若用 SUBSCRIBE blacklist:update,所有网关都会收到全部变更,哪怕只管 mobile zone 的实例也要解析无关消息,徒增 CPU 和 GC 压力。正确做法是让每个网关按自己 zone 订阅模式:PSUBSCRIBE blacklist:api_v1、PSUBSCRIBE blacklist:admin。Redis 服务端完成 pattern 匹配,客户端只收目标消息,零冗余。
封禁指令必须带 version + zone + ttl 字段
单纯发 PUBLISH blacklist:api_v1 {"ip":"1.2.3.4","action":"ban"} 不够。真实生产环境要防乱序、防重放、防误删:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 消息体必须含
version(推荐毫秒级时间戳,如1751723456789),网关本地缓存每条 IP 的最新 version,只处理更大 version 的更新 -
zone字段显式声明作用域,避免跨 zone 误操作(例如 admin zone 的封禁指令不该影响 api_v1 流量) -
ttl字段传入秒数(如300),网关收到后调用shared_dict:set(ip, true, ttl),且该 ttl 必须比 Redis 中实际SETEX blacklist:api_v1:1.2.3.4 1 1的过期时间短 1–2 秒,防止缓存残留
连接存活与降级必须手动兜底
Redis Pub/Sub 连接断开后默认不会自动重连,且 PSUBSCRIBE 是阻塞命令——一旦网络抖动,整个 Lua worker 可能卡死。必须拆成两层保活:
- 用
resty.redis:new()创建 client 后,立即调用red:set_timeout(1000)和red:set_keepalive(10000, 20),把连接放回池子而非销毁 - 在
init_worker_by_lua_block里起一个定时任务(ngx.timer.at(0, reload_subscriber)),每次失败后指数退避重连,并记录ngx.log(ngx.WARN, "pubsub reconnect failed:", err) - 最关键的是降级逻辑:当 Pub/Sub 失效超 5 秒,自动 fallback 到每 5 秒轮询一次 Redis 的
blacklist:api_v1:metahash(存最后更新时间戳),确保不因消息丢失导致全网失效
真正难的不是发一条 PUBLISH,而是让上百个网关实例在断网、重启、扩容瞬间,对同一条封禁指令达成状态一致——这要求每个环节都放弃“它应该正常工作”的假设,转而写明“它坏了怎么办”。

















