这台机器上只有一个 MySQL 实例,却被 WordPress、二维码平台、在线工具箱同时用着。下面的取舍和踩坑记录,对任何”一个数据库服务多个应用”的场景都适用。
按库隔离,不按表前缀
每个应用一套独立的库,按环境再分:
wordpress_prod / wordpress_beta
qrcode_prod / qrcode_beta
tools_prod / tools_beta
users_prod / users_beta ← 注意这个
用独立库而不是”一个库 + 表名前缀”,好处是备份、迁移、授权都能按库粒度做,某天要把某个应用挪走也不用在一堆表里挑挑拣拣。
为什么用户表要单独一个库
这是我改动最大的一个设计。最初每个应用各自建用户表,结果就是:在二维码平台注册的用户,到工具箱还得再注册一次。
后来把用户表抽出来放进独立的 users_* 库,两个应用连同一个用户表,配合同一把 JWT 密钥,登录态就打通了。
代价是应用侧要维护两个连接池:
getDB() → 连业务库(qrcode_* / tools_*)
getAuthDB() → 连共享用户库(users_*)
多一个连接池不算复杂,但它换来的是”一次注册,全站通用”。对用户来说是天壤之别。
坑一:mysql2 不接受 undefined
这个坑的表现非常迷惑:接口返回 500,但代码看起来毫无问题。
// 报错
await db.query('INSERT INTO t (a, b) VALUES (?, ?)', [foo, bar]);
// bar 是 undefined 时 → 500
原因是 mysql2 的绑定参数只接受 null,传 undefined 会直接抛错。而 JavaScript 里”对象上不存在的属性”返回的就是 undefined,这在拼装请求体时太常见了。
解决办法是在数据库访问层统一做一次转换,而不是在每个调用点小心翼翼:
function toParams(arr) {
return arr.map((v) => (v === undefined ? null : v));
}
在数据访问层收口,比靠人记得写 ?? null 可靠得多。
坑二:连接的时区必须等于实例的时区
这个坑更隐蔽——它不报错,只是读出来的时间整体偏移几个小时。
MySQL 连接如果不显式指定时区,驱动会按自己的默认值解释 TIMESTAMP。容器里默认是 UTC,而你可能下意识按东八区理解,于是所有时间都差 8 小时。
// 线上实例是 UTC,就明确写 Z
createPool({
timezone: 'Z',
// ...
});
判断实例实际时区:
SELECT @@global.time_zone, @@session_time_zone, NOW();
然后让连接池的 timezone 和它一致。不要靠猜。
这个方案的真实风险
必须说清楚:单实例 MySQL 就是单点。它挂了,所有应用一起挂。
对个人项目来说这个风险可以接受,但前提是要做到两件事——定期备份,并且真的演练过一次恢复。没有演练过的备份等于没有备份。