多租户连接需动态路由:启动时缓存租户-DB映射,中间件校验tenant_id白名单,每租户独占连接池;ORM须强制注入tenant_id条件;PostgreSQL schema模式下需重置search_path并禁用连接复用;MySQL分库后禁跨实例事务,改用Saga补偿或TiDB等分布式数据库。
连接时怎么自动路由到对应租户库
多租户环境下,connection 不能写死库名,得在建立连接前动态替换。常见做法是把租户标识(比如 tenant_id 或子域名)从请求上下文里提出来,查配置表或内存映射拿到对应数据库的 host、port、database,再拼成完整 dsn。
容易踩的坑:
– 配置没缓存,每次请求都查一次配置库,拖慢首字节响应;
– 忘记校验 tenant_id 合法性,被恶意构造参数打到其他租户库;
– 连接池没按租户隔离,A 租户的连接被 B 租户复用,导致数据错乱。
实操建议:
– 启动时加载租户-DB 映射到 sync.Map(Go)或 ConcurrentHashMap(Java),带 TTL 自动刷新;
– 中间件层强制校验 tenant_id 是否在白名单内,不在则直接 403;
– 每个租户独占一个连接池,池名带上 tenant_id,避免混用。
ORM 怎么避免跨租户查询漏加 tenant_id 条件
即使库隔离了,如果 ORM 允许跨库 JOIN 或租户共用同一库(共享模式),WHERE 漏写 tenant_id = ? 就会查出别家数据。这不是连接问题,是查询逻辑漏洞。
常见错误现象:
– 测试环境只有一租户,查不到问题;上线后突然出现数据串门;
– 使用 scope(如 Rails 的 default_scope)但没覆盖所有模型,漏掉关联查询。
实操建议:
– 所有查询入口(Controller / Service)必须显式传入 tenant_id,禁止从全局变量读;
– 在 DAO 层封装 FindForTenant()、UpdateForTenant() 等方法,强制注入 tenant_id 条件;
– 数据库开启 sql_mode=STRICT_TRANS_TABLES + 审计日志,定期扫出没带 tenant_id 的 SELECT。
PostgreSQL schema 模式下如何安全切换 search_path
用不同 schema 隔离租户时,靠 SET search_path TO tenant_123 切换上下文。但这个设置是 session 级的,一旦连接被连接池复用,可能残留上一个租户的 search_path。
性能影响:
– 每次查询前都 SET 一次,增加 round-trip;
– 如果用 prepared statement,search_path 变更后需重新 prepare,否则走错 schema。
实操建议:
– 连接归还池前,统一 RESET search_path;
– 在连接获取后、业务逻辑前,立即 SET search_path,并用 pgx.Conn.PgConn().ExecParams()(Go)等底层接口确保执行无延迟;
– 禁用连接池的 connection reuse 超时(如 pgBouncer 的 pool_mode = transaction),改用 session 模式——代价是连接数上升,但安全边界清晰。
MySQL 分库分表后怎么保证事务不跨实例
租户分散在不同 MySQL 实例时,天然不支持跨库事务。如果业务代码写了 BEGIN; UPDATE db_a.users…; UPDATE db_b.orders…; COMMIT;,第二条就会失败或静默降级为自动提交。
错误现象:
– 本地事务看似成功,但部分写入丢失,日志里只有 ERROR 1390 (HY000): Prepared statement contains a reference to a non-existent table 这类模糊报错;
– 使用 ShardingSphere 或 MyCat 代理时,误以为它能兜底分布式事务,实际默认关闭 XA。
实操建议:
– 服务层拆成多个本地事务,用 Saga 模式补偿(例如:先扣库存,再创订单,失败则回调补库存);
– 禁止在应用代码里写跨库 SQL,SQL 审计工具(如 SOAR)加入规则拦截含多个 db_name.table_name 的语句;
– 如果真要强一致,改用 TiDB 或 CockroachDB,但得接受它们对锁和 GC 的额外开销。
最麻烦的不是技术选型,是业务同学总想“顺手 join 一下另一个租户的统计表”——这种需求得卡在 PR 评审环节,不改完不合并。










