MySQL 8.0从库仅设read_only=1会导致SQL线程复制中断,因该参数不限制SUPER用户且会阻止relay log回放;必须配合super_read_only=1(限制所有用户写入,但允许SQL线程正常执行)、专用只读用户(非root)、显式回收LOCK TABLES等隐式写权限,并通过SET PERSIST持久化配置。

MySQL 8.0 中只读副本(从库)的“只读”不能只靠 read_only=1 就完事——它对 SUPER 用户无效,且会直接阻断 SQL 线程回放 relay log,导致复制中断。真正安全、可自动化的方案是:**用户级权限控制 + 实例级 super_read_only=1 + 显式禁用隐式写操作**。
为什么 read_only=1 在从库上不能单独用
从库开启 read_only=1 后,SQL 线程(负责执行 relay log 中的事件)因默认无 SUPER 权限,会立即报错:ERROR 1792 (HY000): Cannot execute statement in a READ ONLY transaction。这不是配置错了,而是 MySQL 的设计逻辑:SQL 线程必须能写,但普通连接不能写。所以仅设 read_only=1 会导致复制停摆。
实操建议:
- 必须配合
super_read_only=1(MySQL 5.7.8+ 支持),它比read_only更严格:既限制非 SUPER 用户,也限制 SUPER 用户的写操作(除 SQL 线程本身) - 但注意:
super_read_only=1不影响 SQL 线程——这是它被设计出来的根本原因 - 确认已启用:
SELECT @@global.super_read_only;返回1才生效
自动化脚本必须创建专用只读用户,而非复用 root
root 用户默认带 SUPER 权限,即使开了 super_read_only=1,也能通过 SET SESSION super_read_only = 0 绕过(只要没被显式回收权限)。生产环境绝不允许应用或监控直连 root。
正确做法是用初始化脚本自动创建最小权限账号:
-- setup-ro-user.sql(挂载到 /docker-entrypoint-initdb.d/ 下) CREATE USER 'ro_app'@'%' IDENTIFIED WITH mysql_native_password BY 'ro_4ppl1c4t10n!'; GRANT SELECT ON `reporting`.* TO 'ro_app'@'%'; GRANT SELECT ON `analytics`.* TO 'ro_app'@'%'; -- 显式拒绝锁表类操作(避免 SELECT ... FOR UPDATE 被误用) REVOKE LOCK TABLES, PROCESS, SHOW DATABASES ON *.* FROM 'ro_app'@'%'; FLUSH PRIVILEGES;
关键点:
- 不要用
GRANT SELECT ON *.*,防止意外读取mysql或performance_schema敏感系统库 - 显式
REVOKE锁和元数据权限,因为SELECT授权不自动包含它们,但某些客户端会尝试调用(如某些 ORM 的健康检查) - MySQL 8.0 必须先
CREATE USER再GRANT,顺序错会报错ERROR 1410 (42000)
Docker 镜像中自动化配置 super_read_only 的陷阱
在 my.cnf 里写 super_read_only=1 很容易失效——Docker 官方镜像启动时会忽略 [mysqld] 段外的配置,且部分高可用组件(如 Orchestrator、MHA)会在运行时动态关闭它。
可靠做法是:在容器启动后、应用连接前,用健康检查脚本强制设置:
#!/bin/bash # wait-for-sro.sh(作为 entrypoint wrapper) until mysql -h localhost -P 3306 -u root -prootpass -e "SET PERSIST super_read_only = 1;"; do echo "Waiting for MySQL..." sleep 2 done echo "super_read_only enabled." exec "$@"
说明:
- 用
SET PERSIST(MySQL 8.0+)替代SET GLOBAL,它会持久化到mysqld-auto.cnf,重启不丢失 - 不要依赖
my.cnf静态配置,因为 Docker 初始化流程中,mysqld可能先于配置文件就绪 - 若用
SET GLOBAL,必须每次容器重启都重跑,否则失效
验证脚本必须覆盖三类典型失败场景
自动化部署后,光看 SHOW GRANTS 不够。要模拟真实应用行为,验证以下三点是否全部拦截成功:
-
INSERT INTO reporting.users VALUES (1,'test');→ 必须报ERROR 1142 (42000): INSERT command denied -
SELECT * FROM reporting.users LOCK IN SHARE MODE;→ 必须报ERROR 1045 (28000): Access denied for user(因已REVOKE LOCK TABLES) -
SELECT * FROM performance_schema.events_statements_summary_by_digest;→ 若未授权,应报ERROR 1142;若需查,应单独GRANT SELECT到该库,而非开SHOW DATABASES
最容易被忽略的是第三点:很多监控工具默认查 performance_schema,但它不属于用户数据,需要显式授权,且不能靠 SELECT ON *.* 一并授予——必须精确到库表。


















