做业务的人,对“黑名单”这三个字应该都不陌生。大促期间风控要快速拉黑一批恶意账号,客服要封禁被投诉的违规用户,运营要限制某个设备ID参与活动,甚至某个接口突然被刷,需要立刻切断某个来源的调用。这些需求听起来都很简单,但一旦业务跑在 K8S 集群里,事情就不再是“改个配置重启一下”能解决的了。
我自己在这上面是踩过大坑的。早期我们的服务部署在虚拟机,黑名单写进应用配置文件,每次更新靠运维手动分发,虽然麻烦,但勉强能用。后来服务整体容器化并迁移到 K8S,问题立刻暴露:副本数一多,IP 是漂移的,发布是滚动的,本地文件配置根本没法做到多实例同步。更麻烦的是,业务拆成微服务之后,同一个黑名单判断可能散落在五六个服务里,你在这个服务加了限制,流量从另一个服务绕过去,禁令就形同虚设。
这篇文章就围绕“业务禁令黑名单”在 K8S 环境下的技术选型来展开。我不会只给一个标准答案,而是把我在实际项目中对比过的几条路线、每个方案的适用边界、以及最终选型的判断依据都摊开来讲,包括完整的落地思路和排错经验。对于正准备做类似功能,或者正在为“黑名单到底该写在哪一层”纠结的朋友,这篇内容应该能帮你省下不少试错的时间。
1. 先搞清楚业务禁令到底要拦什么
很多人在技术选型里纠结半天,最后发现是需求定义没想清楚。业务禁令看起来都是“不允许某些对象执行某些操作”,但拦的位置不同,方案天差地别。在 K8S 环境下,这个问题被放得更大,因为入口流量、服务间调用、业务内部逻辑这三条路径是完全独立的。
1.1 从一次“大促风控”风波说起
去年我们做一次大型促销活动,运营在活动开始两小时后发现一批账号在批量刷优惠券。风控组那边给出了一个大约三千个账号 ID 的名单,要求在十分钟内让这些账号无法领取任何优惠券。
放在以前单体应用时代,改个配置重启就行。但当时我们的领券服务在 K8S 里有二十个副本,滚动发布一轮要七八分钟,而且风控后面还在陆续补充新的账号。要知道,在 K8S 环境里,多副本部署意味着任何一个本地方案都会失效——你改了第一个 Pod 里的文件,其他十九个 Pod 还是旧的;等滚动发布完成,第一批被封的账号在缓存里已经领过一轮了。这个场景让我意识到,K8S 下的黑名单方案,必须具有天然的分布式属性。
1.2 黑名单的三个拦截层级
我把业务禁令的拦截位置拆成三层:
第一层是入口网关层,拦截维度主要是 IP、User-Agent、设备指纹、JWT 中的用户标识。这一层拦的是“请求还没到业务服务”之前。优点是覆盖面广,一个规则能管住所有下游服务;缺点是拿不到业务上下文,做不了精细判断,比如“这个用户是黑名单账号但他手里有未核销的券需要允许看详情页”。
第二层是服务间调用层,也就是微服务 A 调用微服务 B 时,在调用链路上做拦截。这一层能识别调用来源、目标接口、携带的用户信息,可以做比较精细的控制。在 K8S 里,这一层通常由 Service Mesh 或者服务框架的过滤器实现。
第三层是业务逻辑层,拦截时机是在代码里执行具体的业务规则,比如下订单前判断用户是否在白名单里,领券前判断设备是否命中风控名单。这一层最灵活,但侵入性最强,每个需要做判断的业务方都得单独接入,黑名单逻辑容易在各服务间复制粘贴,后期维护相当痛苦。
选型的第一步,就是明确你的禁令到底要覆盖哪一层。只拦入口,还是内网全链路都要拦,这两个方向对应的技术方案完全不同。
2. 主流方案盘点:从硬编码到全链路治理
在 K8S 环境里做黑名单,我梳理下来大致有五条路线:应用内本地缓存、Redis 分布式标记、网关层拦截、Service Mesh 流量治理、K8S 原生 NetworkPolicy。它们不是替代关系,而是适用场景的差异。
2.1 应用内硬编码与本地缓存方案
这是最朴素的做法。把黑名单写死在代码里,或者启动时从配置中心拉取一次加载到本地内存,然后业务逻辑里判断。
优点是实现简单,查询极快,不依赖外部组件。但缺点在 K8S 环境下非常致命:多副本之间数据不一致,滚动发布期间新旧 Pod 的名单不同,更新需要走完整发版流程,在需要秒级生效的风控场景里根本不可接受。我见过有团队用 Apollo 或 Nacos 做配置中心,配置改了能推送,但业务服务需要监听配置变更并重建本地缓存,稍微有点并发就会遇到新旧版本不一致。这个方案适合那些“禁用一个功能开关”而不是“禁用一个可变名单”的场景,比如临时下线某个接口,但它的灵活性完全不够,所以不建议作为主方案。
2.2 Redis 分布式标记:业务侧的“缓存版禁令”
这个方案在现在的互联网公司里最普遍。业务服务在判断逻辑处调用 Redis,查询某个用户 ID 是否在某个黑名单 Key 里,或者查某个设备 ID 是否命中限购标记。榜单存在 Redis 的 Set、Hash、或者 String 类型里,配合 TTL 实现自动解禁。
这个方案的灵魂在于“分布式”和“热更新”。所有 Pod 共享同一个 Redis,数据天然一致;写入黑名单只是操作 Redis,毫秒级全局生效;TTL 到期自动清理,不需要额外任务去解禁。我甚至可以开一个管理后台,运营点一下“封禁”,Redis 里这个 Key 就写入一个账号,业务侧下一次请求立刻就能感知。
但它的短板也很明显:需要业务代码显式接入,如果黑名单判断分散在多个服务,每个服务都要做一次一样的查询;而且如果业务方偷懒直接用本地缓存套 Redis,就会遇到我们后面会讲的缓存一致性问题。
2.3 网关层拦截:Ingress、API 网关与 Lua 脚本
网关层方案在 K8S 环境里非常自然。请求先经过 Ingress Controller 再进入 Service,所以在 Ingress 这一层做黑名单判断,理论上能拦截掉所有外部流量。常见的实现包括:
- 用 Nginx Ingress 的 lua_shared_dict 配合 Lua 脚本,在 access 阶段做 IP 或 Header 匹配。
- 使用 APISIX、Kong 这类云原生网关,通过插件机制做限流和黑白名单,规则存在 etcd 或 Redis 里。
- 用 Envoy Gateway 或者 Istio Ingress Gateway,配合 EnvoyFilter 或 Lua Filter 做拦截。
网关方案的最大好处是业务无侵入。我们可以做到业务服务完全不知道黑名单的存在,流量在网关就被拦掉了。比较典型的是全局 IP 黑名单、恶意 UA 拦截、以及按 JWT Claim 里的用户等级做粗粒度限制。
但网关方案的粒度受限于网关能解析的内容。比如网关能看到 Authorization 头里的 JWT,但看不到数据库里的用户优惠券状态;它能拦截掉恶意 IP 的请求,但没法判断一个“已登录但设备指纹异常”的请求是否应该被放行。另外,网关层的规则更新依赖网关自身的配置管理,在 K8S 里通常表现为 ConfigMap 更新 + Ingress Controller Reload,这个过程中可能存在几秒到几十秒的延迟,对大促秒级封禁来说并不理想。
2.4 Service Mesh 层拦截:Istio EnvoyFilter
如果你的集群已经上了 Service Mesh(Istio 是目前最主流的),那么 EnvoyFilter 就是另一个值得考虑的方案。EnvoyFilter 允许你在 Envoy 的过滤器链上插入自定义 Lua 或 WASM 过滤器,拦截条件基于流量属性:source workload、destination service、URL path、Header、JWT payload 等。
这个方案能做到全链路统一拦截。不管调用方是网关、其他服务,还是定时任务,只要流量经过 Sidecar Proxy,就会被统一规则约束。比如我们要“禁止用户 10086 调用下单接口”,可以在 EnvoyFilter 里写一条规则,对所有流向下单服务的请求检查 Header 里的用户 ID,命中就返回 403,业务 Pod 全程无感知。
但 Service Mesh 的复杂度是真实存在的。EnvoyFilter 的匹配语法有点陡峭,排障时需要看 Envoy 的访问日志和动态配置,运维成本比前面几个方案高一个量级。而且 WASM 过滤器的开发和调试,说实话没有 Redis 方案那么直观。如果你的团队已经熟练运维 Istio,这个方案很优雅;如果只是为了黑名单去引入 Service Mesh,我劝你慎重。
2.5 K8S 原生方案:NetworkPolicy 与准入控制
最后提一下 K8S 原生的 NetworkPolicy,很多人一看到“禁令”二字就想用它。NetworkPolicy 负责的是网络层的允许/拒绝,比如“禁止 Pod A 访问 Pod B 的 8080 端口”。它做物理隔离没问题,但做业务黑名单其实非常不顺手。
原因有三点:第一,业务黑名单的维度是用户 ID、设备 ID、订单号,而 NetworkPolicy 的维度是 IP 和端口,需要把用户维度翻译成 IP 维度,而 Pod IP 又是动态变化的,运维成本极高;第二,NetworkPolicy 在网络层直接丢包,业务拿不到任何业务语义的错误码,排查问题很被动;第三,NetworkPolicy 的更新依赖 CNI 插件的实现方式,策略生效时间不可控。所以我基本不会把 NetworkPolicy 划入业务禁令的候选方案,它更适合做租户隔离和基础安全防护,而不是业务规则。
2.6 五个方案横向对比
我日常选型时会用一张速查表评估方案。
| 方案 | 生效时效 | 业务侵入性 | 覆盖层级 | 运维成本 | 适用场景 |
|---|---|---|---|---|---|
| 应用内硬编码/本地缓存 | 分钟级~小时级 | 高 | 业务逻辑层 | 低 | 永久性功能开关,名单极少变更 |
| Redis 分布式标记 | 毫秒级 | 中 | 业务逻辑层/服务调用层 | 低 | 风控封禁、限购、账号封禁,需要热更新 |
| 网关层拦截 | 秒级~分钟级 | 低 | 入口流量层 | 中 | 恶意 IP/UA 拦截、全局黑名单、限流 |
| Service Mesh | 秒级 | 极低 | 全链路流量层 | 高 | 全链路统一禁令,团队有 Mesh 运维能力 |
| NetworkPolicy | 依赖 CNI,分钟级 | 无 | 网络层 | 中 | 租户隔离、基础安全,不适合业务规则 |
这个表格基本回答了我每次技术评审都会被问到的几个问题。按经验来看,Redis 方案适合 70% 的业务场景,网关方案适合入口治理,Service Mesh 适合“我就是要一刀切”的全链路管控。下面我会重点讲我最终选型的组合方案,以及落地过程中的关键细节。
3. 选型判断:四个决策指标与三个推荐组合
选型不能只看网上博客的推荐,要把你自己的场景参数带进去算一遍。我习惯用四个指标来卡方案,分别是生效时效、作用域、业务侵入性和运维成本。
3.1 四个决策指标的判断方法
生效时效指从“更新黑名单”到“拦截生效”的延迟。要明确的是,K8S 滚动发布带来的延迟问题经常被人低估。比如 ConfigMap 更新后,Kubelet 默认刷新周期是 60 秒,而 Pod 内挂载的文件还要更久才能看到变化,这一下就是几分钟。如果业务要求“分钟级封禁”,那就要放弃一切依赖配置文件的方案。
作用域指禁令覆盖的范围。是只拦入口流量,还是要把服务 A 调服务 B 也给拦住?在 K8S 里,如果两个服务在同一个 namespace 内可以直接 Service 名通信,但服务间调用不一定过网关,所以网关方案覆盖不到服务间调用。如果你的场景是全链路禁止,就得考虑 Service Mesh 或者在业务代码里统一拦截。
业务侵入性指为了接入黑名单方案,业务方要改多少代码。Redis 方案要引入 SDK,网关方案业务无感,Service Mesh 方案业务也基本无感。侵入性越高,落地阻力越大,推广越难。
运维成本包括组件部署、告警、排障和升级维护的投入。Redis 方案通常是复用已有 Redis,运维成本最低;Service Mesh 需要维护 EnvoyFilter 和环境基线,成本最高。
3.2 推荐组合一:大促风控型禁令,选 Redis 双层标记
对时效要求极高的补丁型禁令,我会首选 Redis。具体做法是:定义一个黑名单 Hash,Key 为业务场景名,Field 为用户 ID 或设备 ID,Value 为禁令类型和生效时间。业务侧在关键入口调用一个封装好的访问控制组件,组件先查 Redis,再决定是否放行。
这套方案的生效时效几乎为零,因为 Redis 操作本身就是业务请求链路的一部分。名单管理端写一套运营后台,操作 Redis 即可。对于 K8S 环境,Redis 是无状态依赖,和 Pod 的调度、滚动发布没有任何耦合,这是它最大的优势。
3.3 推荐组合二:入口级统一拦截,选网关 Lua 脚本
如果黑名单对象是 IP、请求特征这些入口属性,不涉及具体业务数据,那就放到网关层拦截。Nginx Ingress 或 APISIX 的 Lua 脚本能做到在 access 阶段直接 return 403,业务 Pod 连日志都不用打。
我这边有一个典型场景:某个第三方服务商的服务器在疯狂刷我们首页接口,对方的出口 IP 段我们全知道,这时候在网关配一个 IP 黑名单,用 lua_shared_dict 缓存名单列表,配合 nginx 定时拉取配置,几十秒内全局生效。
3.4 推荐组合三:全链路统一禁令,选 Service Mesh
如果你所在的部门已经掌握了 K8S 和 Istio,且有“某个用户在所有服务里都不能再操作”这类需求,那就别在业务代码里到处埋点,直接用 EnvoyFilter。让流量带用户标识,比如通过 HeaderX-User-Id,Envoy 在进入业务 Pod 前检查这个 Header 是否命中全局黑名单,命中直接返回 403。
这个方案的好处是响应链路极短,规则代理本地完成,不经过业务逻辑;坏处是排障链路长,你需要看 Envoy 日志才能确定请求是被哪个 Filter 拦截的。我建议在 Filter 里写清楚一个自定义 Header 标记拦截原因,方便后续排查。
3.5 我不推荐的组合
本地文件配置方案在今天的 K8S 环境里基本可以淘汰了。除非你的集群规模很小、服务只有单副本,或者黑名单一年才改两三次,否则本地文件方案会因为多副本数据不一致而埋下巨大的安全隐患。
还有一点要提醒:不要把黑名单强耦合在业务数据库里。有人喜欢在 MySQL 里建一张黑名单表,应用每次查询。数据一致性有了,但每次请求多一次数据库 IOC,延迟上去不说,数据库压力也会白白增加。在高频热路径上,缓存是必需品。
4. 实操记录:Redis + Lua 热更新黑名单的落地细节
前面讲了这么多选型依据,这块我用一个我实际迭代过的方案作为例子:面向业务逻辑层的 Redis 黑名单组件,配合 K8S ConfigMap 做场景配置。这套方案适合大多数基于微服务的中小型业务团队。
4.1 数据结构设计:为什么用 Hash 而不是 String
黑名单的存储结构我先后试过 String、Set 和 Hash,最终固定为 Hash。原因有三个:
第一,Hash 支持按业务场景隔离。一个 Redis Key 对应一个场景,Field 是黑名单对象标识,互不干扰。比如说biz:ban:coupon存领券禁令名单,biz:ban:login存登录禁令名单。
第二,Hash 可以单个 Field 设置 TTL,虽然并不是原生支持,但可以通过 Redis 7 的HEXPIRE命令实现,或者在 Value 里存过期时间,通过 Lua 脚本统一判断。这样同一个场景里,不同用户的封禁时长可以不同,到期自动失效。
第三,Hash 的查询是 O(1),在超大规模黑名单场景下性能有保障。如果黑名单数量到了百万级,Set 和 String 也能查,但 Hash 的粒度管理能力更强。
我使用的 Key 结构如下:
# 领券业务黑名单,key = biz:ban:coupon HSET biz:ban:coupon 10086 "user" HSET biz:ban:coupon 10010 "user"写入时附带封禁时间信息,可以用一个辅助 String Key:
# 记录封禁到期时间戳 SET biz:ban:coupon:expire:10086 1735689600这样读取时先查 Hash 是否存在,再比对到期时间戳。当然,如果你们团队用的 Redis 版本支持,直接上 HEXPIRE 更省事。
4.2 用 Lua 脚本保证封禁查询和到期判断的原子性
在判断请求是否命中黑名单时,要用 Lua 脚本把“是否存在 + 是否过期”两步操作合成一步执行,避免并发问题。
-- KEYS[1]: blacklist hash key -- KEYS[2]: expire prefix key -- ARGV[1]: target field (user/device id) -- ARGV[2]: current timestamp local banned = redis.call('HEXISTS', KEYS[1], ARGV[1]) if banned == 0 then return 0 end local expireAt = redis.call('GET', KEYS[2] .. ARGV[1]) if expireAt and tonumber(expireAt) < tonumber(ARGV[2]) then return 0 end return 1业务侧封装一个统一的 SDK,调用这个 Lua 脚本,返回 1 则直接拒绝并抛出业务错误码。封装好之后,业务方只需一行代码接入:
if (banClient.isBanned("coupon", userId)) { throw new BizException(403, "你已被限制参与本活动"); }这整个查询过程在 Redis 上通常 1 毫秒内返回,对业务延迟基本无感。
4.3 写入黑名单的管理通道
黑名单的写入不能走业务应用直连 Redis SET,我建议走一个独立的管理接口或者定时任务。管理接口负责参数校验、风控审批记录和 Redis 写入。这样后续要追溯“谁在什么时间封禁了谁”,有据可查。
public void banUser(String scene, String userId, Duration duration) { String hashKey = "biz:ban:" + scene; String expireKey = "biz:ban:expire:" + scene + ":" + userId; long expireAt = System.currentTimeMillis() / 1000 + duration.getSeconds(); jedis.hset(hashKey, userId, "1"); jedis.set(expireKey, String.valueOf(expireAt)); jedis.expire(hashKey, duration.getSeconds() + 86400); // 保险过期 }同时在管理后台里保留操作日志,关键黑名单操作需要二次确认。这些虽然看起来和 K8S 没啥关系,但线上环境的安全审计往往比技术方案本身更重要。
4.4 K8S 部署中的几个雷区
这套方案整体不依赖 K8S 内部状态,但在部署时仍有两个问题要注意。
第一个是 Redis 的地址配置。不要把 Redis 连接地址写成某个具体 Pod 的 IP,因为 K8S 里的 Pod IP 是不固定的。应该使用 K8S Service 的 DNS 名称,比如redis-headless.default.svc.cluster.local:6379,并且开启 Sentinel 或 Cluster 模式时,让客户端从服务发现中获取节点列表。这里经常能见到有人把 Redis 地址配置在 ConfigMap 里,然后滚动发布时配错了 namespace,导致服务连不上 Redis。建议统一使用 ConfigMap 管理,并做好环境隔离。
第二个是黑名单管理服务和业务服务最好放在不同的 namespace 下,通过 RBAC 控制权限。这样即使用户误操作删掉了业务 namespace,黑名单管理通道依然可用。另外,如果你的集群规模很大,Redis 黑名单最好独立部署一套,或者使用独立的 Redis 逻辑库,避免和大促交易缓存互相影响导致 Redis 阻塞,进而拖垮黑名单判断。这里就涉及到 K8S 资源调度的问题了,黑名单 Redis 的 Pod 要设置足够的 CPU 和内存 Limit,并配置独立的 PodDisruptionBudget。
5. 常见问题与排查技巧实录
任何方案上线后都会遇到问题。下面这几个坑,是我在这些年的 K8S 黑名单项目中真实踩过的,分享出来,至少能让你在排查时少走弯路。
5.1 黑名单不生效的典型原因
最经典的案例是“明明把用户加进黑名单了,但他还是能正常访问”。排查顺序我建议这么走:
第一,确认 Redis 里数据写进去了。用HGETALL biz:ban:coupon看一眼,如果数据在,再看看过期时间戳是否已过。我们的封禁组件里曾经有个 bug,过期时间写的是System.currentTimeMillis()而不是秒,导致封禁瞬间就失效了。
第二,确认业务代码里真的调用了黑名单判断。很多时候加了黑名单组件,但业务链路的某个分支漏掉了,比如领券时判断了,但兑换时没判断,用户绕道兑换入口完成了操作。
第三,如果是多副本部署,检查本地缓存。如果业务开发图省事,在应用里加了一层 Caffeine 本地缓存来缓存黑名单结果,又没有设置合理的过期时间,就会出现“有副本还是旧数据”的经典问题。在 K8S 环境下,这种情况特别隐蔽,因为你滚动更新后部分实例会恢复,但没更新的实例依旧放行。
5.2 误杀用户后的“急救”流程
误杀在风控场景里太常见了。一个正常用户被错误封禁,这时最重要的不是分析原因,而是先放行。
所以黑名单方案的优先级设计,一定要有“白名单高于黑名单”的概念。用户在操作时,如果命中白名单,就直接跳过黑名单判断。这个操作要独立于黑名单 Redis,可以用单独的 Key 前缀存储。
# 白名单 key SET biz:whitelist:coupon:user:10010 1急救流程固定成三步:确认用户 ID,写入对应场景白名单,再通知业务侧观察日志。等确认是误杀后,再清理黑名单里对应的 Field。白名单的设计虽然看起来笨,但它在关键时刻是救命的。
5.3 黑名单规模过大时的性能策略
有人会遇到黑名单数量持续增长,Redis Hash 越来越大,内存压力增加。这时候有两个调整方向。
第一个是拆分场景,不要所有禁令都堆在一个 Hash 里。比如“领券禁令”和“下单禁令”是完全不同的业务场景,分开管理可以避免单个 Key 过大和热 Key 问题。
第二个是引入布隆过滤器做前置过滤。如果黑名单集合非常大,比如上千万级设备 ID 需要在毫秒级内判断,可以在 Redis 前加一层本地布隆过滤器,只有命中布隆过滤器时才回源 Redis 精确判断。不过这个方案会带来误判率,而且布隆过滤器不支持删除,需要定期重建。所以这个方案我一般只在集合达到千万级以上时才建议使用。
5.4 和 K8S 滚动发布相关的黑名单隐患
滚动发布时,如果旧 Pod 还在处理存量请求,而新 Pod 已经加载了新的黑名单配置,同一个用户可能在新 Pod 被拦截,在旧 Pod 放行,造成业务状态不一致。这个问题在网关层方案里更明显,因为 Nginx Ingress 的配置 Reload 会短暂中断现有连接。
经验做法是:发布期间,黑名单管理后台禁止大批量操作;如果必须操作,优先使用 Redis 方案而不是 ConfigMap 方案,因为 Redis 不依赖 Pod 生命周期,不会受到滚动发布的影响。
另外,如果你用 ConfigMap 挂载黑名单配置到 Pod 中,要清楚 Kubelet 的默认同步周期。你改了 ConfigMap,Pod 里的文件不会立刻变化。有些团队对这个时间差拿不准,就很容易出现“我明明改了配置,怎么没生效”的乌龙。这种场景下更合理的做法,是把 ConfigMap 当作“初始值”,运行时的黑名单变化始终走 Redis。
6. 这套选型思路还能怎么扩展
黑名单这件事,本质上是一类“状态管理”问题:在分布式环境里,快速判断某个对象是否处于某种特殊状态。想通这一点,很多场景都能复用。
比如灰度发布。在一个 K8S 集群里同时运行新旧两个版本的服务,你可以用同样的 Redis 标记方案,按用户 ID 灰度切流,实现“白名单先体验、逐步放量”的发布流程。这和黑名单的机制区别,不过是判断逻辑反过来而已。
再比如限流。以用户维度做限流时,“这个用户已经超过限制次数”本质上就是一个禁令判断。用 Redis 计数器配合 Lua 脚本,既可以统计次数,又可以在超过阈值时直接拦截。这和黑名单方案共用一套基础设施。
还可以扩展为功能开关。某个功能出问题了,想临时对部分用户关闭,不需要紧急发版,直接通过 Redis 写入该场景的黑名单,把用户挡在功能入口前。等修复后再删除对应 Key,用户无感恢复。其实你仔细看各大厂提到的“全链路灰度”“全链路压测”,底层都会有类似的控制平面和数据平面分离的思想,K8S 只是让这些控制手段更容易规模化落地而已。
从我个人的实际体验来看,做这类技术选型,最重要的不是纠结某个组件有多先进,而是想清楚你的禁令要在哪一层生效、有效期多长、更新多频繁。把这三个问题想透了,选型基本就定了一大半。
最后再分享一个小技巧。无论你选择了哪个方案,一定记得给“黑名单判断”单独增加监控指标。我们封装的黑名单组件里,每次判断都会记录命中数、放行数、Redis 耗时三个指标。有了这些指标,后续做风控效果分析、判断是否有误杀、评估 Redis 容量压力,全都有了数据支撑。没有监控的黑名单就是一颗定时炸弹,这句话我在无数个复盘会上讲过。