
1. 先拆需求MySQL 黑名单到底要挡什么刚接手一套线上系统运维同事跑过来问我“MySQL 有没有黑名单功能最近有人扫端口我想把一批 IP 直接挡掉。”这个问题看着简单其实能拆出四五种完全不同的实现路径。被扫端口可能要在防火墙挡如果是业务里有人刷接口可能需要一张黑名单表如果是某个账号被人拿去跑查询可能要把账号锁了如果是从 MySQL 层想拦截特定来源主机init_connect才是关键。这篇文章就围绕“mysql 黑名单”这个关键词把连接层、账号层、业务层的几种做法全部捋一遍顺便附上我实际踩过的坑。1.1 最常见的三种黑名单诉求我把平时收到的需求归成三类不同诉求对应完全不同的方案千万别混着用。第一类是连接级封禁。典型场景某个机房 IP 天天来爆破 3306错误日志里全是 Access denied或者某个合作方 IP 大量占用连接数把正常业务挤垮。诉求是“别让这个 IP 连上来”。第二类是账号级封禁。典型场景外包离职后账号没回收有人拿着这个账号半夜跑全表扫描或者某个只读账号的密码泄露需要紧急停用。诉求是“让这个账号失去能力”不管它从哪个 IP 来。第三类是业务数据级黑名单。典型场景风控系统识别出恶意用户后需要把 userId、手机号、身份证号挡在订单系统之外运营把某个大 V 拉黑后所有接口都不再返回他的数据。诉求是“业务逻辑上的拒绝”。这三种诉求虽然都叫“黑名单”但实现手段、运维方式、回滚成本完全不一样。我在文档里看到有些人把账号锁了然后拿来做业务黑名单这是典型的方案错配后面查问题会非常痛苦。1.2 为什么 MySQL 没有官方“黑名单”开关很多人上来就问“MySQL 有没有设置黑名单的命令”这是个误区。MySQL 的权限模型是白名单模型你通过GRANT给某个用户授予某些权限用户只能做被允许的事没被允许的一律拒绝。它压根没有设计“单独禁止某个数据库、某个表、某条 SQL、某个 IP”的黑名单语法。为什么会这样设计因为白名单比黑名单安全。白名单天然是“默认拒绝”漏配权限只是功能不可用黑名单是“默认放行”漏配一条规则就可能是安全漏洞。MySQL 从血缘上继承了这种安全思路所以官方文档里你找不到BLOCK USER这样的指令。但是场景逼到面前我们不能没有黑名单。于是业界用三种方式补位修改授权表做账号停用、init_connect做连接拦截、业务表做数据过滤。理解了 MySQL 的权限模型你就能明白这些补位方案为什么会有各种各样的限制理解限制比记住命令更重要。1.3 选型原则先用最小成本挡住最痛的场景我见过不少团队一上来就搞复杂的拦截系统结果三个月后没人维护。我的建议是先分清楚当前最痛的是哪一层用最小改动把它挡住再考虑要不要做覆盖全环节的黑名单体系。如果是环境安全扫描发现弱口令账号优先做账号层处理停用、锁账号、改密码。如果是单 IP 攻击优先在安全组或防火墙层挡不要为了几个 IP 去改数据库配置因为数据库层误伤面太大。如果是业务被刷优先做业务黑名单表配合缓存降低数据库压力。如果确实需要数据库层拦截来源 IP再考虑init_connect但必须接受它只对非特权用户生效这个限制。选型时还要考虑回滚成本。防火墙规则删一条就行账号解锁也是一条语句但如果你在init_connect里写错了拦截逻辑导致所有普通用户连不上数据库那可比被攻击还要命。下一章先讲最不容易误伤的账号层方案。2. 账号与权限层面的黑名单实操账号级黑名单最直接而且 MySQL 本身就提供了完整的账号停用机制只是很多人没用过。这一层是数据库管理员最应该先掌握的黑名单手段。2.1 用 create user 和 alter user 直接锁人账号锁定的标准语法是ACCOUNT LOCK。你可以创建时就锁也可以事后锁。-- 新建一个账号默认就是锁定状态不给任何权限 CREATE USER temp_outsource% IDENTIFIED BY InitPassw0rd ACCOUNT LOCK; -- 对一个已经存在的账号进行锁定 ALTER USER temp_outsource% ACCOUNT LOCK; -- 解锁 ALTER USER temp_outsource% ACCOUNT UNLOCK;账号锁定后这个用户尝试连接时会直接报ERROR 3118 (HY000): Access denied for user temp_outsource%. Account is locked.比单纯改密码更干脆。改密码只能防止旧密码被使用如果业务配置里已经把新密码同步出去了那依然能连ACCOUNT LOCK则没有这个漏洞无论密码对不对都进不来。批量处理时可以直接查mysql.user表生成锁定语句。比如把 90 天没登录的开发账号全部锁掉SELECT CONCAT(ALTER USER , user, , host, ACCOUNT LOCK;) FROM mysql.user WHERE account_locked N AND plugin IN (caching_sha2_password, mysql_native_password) AND (last_attempt_time IS NULL OR last_attempt_time NOW() - INTERVAL 90 DAY);注意mysql.user里的字段不同版本有差异8.0 里是account_locked和last_attempt_time8.0.29 之后还能看到last_attempt_failed。如果你用的是 5.7字段名需要先DESC mysql.user确认一下。还有一个细节ALTER USER ... ACCOUNT LOCK不会断开已经建立的连接。也就是说正在跑的长事务不会被打断。真要“立刻踢下线”得配合KILL语句或之后讲到的连接层方案来用。2.2 host 字段的正确玩法反白名单实现 IP 拒绝MySQL 的用户由user host共同组成host表示允许从哪里连。很多人以为可以写一个“黑名单主机”比如web_user!192.168.1.66——这个语法 MySQL 是不支持的。但我们可以利用用户匹配顺序做反白名单MySQL 在验证连接时会按 host 的精确匹配顺序选择账号用户名具体IP的优先级高于用户名%。利用这一点可以给要封禁的 IP 建一个专门账号设置为锁定或错误密码让来自该 IP 的连接必然失败。-- 假设线上业务账号是 app_user% -- 现在封禁 192.168.1.66 这个来源 CREATE USER app_user192.168.1.66 IDENTIFIED BY this_will_never_work; ALTER USER app_user192.168.1.66 ACCOUNT LOCK;来自 192.168.1.66 的连接会优先匹配到锁定账号从而被拒。来自其他 IP 的连接仍然匹配app_user%正常放行。这个方案特别适合那些无法改防火墙配置的托管数据库场景。但有个硬前提所有业务连接必须使用同一个用户名如果多个账号也要对同一 IP 封禁就得逐一建。说实话这个方案并不优雅维护起来要小心别把账号弄混。我更建议把 IP 级封禁放到安全组或防火墙做MySQL 层的host字段只做“默认允许范围收窄”。比如把app_user%改成app_user192.168.%在生产环境这是更正确的“反黑名单”姿势。2.3 资源限制让恶意连接自己“饿死”账号确认是安全的但连接建立后恶意跑大查询这种“软黑名单”该怎么处理MySQL 提供了MAX_QUERIES_PER_HOUR、MAX_UPDATES_PER_HOUR、MAX_CONNECTIONS_PER_HOUR、MAX_USER_CONNECTIONS四个资源限制参数可以在创建用户时设置。-- 限制只读账号每小时最多查询 1000 次最多同时 2 个连接 CREATE USER report_ro% IDENTIFIED BY Report2024 WITH MAX_QUERIES_PER_HOUR 1000 MAX_USER_CONNECTIONS 2;限制不是绝对的精确它按小时窗口统计但足以把“拿查询接口当爬虫”这类行为限制住。查询语句超过配额后MySQL 会报ERROR 1226 (42000): User report_ro has exceeded the max_queries_per_hour resource应用层会直接感知到。这里有个坑资源限制按账号维度统计不区分来源 IP。如果一个正常业务账号和一个可疑调用用的是同一个 MySQL 账号资源限制会误伤正常业务。所以最好一个应用一个账号或者干脆把“可疑流量”单独引流到另一个账号去观察。3. 业务黑名单表从表设计到缓存落地的完整方案如果封禁对象是业务实体用户 ID、手机号、设备号数据库层的账号和连接限制都派不上用场。这个时候需要一张真正的“黑名单表”。这一章讲的是我实际在多个项目里用过的设计经得起并发和查询量考验。3.1 黑名单表的字段设计与索引一张好用的黑名单表不是只有一个“黑名单用户 ID”字段就行的。你需要考虑谁拉黑的、什么时候生效、什么时候过期、什么原因以及最重要的——支持按多种维度查询。CREATE TABLE blacklist_item ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, biz_code VARCHAR(32) NOT NULL COMMENT 业务维度USER/PHONE/DEVICE/IP, biz_key VARCHAR(64) NOT NULL COMMENT 具体值如用户ID、手机号, status TINYINT NOT NULL DEFAULT 1 COMMENT 1生效 0失效, reason VARCHAR(255) DEFAULT NULL COMMENT 拉黑原因, source_app VARCHAR(32) DEFAULT NULL COMMENT 来源系统, operator VARCHAR(64) DEFAULT NULL COMMENT 操作人, expire_at DATETIME DEFAULT NULL COMMENT 过期时间NULL为永久, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_biz_key (biz_code, biz_key, expire_at), KEY idx_expire_status (expire_at, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci;这个设计的几个关键点先解释清楚。唯一键不能只建在(biz_code, biz_key)上。业务上需要支持“同一个用户被多次拉黑但生效时间不同”。比如用户今天被拉黑 7 天7 天后又被拉黑 30 天如果我只有(biz_code, biz_key)唯一键第二次插入就报重复键。把expire_at放进唯一键可以通过批量插入实现“多次拉黑互不影响”。status和expire_at为什么要区分这是为了做人工解封和自然过期解封两条路径。expire_at到了程序自动认为不再黑但记录还在审计能看到历史人工解封则把status置 0保留原始记录但立刻不再生效。查询时最常用的 SQL 是SELECT 1 FROM blacklist_item WHERE biz_code USER AND biz_key 123456 AND status 1 AND (expire_at IS NULL OR expire_at NOW()) LIMIT 1;如果表数据量到了千万级别这个查询用uk_biz_key唯一索引代价不小因为唯一索引里带了expire_at。我建议对于高频查询场景可以再建一张“当前生效黑名单”的紧凑表只含biz_code, biz_key, expire_at少了reason、source这些宽字段页缓存能塞下更多行查询更快。3.2 查询与校验一定要吃缓存黑名单的基本特征是“读取频率极高写入频率极低”每天可能几百万次校验命中某几条记录。让每次业务请求都打到 MySQL是我见过最浪费的做法。正确套路是多级缓存 数据库兜底。我常用的架构是本地缓存 Redis 缓存 MySQL 三层本地缓存如 Caffeine保存最近 N 分钟的命中结果抗住瞬时峰值。Redis保存全量“当前生效黑名单”启动时加载或定期刷新。MySQL负责写入和兜底数据一致性以它为准。判断是否命中时先查本地再查 Redis最后才查 MySQL。Redis 里可以直接用SET或HASH存储biz_code:biz_key判断用SISMEMBER或HEXISTSO(1) 操作。缓存要解决两个一致性问题。冷启动加载应用启动时从 MySQL 把“未过期的黑名单”全量刷到 Redis。SQL 是SELECT biz_code, biz_key FROM blacklist_item WHERE status 1 AND (expire_at IS NULL OR expire_at NOW())。千万级数据量也就是几 MB 到几十 MBRedis 完全塞得下。变更推送每次新增黑名单业务代码先写 MySQL再删 Redis 对应 key而不直接更新 Redis。删除让下一次查询 miss然后回源 MySQL 并回填。这个模型叫 Cache Aside简单可靠避免双写不一致。一个容易踩的坑本地缓存的过期时间不要设太长我建议 1 到 5 分钟。因为黑名单误判影响体验如果运营解封后用户要等 5 分钟才能恢复访问投诉量会很难看。网络安全和有损业务可以接受 5 分钟但面向用户体验的业务我倾向于 1 分钟甚至用事件广播主动失效本地缓存。3.3 过期解封与误封补偿怎么处理黑名单一定有过期场景。临时风控拉黑 24 小时到期自动解封这需要有个机制保证数据状态正确。我在生产环境见过最蠢的方案系统定时任务每分钟扫一次expire_at NOW()的记录然后 update。这个逻辑其实没必要——查询条件里已经带了expire_at NOW()过期记录本来就不会命中“当前生效”的判定扫表更新纯粹是自我感动。所以过期不需要主动处理查询条件已经天然过滤了。真正要处理的场景是被动刷新Redis 里如果一直没人访问过期的 key 会一直占内存。解决方案是 Redis key 的 TTL 跟expire_at对齐插入时计算剩余时间本地缓存同理过期时间设为expire_at的剩余时间到点自动消失。误封补偿比过期更麻烦。用户被误封后投诉运营需要立刻解封。这里要求后台解封入口能实时生效不能等缓存过期第一步更新 MySQL把status置 0。第二步删除 Redis 对应 key。第三步通过 Redis Pub/Sub 广播一条消息让所有服务节点删除本地缓存。如果你们没有消息总线退而求其次可以缩短本地缓存 TTL容忍几分钟延迟。但无论如何都不能只改 Redis 不碰 MySQL否则重启之后黑名单又回来了那种“解封失败”的故障最伤信任。4. 自动封禁闭环登录失败监控 事件调度真正的运维痛点其实是“怎么自动封禁”。人工看到告警再执行ALTER USER ... ACCOUNT LOCK中间至少几分钟攻击线程每分钟能试几百次密码。基于 MySQL 自身能力可以搭一套简单的自动封禁闭环用到登录失败日志表、存储过程和事件调度器。4.1 登录失败记录的采集MySQL 不主动记录登录失败明细靠general_log又太费性能。靠谱的做法是让业务层或中间件把认证失败写入一张业务表表名比如login_fail_log。CREATE TABLE login_fail_log ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_name VARCHAR(64) NOT NULL, host_ip VARCHAR(64) NOT NULL, fail_reason VARCHAR(128) DEFAULT NULL, fail_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_time (user_name, fail_time), KEY idx_host_time (host_ip, fail_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;还有一种办法打开 MySQL 的log_error_verbosity3然后定时解析错误日志里的 Access denied 记录。5.7 和 8.0 的错误日志格式不太一样解析脚本要写正则维护成本高。除非没有能力改业务代码否则不推荐。采集到失败记录之后封禁策略就变得很自然同一个用户名 5 分钟失败 5 次同一来源 IP 10 分钟失败 20 次触发封禁。封禁动作不是直接改mysql.user而是写入业务黑名单表同时视情况锁账号。4.2 event_scheduler 自动扫描封禁用事件调度器做周期扫描是成本最低的自动化方案。先确认事件调度是开启的SHOW VARIABLES LIKE event_scheduler; SET GLOBAL event_scheduler ON;注意SET GLOBAL event_scheduler ON在 MySQL 8.0 之后的某些版本需要登录用户有SYSTEM_VARIABLES_ADMIN权限普通运维账号默认可能没有。建议直接用 root 或具备该权限的账号执行并且把event_schedulerON写进配置文件保证重启不丢。然后创建扫描事件CREATE EVENT auto_block_weak_accounts ON SCHEDULE EVERY 1 MINUTE COMMENT 自动锁定暴力破解账号 DO BEGIN -- 找出10分钟内失败超过5次的账号写入账号黑名单并锁定 INSERT INTO blacklist_item (biz_code, biz_key, reason, source_app, expire_at) SELECT USER, user_name, auto: too many login failures, mysql_event, DATE_ADD(NOW(), INTERVAL 24 HOUR) FROM login_fail_log WHERE fail_time NOW() - INTERVAL 10 MINUTE GROUP BY user_name HAVING COUNT(*) 5 ON DUPLICATE KEY UPDATE reason VALUES(reason); -- 对已经确认的攻击账号执行锁定 UPDATE mysql.user u JOIN ( SELECT user_name, COUNT(*) c FROM login_fail_log WHERE fail_time NOW() - INTERVAL 10 MINUTE GROUP BY user_name HAVING c 10 ) t ON u.user t.user_name SET u.account_locked Y; FLUSH PRIVILEGES; END这里有两个技术细节需要特别提醒。事件里能否直接改mysql.user可以但事件是以定义者权限执行所以创建事件的账号必须有UPDATE权限作用于mysql.user表以及FLUSH PRIVILEGES权限。8.0 里更推荐用ALTER USER ... ACCOUNT LOCK但事件内部的动态 SQL 写法比较绕直接 UPDATE 表虽然粗暴但确实可行。生产环境慎用直接 UPDATE 系统表因为你不知道 8.0.14 之后的版本会不会校验mysql.user里的其他字段。事件调度的延迟。EVERY 1 MINUTE意味着从攻击开始到账号被封最多有 1 分钟窗口这个窗口对暴力破解来说还是偏大。缩短到 30 秒会好一些但要评估login_fail_log的容量和事件执行开销。实际上连接被拒绝本身的消耗远大于扫描30 秒到 1 分钟是可接受的区间。4.3 触发器在业务黑名单里的另类用法事件调度的周期最长几十秒如果想做到“写入黑名单后立即生效”可以在黑名单表上建触发器同步删除缓存或者记录变更日志。CREATE TRIGGER trg_blacklist_after_insert AFTER INSERT ON blacklist_item FOR EACH ROW BEGIN INSERT INTO blacklist_change_log (biz_code, biz_key, action, change_time) VALUES (NEW.biz_code, NEW.biz_key, INSERT, NOW()); END触发器不能直接操作 Redis但它可以写入一张blacklist_change_log应用程序监听这张表的主键 ID 增量发现有新记录就刷新缓存或删 key。本质上是把数据库表当成消息队列用比在业务代码里每个“写黑名单”的入口都补一段刷缓存逻辑要可靠得多。但触发器有个隐患AFTER INSERT触发器里的 INSERT 如果失败会导致主表插入回滚。比如blacklist_change_log表空间满了前端用户拉黑操作直接失败。我实际遇到过类似事故后来把blacklist_change_log换成了独立的队列表且把表空间预警加到了监控里。触发器是好用的削峰手段但千万不要让它成为主链路的单点依赖。5. 连接层拦截init_connect 黑名单与防火墙配合账号锁定了业务账号业务表拦住了业务用户但还有一个常见的“连接层黑名单”需求某个来源 IP 就是不让他连 MySQL。这正是init_connect的主场但用之前必须先知道它的致命限制。5.1 init_connect 全局拦截init_connect是 MySQL 的一个全局变量用户每次建立连接成功后、正式处理 SQL 之前MySQL 都会执行这个变量里设置的语句。如果语句执行报错连接会被直接中断。SET GLOBAL init_connect CALL mysql_reject_blacklist();为了让黑名单逻辑可以在连接时判断来源我们需要先建一个存储过程DELIMITER $$ CREATE PROCEDURE mysql_reject_blacklist() BEGIN DECLARE v_host VARCHAR(64); DECLARE v_user VARCHAR(64); SET v_user SUBSTRING_INDEX(USER(), , 1); SET v_host SUBSTRING_INDEX(USER(), , -1); IF EXISTS (SELECT 1 FROM blacklist_item WHERE biz_code IP AND biz_key v_host AND status 1 AND (expire_at IS NULL OR expire_at NOW())) THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT Source IP is blacklisted, connection rejected; END IF; IF EXISTS (SELECT 1 FROM blacklist_item WHERE biz_code USER AND biz_key v_user AND status 1 AND (expire_at IS NULL OR expire_at NOW())) THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT Account is blacklisted, connection rejected; END IF; END$$ DELIMITER ;连接建立时USER()会返回类似web_user192.168.1.10拆出后半部分就是来源 IP。这个方案能做到所有普通用户的连接建立时都过一遍黑名单真正实现数据库层的 IP 级拦截。但是init_connect对拥有CONNECTION_ADMIN8.0或SUPER5.7权限的用户不生效。这是安全设计是为了防止管理员被init_connect误伤后连不进去。所以这个方案默认只对业务账号有效DBA 账号和 root 不受影响。这其实是优点防止自己误封管理员。这个方案的性能开销必须说清楚每建立一条连接都会额外执行一次CALL如果是短连接高频场景压力不小。据我实测init_connect里一次简单EXISTS查询大约增加 0.1 到 0.3 毫秒看似不多但连接建立频率每秒几百次时等于多了一台小型数据库的读压力。建议配合连接池使用长连接或者把黑名单表做小、加索引。5.2 防火墙层与数据库层怎么分工我反复强调一个原则能用防火墙解决的不要用数据库解决。云安全组、iptables、firewalld 处理 IP 级封禁既快又无侵入一条规则就能挡掉所有端口。比如封禁单个 IP# firewalld firewall-cmd --permanent --add-rich-rulerule familyipv4 source address192.168.1.66 reject firewall-cmd --reload # iptables iptables -A INPUT -s 192.168.1.66 -p tcp --dport 3306 -j REJECT数据库层init_connect方案适合以下场景你不能控制数据库前面的网络设备比如托管数据库、云数据库实例安全组配置权限没开通黑名单规则需要频繁变动且不想让网络运维介入需要按“账号 IP”组合维度拦截而防火墙只能拦 IP。如果连防火墙都能改建议组合使用防火墙负责挡已经确认的攻击 IP数据库层负责挡业务风控产生的动态黑名单。两者互不替代因为防火墙拦截不会记录“谁在什么时候被挡”数据库层可以留审计信息方便复盘攻击来源。5.3 高危 SQL 的“软黑名单”审计除了来源 IP 和账号还有一种黑名单是对 SQL 规则的匹配比如禁止业务账号执行DROP TABLE、DELETE FROM xxx WHERE 11。MySQL 没有原生的 SQL 黑名单解析器但可以用审计插件实现软拦截。社区版常见的做法是开启audit_log插件把 SQL 记录到审计日志之后用脚本对日志做规则匹配和分析。如果是企业版MySQL Enterprise Audit 支持配置审计规则有audit_log_filter_set_filter之类的命令但没有“直接拒绝高危 SQL”的能力只能记录后告警。所以严格来说“SQL 内容级黑名单”用 MySQL 自身做无论哪个版本都不够好。真要做到禁止执行靠的是数据库账号权限业务账号不授予 DDL 权限自然无法 DROP 表写操作账号和只读账号分离DELETE权限按需授予。权限白名单就是最好的 SQL 黑名单很多团队到处找 SQL 拦截工具的答案其实第一步应该是检查账号权限是不是给大了。6. 真实踩坑与排错手记前面讲了很多方案实际部署时最容易出问题的反而是几个“边角料”细节。我把这些年遇到的高频故障整理成速查表再挑几个典型场景细说一下希望能帮你避开我已经填过的坑。6.1 常见问题速查表问题现象可能原因处理办法执行ALTER USER ... ACCOUNT LOCK后已连接会话还在跑锁定只拦截新连接不断开已有连接配合KILL对应连接用information_schema.processlist查init_connect设置了存储过程但普通用户连接没被执行用户拥有CONNECTION_ADMIN或SUPER权限init_connect对其不生效检查账号权限业务账号不应授予此类管理权限init_connect里存储过程报错业务连接全部中断CALL的目标存储过程不存在或当前用户无EXECUTE权限先CALL手动验证给业务账号授予存储过程EXECUTE权限随时备好一条清空init_connect的恢复命令事件调度器创建成功但从不执行event_schedulerOFF或事件状态不是ENABLEDSHOW VARIABLES LIKE event_schedulerALTER EVENT 事件名 ENABLE黑名单表短时间大量写入业务查询变慢表膨胀、索引碎片、缓存命中率下降对created_at等字段建索引定期归档历史黑名单高频查询走缓存锁定了攻击账号但mysql.user里又出现该账号业务代码或初始化脚本用 root 重建账号清理脚本中的CREATE USER IF NOT EXISTS逻辑账号创建统一走审批Redis 里的黑名单过期了MySQL 里的记录还在TTL 与expire_at未对齐插入 Redis 时 TTL 设为expire_at - now()到点自动删除6.2 锁错管理员账号后的自救这个场景我提一次就够在init_connect里加黑名单逻辑时如果用USER()判断用户但是恰好这个管理员账号没有CONNECTION_ADMIN权限那么新的规则生效后这个管理员也会被拦。等规则上线你自己先连不上了。谁来救你只有 root 能救你但 root 必须通过本地 socket 登录不能走 TCP。如果你把 root 的 host 也限制得很死就只能去物理服务器上恢复。我的建议是养成几个习惯在修改init_connect之前把备份 SQL 写好放在本机SET GLOBAL init_connect ;万一出错用mysql -uroot -p走本地 socket 登录执行。永远保留一个rootlocalhost账号且host是localhost不要改它。它是“跳出黑名单”的逃生门。用init_connect做拦截前先在测试环境用普通账号验证CALL能正常执行再上生产。我在生产上因为忘了给业务账号授权EXECUTE导致所有新连接直接失败这个教训印象太深了。6.3 误封冲击与雪崩黑名单最怕误伤。风控规则太激进把一大批正常用户 ID 写入黑名单瞬间所有订单接口都拒绝业务方电话会被打爆。更可怕的是黑名单表查询量暴增拖垮数据库形成雪崩。要防雪崩我总结了三条经验。第一黑名单规则要有“熔断阈值”。自动封禁任务里加一个统计逻辑单次扫描插入的黑名单数量如果超过历史均值 N 倍就停止写入并告警。宁可不封也不要全封因为攻击最多是损失误封是灾难。第二黑名单写入要有“观察期”。不要第一次失败就立即永久拉黑改成 10 分钟临时拉黑第二次触发再拉黑 24 小时。这样即使误判影响面也是可控的。第三黑名单的发布要可灰度。我可以接受所有节点同时读同一份 Redis 里的黑名单但写入口必须经过某个决策服务而不是每个业务服务自己想写就写。否则你根本不知道黑名单里为什么多了几万个 ID。这三条不能解决所有问题但至少能保证当天的告警可以被处理而不是直接全站瘫痪。6.4 回归一个最小可用的生产组合最后给一个可以直接抄作业的组合方案覆盖我前面讲的三个层级连接层云安全组挡已知攻击 IP数据库层用init_connect 黑名单表做动态 IP/账号拦截适配线上动态风控需求。账号层给每个应用独立账号不授予管理权限离职人员账号走ACCOUNT LOCK停用并定期审计账号清单。业务层blacklist_item表 Redis/本地缓存 变更日志缓存 TTL 与expire_at对齐解封走 Cache Aside 主动失效。这套组合我在多个中等规模项目上验证过既能挡住暴破又不至于给日常维护添太多麻烦。如果你现在的系统连账号权限都是乱的先不要急着上init_connect把每个应用账号的权限梳理成白名单收益比任何黑名单方案都大。权限收得越紧你需要的“黑名单”就越少。我个人在实际操作中的体会是黑名单是例外机制是用来兜底的不是常规安全设计。真正安全的系统依赖的是“默认拒绝”的白名单权限模型。把GRANT给细、账号分清楚、网络层挡干净再配合一两个黑名单机制做兜底这套组合拳比你在数据库里堆一百条黑名单规则都管用。以后有人再问你“MySQL 黑名单怎么做”你可以先问回一句你要挡的到底是 IP是账号还是业务用户答案不同代码完全不同。