不行,因连接池复用会导致 schema 串租户;应为每个租户独占 *sql.DB 实例,通过 sync.Map 缓存,DSN 或 search_path 按租户动态设置,并强制校验 tenant_id。

用 sql.DB 连接池做租户级数据库路由行不行?
不行,直接拿一个 sql.DB 实例去“动态切换 schema”是危险的。Go 的 database/sql 包本身不支持运行时切换 schema,SET search_path(PostgreSQL)或 USE database(MySQL)这类语句只对当前连接生效,而连接池里的连接会被复用、不可控——你上一秒在租户 A 的连接上执行了 SET search_path = 'tenant_a',下一秒这个连接可能被另一个 goroutine 拿去查租户 B,结果查到错库。
- 真正安全的做法是:每个租户独占一套
*sql.DB实例(即独立连接池),按租户 ID 查表或查 map 拿到对应实例 - 连接池大小要按租户量级预估,100 个租户配 100 个
*sql.DB,每个池设SetMaxOpenConns(10),比单池设 1000 更稳 - 别用全局
var db *sql.DB然后靠中间件“临时改 schema”,那是并发雷区
PostgreSQL 多 schema 方案里,search_path 怎么设才不串租户?
必须在获取连接后、执行 SQL 前,立刻执行 SET search_path = 'tenant_123',且该连接此后只能服务该租户请求。不能依赖事务开头设一次就一劳永逸——连接归还池前没重置,下个租户拿到就继承了旧 search_path。
- 推荐封装一个
GetTenantDB(tenantID string) *sql.DB,内部用sync.Map缓存已初始化的*sql.DB,key 是租户 ID - 每次
db.Query前不手动 SET,而是用db.Exec("SET search_path = $1", schemaName)+db.Query组合,但得确保这俩操作在同一个连接上——所以得用db.Conn(ctx)显式取连接 - 更稳妥的是:建库时就按租户名建 schema,SQL 里写死
SELECT * FROM tenant_123.users,彻底绕过search_path变量依赖
MySQL 分库 vs 分 schema,sql.Open 的 DSN 怎么动态拼?
MySQL 不支持 schema 切换隔离(不像 PostgreSQL 的 search_path),必须分库。这时不能共用一个 DSN,得为每个租户生成独立 DSN,再调用 sql.Open 初始化 *sql.DB。
- DSN 拼接示例:
user:pass@tcp(127.0.0.1:3306)/tenant_123?parseTime=true,注意末尾的数据库名不是占位符,是真实库名 - 第一次访问新租户时触发
sql.Open,失败要降级或报错,不能 fallback 到默认库——数据混写比服务不可用更严重 - 连接字符串里不要带
multiStatements=true,避免租户 SQL 注入后跨库执行 - 记得调
db.SetConnMaxLifetime和SetMaxIdleConns,不同租户库负载不均时,连接池参数最好也能按租户分级配置
租户 ID 从哪来?HTTP 请求里漏传 X-Tenant-ID 怎么兜底?
不能靠 session 或 cookie 推断租户,必须显式传递。常见错误是中间件里只取 header,但 gRPC 或 CLI 场景没 header;或者用了 JWT 但没校验 tenant_id 字段是否在白名单里。
立即学习“go语言免费学习笔记(深入)”;
- 入口层(HTTP handler / gRPC interceptor)必须校验
X-Tenant-ID或tenant_id参数存在且非空,缺失立即返回400 Bad Request - 绝不允许 fallback 到 “default” 租户库——哪怕只是读配置,也得明确拒绝,否则测试环境跑着跑着就往生产租户库里写数据
- 日志打点必须包含
tenant_id,否则排查时根本分不清是哪个租户的慢查询拖垮了整个连接池
最麻烦的不是怎么路由,而是怎么让所有数据访问路径(包括 migration、admin 工具、后台 job)都强制带上租户上下文。漏掉一个 cron job,就可能全量扫错库。



















