只读账号必须显式禁用所有写权限,不能依赖角色继承;正确做法是先删除旧账号,再创建并仅授予select权限,最后用revoke明确撤销insert、update、delete、drop、create、alter、index、lock tables等权限,并限制ip范围、执行flush privileges。

只读账号必须显式禁用写权限,不能只靠角色继承
MySQL 的 SELECT 权限本身不排斥写操作,哪怕只授予 SELECT,如果用户还拥有 USAGE 以外的隐式权限(比如通过 mysql.role_edges 继承了高权限角色),仍可能执行 INSERT 或 DROP。真实线上出过只读账号删库的事故,根源就是没清空其他权限。
正确做法是:先用 DROP USER IF EXISTS 彻底删除旧账号,再用 CREATE USER 显式指定认证插件和密码强度,最后用 GRANT SELECT + REVOKE 组合强制剥离所有非读权限:
CREATE USER 'app_ro'@'192.168.10.%' IDENTIFIED WITH caching_sha2_password BY 'strong-pass-2024'; GRANT SELECT ON `app_db`.* TO 'app_ro'@'192.168.10.%'; REVOKE INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, INDEX, LOCK TABLES ON `app_db`.* FROM 'app_ro'@'192.168.10.%'; FLUSH PRIVILEGES;
-
REVOKE必须明确列出所有写类权限,不能省略LOCK TABLES—— 它允许阻塞读操作,破坏读写分离语义 - 主机段用
192.168.10.%而非%,避免从任意 IP 连入只读实例 - 务必执行
FLUSH PRIVILEGES,否则权限变更不生效(尤其在跳过 grant 表启动时)
读写账号要限制连接来源且禁用 SUPER 权限
读写账号不是“全能管理员”,它只负责业务 DML/DDL,不能执行 KILL、SET GLOBAL 或修改系统变量。很多团队误授 SUPER 导致应用账号能动态关掉从库复制线程,引发主从延迟雪崩。
创建时需分三步隔离风险:
CREATE USER 'app_rw'@'192.168.10.50' IDENTIFIED BY 'rw-pass-2024'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON `app_db`.* TO 'app_rw'@'192.168.10.50'; REVOKE SUPER, REPLICATION CLIENT, REPLICATION SLAVE ON *.* FROM 'app_rw'@'192.168.10.50';
- 主机地址精确到应用服务器 IP(如
192.168.10.50),禁止通配符——读写流量必须来自可信中间件或应用节点 - 显式
REVOKE SUPER,即使创建时没授也要 revoke,因为 MySQL 8.0+ 默认角色可能带该权限 - 不授
GRANT OPTION,防止账号自行扩散权限
主从实例间权限表不同步是常见故障点
MySQL 主从复制默认不复制 mysql 库,所以你在主库上执行 CREATE USER,从库不会自动创建同名账号。结果就是:读写账号连主库成功,但读请求路由到从库时直接报 Access denied for user。
解决方法只有两个,没有第三种:
- 手动在每个从库执行完全相同的
CREATE USER和GRANT语句(适合从库数量 ≤ 3) - 启用
replicate_replica_tables = ON(MySQL 8.0.22+),并在主库开启binlog_row_metadata = FULL,让权限变更进入 binlog(注意:这会增加主库负载,且要求所有从库版本 ≥ 8.0.22)
检查是否同步到位,登录从库运行:SELECT User,Host FROM mysql.user WHERE User IN ('app_ro','app_rw'); —— 结果必须与主库一致。
应用层连接串必须匹配账号权限边界
权限配得再严,如果应用用 app_rw 账号连从库,或者用 app_ro 连主库,读写分离就形同虚设。中间件(如 MyCat、ShardingSphere)或 ORM(如 SQLAlchemy 的 read_only=True)配置错误是高频原因。
验证方式很简单:在从库上抓包或查 performance_schema.threads:
SELECT PROCESSLIST_USER, PROCESSLIST_HOST, PROCESSLIST_DB, PROCESSLIST_COMMAND
FROM performance_schema.threads
WHERE PROCESSLIST_USER IN ('app_ro', 'app_rw') AND PROCESSLIST_COMMAND != 'Sleep';
- 若看到
app_rw出现在从库的查询中,立刻检查中间件路由规则 - 若
app_ro在主库执行了UPDATE,说明应用代码绕过了读写分离逻辑(比如用了 raw SQL 或事务内混用) - 注意:MySQL 8.0.22+ 的
read_only=ON对 super 用户无效,所以app_rw即使连了从库,只要没被REVOKE写权限,仍可能成功执行 DML —— 这正是为什么权限必须双保险(实例级 + 账号级)
最易被忽略的是事务场景:一个 BEGIN 后的读请求,会被路由到主库,此时若应用未显式关闭事务,后续所有读都走主库,导致只读账号在主库执行 SELECT 没问题,但权限设计初衷已被打破。











