ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL read_only 深度拆解:权限边界、复制豁免与运维踩坑

MySQL read_only 深度拆解:权限边界、复制豁免与运维踩坑 干 DBA 这行SET GLOBAL read_only ON;这行命令大概是我手速最快的一条。主从切换要关写、数据库迁移要锁源库、半夜做备份怕业务把数据写飘脑子刚把方案理清楚手指头已经把这行命令敲出去了。但要较真地讲清楚这条命令很多人其实是含糊的read_only 到底拦谁、不拦谁为什么从库开着只读还能追上主库的 binlog为什么有时候开了只读应用还被告知“写入成功”这篇文章就按庖丁解牛的架势把这条命令从权限模型、执行边界、运维场景到踩坑现场里外拆一遍。适合刚开始接触主从复制的 DBA、被线上误写问题折腾过的运维以及想弄明白“只读”和“不能写”到底差在哪里的后端研发。1. 先拆解这条命令到底在干什么1.1 一行命令的三个部分SET GLOBAL read_only ON;可以拆成三段来看。SET GLOBAL表示这是一次全局系统变量设置。它面向整个 MySQL 实例所有连接——不管是这条命令执行之前就建立的老连接还是之后新建的连接——都会受到这个变量状态的影响。它不是一个 session 变量所以如果你手误写成SET read_only ON;MySQL 会直接回你一个错误Variable read_only is a GLOBAL variable and should be set with SET GLOBAL。read_only是 MySQL 自带的系统变量直译就是“只读”。在 5.7、8.0 里它都是动态变量意味着不用重启实例就能通过SET GLOBAL改状态。ON是布尔值等价写法是SET GLOBAL read_only 1;。OFF 或 0 表示关闭只读。每次看到有人在这条命令后面注释“开启数据库只读”我都想补一句它开启的是一种对写入请求做“准入检查”的状态而不是把你的库像 PDF 文件一样整个锁死。说到这里顺便提醒一个高频混淆点read_only和transaction_read_only是两回事。后者也叫tx_read_only是会话级参数控制事务是否默认只读比如START TRANSACTION READ ONLY这类行为它跟服务端能不能接受业务写入没有关系。查到报错时先分清楚能少走很多弯路。1.2 它管住谁、放过谁read_only的拦截规则用一张表能说清楚行为read_onlyON 时是否被拦截普通用户对持久表的 INSERT/UPDATE/DELETE/REPLACE拦截报 ERROR 1290普通用户的 CREATE/DROP/ALTER/TRUNCATE 等持久表 DDL拦截普通用户对 mysql 系统库的写入GRANT、CREATE USER 等拦截具有 SUPER 权限的账号写入不拦复制 SQL 线程从库回放主库 binlog不拦创建临时表、在临时表里写入不拦SELECT、SHOW、EXPLAIN 等只读查询操作不拦第一行最好理解打开只读后普通业务账号的写语句直接会被 ERROR 1290 怼回来。拦截粒度是 statement 级一条 UPDATE 在执行前就被拒掉整条语句不会生效也不会产生 binlog。第二行容易被忽略其实 DDL 比 DML 更需要拦住。CREATE TABLE、ALTER TABLE 这类操作会改写数据字典本质也是写只读状态下同样报 1290。我之前见过有人以为“只读只挡 DML不挡 DDL”结果在只读实例上执行 ALTER TABLE 失败查了半天才发现是自己对只读边界理解有误。SUPER 不拦这个设计本意是给 DBA 留一扇应急门。实例开了只读但出现明显的数据错误需要立刻修复时高权限账号可以不进维护窗口直接处理。但这扇门也是 read_only 最大的风险点后面讲踩坑的时候会重点展开。复制线程不拦是底线逻辑。主从架构里从库常年开着 read_only 是常态binlog 回放线程本身就在做 INSERT/UPDATE如果 read_only 把回放也拦了从库永远追不平主从架构直接崩掉。所以 MySQL 在实现上让复制 SQL 线程拥有豁免权它执行的事件不受只读限制。这也是为什么你会看到只读从库的数据一直在变——这是正常现象不是 read_only 失效。1.3 影响范围立即、全局、不可商量read_onlyON的影响是即时且全局的命令执行成功的下一秒所有连接的下一条写语句就会开始报错不需要重连不需要等缓冲。影响面包括所有普通业务账号、运维平台使用的低权限账号以及临时连上来做数据订正的普通会话。对查询流量则完全无感SELECT 照常走连接数也不会有波动。这里要特别提醒一点运维上不能只看数据库这边。应用对写入失败的处理方式千奇百怪有的把 ERROR 1290 当成“连接坏了”触发连接池重建结果瞬间把实例连接数打满有的在重试逻辑里反复写入几分钟内把错误日志刷爆。所以切只读之前一定要在演练环境里把应用的行为摸清楚。这也是我为什么坚持把 read_only 当作“需要发布公告的变更”来管理而不是随手敲完就走。2. 什么时候会用到 read_only从架构保护到迁移维护2.1 主从架构里的双向保护主从架构里read_only 承担的是“双向保护”。从库这一侧read_only 加上 super_read_only 是双保险。从库不应该接收业务写请求但总有例外某个同事把从库地址当成主库连了上去或者自动化脚本里配错了节点。只靠账号权限管理挡不住配置错误而 read_only 能在实例层直接把这种误写拦住。主库这一侧read_only 是切换流程中最关键的一道闸。标准的主从切换我一般是这么做的先在当前主库执行SET GLOBAL read_only ON;把业务写请求挡在门外。确认目标新主库已经追平GTID 模式下对比gtid_executed非 GTID 模式看Seconds_Behind_Source是否为 0。在新主库上停掉复制、清掉复制信息然后关掉read_only和super_read_only把应用流量切过去。老主库保持read_onlyON。这个状态要一直维持到应用流量彻底切走防止还有残余请求落到老主库上造成双写——这是防“双主脑裂”最简单也最有效的一层闸。有个细节值得说老主库的 read_only 不会拦住复制通道。如果后续需要把老主库重新搭成新主库的从库binlog 一样能正常追上来。只读闸门拦的是应用写入不是数据同步这个特性在切换过程中非常有用。2.2 维护窗口与数据迁移做线上大版本升级、批量数据订正或者跨机房迁移数据时我最依赖的也是 read_only。迁移场景的具体节奏一般是源库开 read_only → 记下当前位点或 GTID → 导出全量数据或开启增量同步 → 在目标库校验行数和抽样数据 → 切流量 → 源库关 read_only 或直接下线。如果少了源库只读这一步校验数据期间业务一写源库和目标库的数据又会产生差异你根本没法判断到底是迁移漏了数据还是校验期间产生了新增量。开了只读之后你手里的每一个校验结果都是可信的排查问题的口径会清晰很多。维护窗口里的用法更纯粹窗口时间有限希望 DBA 看到的、改到的就是最终数据。打开 read_only 之后哪怕业务侧还有漏网的定时任务、漏改的配置在尝试写入也一律被 1290 挡住不会弄脏你的操作现场。2.3 备份与合规要求下的只读保障备份场景里 read_only 不是必需的但要分情况。用mysqldump --single-transaction做逻辑备份时依赖 InnoDB 的 MVCC 就能拿到一致快照不需要关写。可如果是做文件级备份、冷备或者在低版本、混合引擎的环境里一个“无写入窗口”能省掉很多一致性上的麻烦。合规和审计场景用得更多。比如要给外部团队开一个“数据查阅”入口或者给测试环境同步一份生产数据用于验收你不可能逐账号去收写权限直接SET GLOBAL read_only ON;是最干净的做法。开完之后不管是谁、从哪个入口进来都只能读验收结论才站得住脚。我遇到过的审计要求是“某个时间窗口内库内数据零变更”这种要求除了 read_only没有第二种更可靠的实现方式。3. 实操手册怎么开、怎么关、怎么验证3.1 三种设置方式与持久化差别先给出一份可以直接抄的设置对照设置方式生效时机重启后是否保留典型场景SET GLOBAL read_only ON;立即不保留临时维护窗口、切换流程SET PERSIST read_only ON;立即保留写入 mysqld-auto.cnf需要长期固定只读状态MySQL 8.0 推荐my.cnf 中[mysqld] read_only ON重启后保留从库默认配置-- MySQL 8.0立即生效并持久化 SET PERSIST read_only ON; SET PERSIST super_read_only ON; -- 只写配置、不立即生效下次重启才应用 SET PERSIST_ONLY read_only ON;SET PERSIST是 MySQL 8.0 开始支持的它会同时修改运行状态并把配置写入数据目录下的 mysqld-auto.cnf。SET PERSIST_ONLY只写配置不动运行状态适合你预先把“下次重启进入只读”这个变更排进发版窗口的场景。5.7 没有这些能力只能SET GLOBAL加改 my.cnf 两条腿走路。权限方面也提醒一下5.7 设置这个变量需要 SUPERMySQL 8.0 改成了 SYSTEM_VARIABLES_ADMIN。权限不够时你会看到ERROR 1227 (42000): Access denied这时候不是命令语法错了是账号权限不足。3.2 搭配 super_read_only我强烈建议一起开只开 read_only 是不够的因为 SUPER 后门还开着。如果哪个同事拿 root 账号“顺手做个数据修复”写入直接就进去了你精心准备的只读窗口形同虚设。super_read_only是 MySQL 5.7.8 引入的变量专门用来堵 SUPER 后门开启后拥有 SUPER 权限的账号也不能执行写操作复制线程依然豁免。官方语义里super_read_only 只有在 read_only 也开启时才真正生效所以生产环境的正确姿势是两条命令一起下-- 开启先堵后门再关大门 SET GLOBAL super_read_only ON; SET GLOBAL read_only ON; -- 关闭两条都放开 SET GLOBAL read_only OFF; SET GLOBAL super_read_only OFF;开启顺序我建议先 super_read_only 再 read_only因为 super_read_only 单独开着没有拦截效果先开它不会误伤任何写入等 read_only 一开SUPER 后门已经被堵住空窗期最小。关闭时顺序反而无所谓两条都关掉就行。在从库上我更推荐直接把两个变量写进 my.cnf让从库从启动起就是只读状态而不是等故障发生了再靠脚本去补。3.3 一条命令验证是否真的生效设置完别急着走验证要跟上。先看变量状态SHOW GLOBAL VARIABLES LIKE read_only; SHOW GLOBAL VARIABLES LIKE super_read_only; SELECT global.read_only, global.super_read_only;再用一个业务账号实测写入mysql -uapp_user -p -h127.0.0.1 -e INSERT INTO test.t1 VALUES (1); # 期望结果 # ERROR 1290 (HY000): The MySQL server is running with the --read-only option so it cannot execute this statement注意报错文案里说的是“--read-only option”即使你是用SET GLOBAL设的MySQL 也会用启动参数的文案提示这个细节不影响判断但别被它带偏。最后确认一下复制状态是否正常-- MySQL 8.0 语法 SHOW REPLICA STATUS\G -- 5.7 语法 SHOW SLAVE STATUS\G重点看Replica_IO_Running或Slave_IO_Running和Replica_SQL_Running或Slave_SQL_Running两个字段是不是 Yes。如果从库开着只读、复制线程还正常说明配置是对的读保护生效复制豁免也生效。4. 常见误区和踩坑实录4.1 误以为 read_only 能拦下所有写入这是最大的误区。read_only 不拦 SUPER意味着所有能写库的“特权账号”都在它的管辖之外。我经历过一次印象很深的演练主库已经开了只读结果监控系统里一个用于“自动清理过期数据”的账号因为配置了 SUPER 权限在只读状态下照样把一张线上表的历史数据清了。那次之后我把所有能写库的自动化账号重新梳理了一遍操作类账号一律不给 SUPER真正需要 SUPER 的只保留在 DBA 手里从库全部开启 super_read_only。还有一个容易漏的点是定时任务如果数据库里开着事件调度器event_scheduler事件的 DML 是以 definer 身份执行的definer 如果是高权限账号只读状态下一个“心跳表写入”事件依然可能执行成功。这不是 read_only 失效而是 definer 权限绕过了它。切只读之前要么把事件调度器关掉要么把所有事件的 definer 收敛成无 SUPER 账号。4.2 误以为只读库里临时表也不能用read_only 的校验对 TEMPORARY 表是放行的临时表只属于当前会话不会影响持久数据所以服务器允许创建和修改临时表。这本身是设计如此但应用层的感知可能会出问题。我之前排查过一个案例应用在写主表之前先建临时表做数据清洗然后 INSERT ... SELECT 把清洗结果写进主表。只读开启后临时表阶段完全正常应用日志里看不到任何报错直到最后一步写主表才爆 1290。业务方一度以为是数据库“部分只读”其实只是临时表绕过了校验而已。如果你要做的是“绝对无写”的合规窗口连临时表都不能被容忍那靠 read_only 是不够的还需要从账号层面收回CREATE TEMPORARY TABLES权限。记住一句话read_only 不等于没有任何写操作它拦截的是对持久化数据的写。4.3 误以为 read_only 可以暂停复制这是从库维护里最常见的误解。有人在从库上执行SET GLOBAL read_only ON;以为这样就能让从库停止应用主库的 binlog结果数据还在一直变于是以为命令没生效反复执行。实际上 read_only 对复制线程完全没有约束力。想暂停从库追数据正确操作是STOP REPLICA; -- 这里执行备份、维护操作 START REPLICA;注意 8.0 里STOP REPLICA是官方推荐语法5.7 里对应的是STOP SLAVE需要 REPLICATION_SLAVE_ADMIN 或 SUPER 权限。这个误解背后还有一个安全隐患有人开着 read_only 就去做物理备份以为“反正库是只读的文件是安全的”结果备份期间 SQL 线程一直在回放备份文件里的数据点是被撕裂的恢复出来主从不一致。记住 runbook 里的铁律文件级备份之前必须STOP REPLICAread_only 替代不了这个动作。4.4 一次主从切换演练的真实复盘有一次做双节点主从切换演练GTID 模式应用账号是低权限的 app_rw整套流程走下来基本顺利但现场暴露了三个问题值得拿出来讲。第一个问题是切换后仍有一小批连接在报 1290。查了半天发现是应用连接池里还握着指向老主库的旧连接而老主库保持着read_onlyON。这些连接在流量切换之后第一次发写请求直接命中只读校验。解决方案不是关掉老主库的只读而是让应用连接池尽快失效旧连接或者在代理层切换之后做一次应用重启。这个环节不处理好看到的现象会被误判成“新主库没有正确放开写入”。第二个问题在前面踩坑里提过老主库上有个定时事件definer 是高权限账号只读状态下它向心跳表写入了一条记录。当时让现场的同学紧张了一下实际是 definer 权限绕过读保护不是开关失效。从那以后我的切换清单里加了一项切只读前统一检查事件调度器状态。第三个问题是切换结束后脚本没有自动验证新主库的read_only确实为 OFF结果过了几分钟才发现新主库的自动化配置里被写入了一个只读参数应用开始报错。现在我在切换脚本末尾强制加了两步查global.read_only和global.super_read_only再用业务账号做一次真实写入。宁可慢几秒不能带着错误状态上线。5. 快查速记与调试清单5.1 常见问题速查表把我在一线遇到的高频问题整理成一张表遇到情况直接对着查现象排查思路处理建议应用突然报 ERROR 1290确认实例 read_only 是否被打开检查是否有自动化平台误操作属于计划内窗口就通知应用等待非计划内则评估后关闭只读只读已开启但数据仍在变化区分是复制线程回放正常、SUPER 账号写入、事件任务写入、还是临时表操作从库开 super_read_only收敛高权限账号检查事件 definer重启后 read_only 丢失用的是 SET GLOBAL未持久化改用 SET PERSIST或在 my.cnf 中固定从库开只读后数据还在涨复制线程豁免只读正常现象需要停止追数时使用 STOP REPLICA执行 SET GLOBAL 报 ERROR 1227当前账号权限不足5.7 需要 SUPER8.0 需要 SYSTEM_VARIABLES_ADMIN关闭只读后应用仍写不进去检查 super_read_only 是否也关掉了检查应用是否还连着老节点两条都关掉刷新应用连接池5.2 我平时写自动化脚本用的最小模板最后分享一个我自己在用的精简模板可以当作切换、演练脚本的底子#!/bin/bash # 只读模式控制脚本用法 ./readonly.sh on|off MYSQLmysql -uroot -p${MYSQL_PWD} -h127.0.0.1 case $1 in on) $MYSQL -e SET GLOBAL super_read_only ON; SET GLOBAL read_only ON; ;; off) $MYSQL -e SET GLOBAL read_only OFF; SET GLOBAL super_read_only OFF; ;; *) echo Usage: $0 on|off exit 1 ;; esac # 验证并输出当前状态 $MYSQL -e SELECT global.read_only AS read_only, global.super_read_only AS super_read_only;这个模板看起来简单但有几处是有意为之的开启时先 super_read_only 再 read_only把特权账号的空窗期压到最小关闭时直接两条都放开避免半吊子状态最后强制输出当前值让人一眼确认结果。如果实例是 MySQL 8.0 并且希望重启后保持状态把SET GLOBAL换成SET PERSIST即可其余逻辑不变。最后说一个我个人的习惯每次切完只读我都会先执行一遍状态查询再顺手用业务账号跑一条插入测试。别嫌这几秒钟麻烦这条命令提供的确定性和安全感完全值得这个成本。数据库的读写切换往往发生在最容易出错的时间点越是看似简单的命令越要用流程把它钉死。
返回列表