多个database是最稳妥的MySQL多租户隔离方式,原生支持库级权限、备份与审计;需精确授权、禁用SHOW DATABASES、防范information_schema信息泄露,并在应用层严格绑定库名。

MySQL 多租户用多个 database 是最稳妥的隔离方式
单库多 schema(比如加 tenant_id 字段)看似省事,但权限、备份、扩缩容、审计全得自己兜底;用多个 database,MySQL 原生支持库级 GRANT、DROP DATABASE 权限控制,天然隔离数据面和权限面。
常见错误现象:Access denied for user 'app_tenant_a'@'%' to database 'tenant_b' 这类报错其实说明隔离生效了——但前提是授权时没手抖把 ALL PRIVILEGES ON *.* 给出去。
- 每个租户对应一个独立
database,命名建议带前缀,如tenant_a_prod、tenant_b_staging - 创建租户账号时,
GRANT必须精确到库,例如:GRANT SELECT, INSERT, UPDATE ON `tenant_a_prod`.* TO 'tenant_a_app'@'%' - 绝对不要给租户账号
USAGE以外的全局权限(如PROCESS、SHOW DATABASES),否则SHOW DATABASES会暴露其他租户库名 - 如果用 MySQL 8.0+,可配合角色(
CREATE ROLE)统一管理权限模板,避免重复GRANT
如何防止租户账号跨库查询或误删
MySQL 默认允许用户看到所有库名(只要没禁掉 SHOW DATABASES 权限),但看不到表结构和数据——前提是权限收得紧。真正危险的是应用层拼接 SQL 时用了硬编码库名,或者配置没做租户绑定。
使用场景:SaaS 后端连接池初始化、ORM 多数据源路由、备份脚本遍历库名。
- 在创建账号后,显式收回
SHOW DATABASES权限:REVOKE SHOW DATABASES ON *.* FROM 'tenant_a_app'@'%' - 应用连接串里必须指定
database参数(如 JDBC 的useSSL=false&database=tenant_a_prod),不能靠USE tenant_a_prod切库 - 禁止在应用代码里用字符串拼接库名,例如
"SELECT * FROM " + tenantDb + ".orders"—— 这等于把库名变成注入点 - 备份脚本若需遍历租户库,应从白名单配置读取库名,而非
SHOW DATABASES动态获取
MySQL 5.7 和 8.0 在租户授权上的关键差异
8.0 的角色机制和动态权限让租户权限管理更清晰,但 5.7 只能靠脚本批量 GRANT,稍不注意就漏收权限或授过头。
参数差异与兼容性影响:
- MySQL 8.0 支持
CREATE ROLE和SET DEFAULT ROLE,可建tenant_reader、tenant_writer角色,再GRANT给具体用户,便于后期统一调整 - MySQL 5.7 不支持角色,必须为每个租户账号单独执行完整
GRANT,且无法用WITH GRANT OPTION下放权限(否则租户能给自己加权限) - 8.0 默认开启
sql_mode = STRICT_TRANS_TABLES,对空值、截断更敏感,租户表结构变更需更谨慎 - 两者都需关闭
skip-show-database(默认已关),否则SHOW DATABASES权限无效
容易被忽略的「安全缝隙」:information_schema 和 performance_schema
即使租户账号只能访问自己的 database,它仍能查 information_schema.TABLES 和 information_schema.STATISTICS,可能推断出其他库存在甚至表结构。
这不是 bug,是 MySQL 设计使然;但对高敏感租户(如金融、医疗),这属于信息泄露风险。
- MySQL 8.0.19+ 可启用
information_schema_stats_expiry = 0并配合FLUSH TABLES控制统计信息时效,但无法隐藏库名 - 真正有效的做法:在代理层(如 ProxySQL、ShardingSphere)或应用网关拦截含
information_schema的查询,或重写为返回空结果 - 不要依赖
REVOKE SELECT ON information_schema.*—— MySQL 不允许收回该库的SELECT权限 - 如果租户需要查自己库的表结构,只允许查
information_schema.TABLES中TABLE_SCHEMA = 'tenant_a_prod'的行,其他一律拒绝
租户隔离真正的难点不在建库授权,而在整个请求生命周期里有没有哪一环悄悄把库名、表名、错误信息、执行计划暴露出去——连 ERROR 1146 (42S02): Table 'tenant_b.orders' doesn't exist 这种报错,都可能被用来反推租户拓扑。










