MySQL 8.4 升级踩坑:列级权限与表级权限混用,导致新增表级权限对长连接不生效
在将 MySQL 从旧版本升级到 8.4 后,我们遇到了一次由权限管理引发的线上事故:账号同时存在列级权限和表级权限时,新增的表级 INSERT 权限对已经建立的长连接(TCP 长连接)不生效,直到重启使用该连接的服务、重建连接后才恢复正常。短连接(如 Cron 定时任务)则不受影响,因为每次执行都会重新建立连接。
本文记录事故的发现过程、排查思路、根因分析,以及最终采用的治标和治本方案,供同样在做 8.4 升级评估的团队参考。
说明:截至本文撰写时,我们没有在 Oracle 官方 bugs.mysql.com 和 8.4 Release Notes 中找到与本文现象完全吻合、且被官方明确确认的 bug 记录。下文"根因分析"部分是基于 MySQL 权限校验机制做出的技术推断,并非官方定论。我们已按复现步骤向官方提交 bug 报告,等待确认。
背景
- 数据库版本:从 MySQL 8.0.x 升级到 8.4.x(LTS)
- 升级前,业务账号的权限历史上是列级权限和表级权限混用的:部分表只授予了列级权限(如
GRANT INSERT (col1, col2) ON db.table_a TO user),部分表授予的是表级权限(如GRANT INSERT ON db.table_b TO user) - 升级后新增业务功能,需要给同一个账号新增一张表的 INSERT 权限
事故时间线
| 时间 | 事件 |
|---|---|
| 2026-08-17 16:30 | 开启"提额资格推送"新任务,发现新增表 tp_card_temp_spend_limit_record 没有 INSERT 权限,反馈给 DBA(Turbo) |
| 2026-08-18 15:31 | Turbo 反馈权限已授予;重启 Cron 服务后,tp_card_temp_spend_limit_record 写入成功 |
| 2026-08-19 12:00 | 业务方反馈订单消费失败,排查发现 Http 服务中另一张一直存在的表 tp_vendor_transaction 也因为权限问题 INSERT 失败;重启 Http 服务后立即恢复 |
两个关键细节:
tp_card_temp_spend_limit_record只在 Cron 服务里写入,Cron 是短连接(每次执行都新建连接),所以授权后没有重启也表现正常——这一度让人误以为"权限已经生效了,没必要重启 Http"。tp_vendor_transaction是一张早就存在的表,一直被 Http 服务使用,Http 服务用的是长连接(TCP 长连接池)。这张表的权限其实在同一批操作里也被误动过(列级权限迁移到表级权限的过程中受到影响),但因为长连接不会主动刷新权限缓存,问题被隐藏到第二天才在 Http 侧暴露出来。
也就是说,真正的根因不是"哪张表没权限",而是长连接没有感知到新的 GRANT,这个问题被 Cron 短连接"意外掩盖"了一天,才在 Http 长连接上集中爆发。
直接原因
排查结论:MySQL 8.4 版本下,账号列级权限存在 Bug,可能导致同一账号新增的表级权限对已建立的长连接不生效。
处理方式:
- 把该账号之前所有的列级权限全部回收(
REVOKE ... (col1, col2) ON ... FROM user) - 按表级重新统一授权(
GRANT ... ON db.table TO user),做到同一账号下权限口径统一,不再混用列级和表级 - 由于部分业务使用 MySQL 的 TCP 长连接,仅执行 REVOKE/GRANT 不会让已有连接感知变化,必须重启相关服务、重建连接后权限才真正生效
根因分析(技术推断,非官方结论)
MySQL 官方文档《8.2.13 When Privilege Changes Take Effect》中写明:
Table and column privilege changes take effect with the client's next request.
也就是说,理论上表级和列级权限变更不需要重启连接,客户端下一次请求就应该生效。但我们的实际现象与文档承诺不符——这正是问题的核心矛盾点。
MySQL 8.0 引入了 Acl_map 权限缓存结构,用来加速每次请求的权限校验:每个账号在会话建立/权限变更时,会构建一份该账号的权限快照(表级、列级、库级的位图信息)放在内存中,后续请求直接查这份快照,避免每次都扫描 mysql.tables_priv、mysql.columns_priv 等授权表。
我们推测(尚待官方确认):当一个账号同时存在列级权限记录和表级权限记录时,围绕这份权限快照的合并/重建逻辑存在边界条件问题——具体表现为:
- 对于全新的表(此前该账号在这张表上从未有过任何权限记录,无论列级还是表级),新增的表级 GRANT 在已经建立的长连接上没有触发权限快照的正确刷新;
- 而短连接因为每次都重新握手、重新构建权限快照,所以能立即拿到最新权限,表现"正常";
- 这就解释了为什么 Cron(短连接)授权后马上生效,而 Http(长连接池)授权后要等到重启服务、连接池重建后才生效。
换句话说:这不是权限表数据错了,而是账号的权限快照在长连接场景下没有被正确地标记为"过期并重建"。而列级权限和表级权限混用,很可能是触发这个边界条件的必要条件之一——这也是我们后续把所有列级权限统一收编为表级权限之后,同类问题没有再复现的原因(目前是经验观察,未做穷举验证)。
治标方案
- 紧急止血:重启 Http 服务,重建长连接,恢复业务
- 权限收敛:将受影响账号的列级权限全部 REVOKE,统一改为表级授权,避免同一账号下列级/表级权限混用
- 流程补充:后续任何新增表授权,DBA 授权完成后,必须同步通知所有使用该账号的服务方重启(或提供刷新连接池的手段),不能默认"授权即生效,业务无感"
治本建议
- 权限模型规范化:新架构下,同一账号尽量只用表级权限,避免列级权限和表级权限混用。确需列级权限做敏感字段隔离的场景,单独建专用账号,不与普通表级权限账号共用
- 授权流程加检查项:DBA 在新表授权前,先检查目标账号是否存在列级权限记录;如有,先与业务方确认是否可以统一收编为表级权限
- 连接池侧增加保护:评估给核心长连接服务增加"连接最大存活时间"(如 max connection lifetime),定期强制重建连接,即使权限变更通知有遗漏,也能在有限时间窗口内自愈,而不是无限期卡住
- 向 Oracle 提交 Bug:整理最小复现步骤(账号先有列级权限 → 新增表授予表级权限 → 已有长连接不刷新),提交至 bugs.mysql.com,推动官方确认并给出修复版本
- 升级前的权限体检:后续团队做大版本升级(如后续 8.4 → 9.x)前,先跑一遍权限审计,识别并清理历史遗留的列级/表级混用账号,降低升级后触发同类问题的概率
复现步骤(用于提交官方 Bug / 内部验证)
-- 1. 账号已有列级权限(历史遗留)
GRANT SELECT (col1, col2) ON db.table_a TO 'user1'@'%';
-- 2. 用该账号建立一个长连接并保持存活(如放入连接池)
-- 3. 新建一张表,并给同一账号授予表级权限
CREATE TABLE db.table_b (...);
GRANT INSERT ON db.table_b TO 'user1'@'%';
-- 4. 在步骤 2 中已建立的长连接上执行
INSERT INTO db.table_b (...) VALUES (...);
-- 现象:ERROR 1142 (42000): INSERT command denied to user 'user1'@'%' for table 'table_b'
-- 5. 用该账号新开一个连接,执行同样的 INSERT
-- 现象:立即成功
-- 6. 重启步骤 2 中的服务/连接池,强制重建连接后重试步骤 4
-- 现象:恢复成功小结
这次事故本质上是一次权限变更的生效边界被现有连接机制放大的问题:短连接掩盖了问题的存在,长连接暴露了问题的影响面。表面上看是"MySQL 8.4 的 bug",但更深层的教训是:任何依赖账号级权限缓存的系统,在做权限变更时都不能假设所有客户端都能立即感知变化,尤其是在有大量长连接、连接池的架构里,权限变更后是否需要重启/刷新连接,应该成为授权 SOP 的标准步骤之一,而不是等出了事故才去验证。
0 评论