ThinkPHP多库配置需在database.php中预定义连接名(如'backup_db')并完整配置参数,禁用Db::connect([])动态传参;同步须分片处理、用insertAll批量写入、显式类型转换、持久化checkpoint表。

ThinkPHP 多数据库连接配置写法
ThinkPHP 默认只配一个数据库,要连两个库必须手动在 database.php 里加第二个连接配置,不能靠 Db::connect() 临时传参硬凑——那样每次调用都重新解析配置,开销大还容易连错库。
正确做法是给第二个库起个明确名字,比如 'slave' 或 'log_db',并在配置中完整定义 host、username、database 等字段:
return [
// 主库
'default' => 'mysql',
'connections' => [
'mysql' => [
'type' => 'mysql',
'hostname' => '192.168.1.10',
'database' => 'main_db',
// ...其他主库参数
],
'backup_db' => [ // ← 这个键名就是你后续 connect() 时用的标识
'type' => 'mysql',
'hostname' => '192.168.1.11',
'database' => 'backup_db',
// ...必须独立配全,不能省略 port / charset / prefix
]
]
];- 别用
Db::connect([])动态传配置,它不走连接池,高并发下会快速耗尽 MySQL 连接数 -
backup_db这个连接名必须全局唯一,且不能和已有的default冲突 - 如果两个库字符集不同(比如一个是
utf8mb4,一个是gbk),务必在各自配置里显式写上'charset' => 'utf8mb4',否则中文写入可能乱码或截断
跨库查询与分批写入的边界控制
同步不是把 A 库全查出来再塞进 B 库。内存扛不住,事务也撑不住,更别说锁表风险。核心是「按主键范围分片」或「按时间字段分页」,每次只处理几千条。
假设你要从 user_log 表同步到备份库的同名表,且主键是自增 id:
立即学习“PHP免费学习笔记(深入)”;
$minId = Db::connect('mysql')->table('user_log')->min('id');
$maxId = Db::connect('mysql')->table('user_log')->max('id');
$step = 5000;
<p>for ($start = $minId; $start <= $maxId; $start += $step) {
$end = min($start + $step - 1, $maxId);</p><pre class='brush:php;toolbar:false;'>// 从主库查
$rows = Db::connect('mysql')
->table('user_log')
->where('id', 'between', [$start, $end])
->select();
// 写入备份库(注意:必须用 insertAll,不能 foreach + insert)
if (!empty($rows)) {
Db::connect('backup_db')
->table('user_log')
->insertAll($rows);
}}
- 别用
limit + offset分页——数据在同步过程中可能被删/改,offset会跳行或重复 -
insertAll()比循环insert()快 5–10 倍,且避免单条事务开销;但一次别超 5000 条,MySQL 默认max_allowed_packet可能报Packets larger than max_allowed_packet are not allowed - 如果源表没主键或主键不连续(比如用了 UUID),改用时间字段分片,但得确保该字段有索引,否则
WHERE created_at > ? AND created_at 会全表扫描
同步失败时的幂等与断点续传
网络抖动、目标库临时不可用、唯一键冲突……这些都会让同步中途停下。硬重启脚本会重复写入或漏写,所以必须记录「最后成功同步的 ID 或时间戳」。
最轻量的做法是用一个极小的控制表(比如 sync_checkpoint)存进度:
// 创建控制表(只需一次) CREATE TABLE `sync_checkpoint` ( `task_name` varchar(50) PRIMARY KEY, `last_id` bigint NOT NULL DEFAULT 0, `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB;
同步逻辑里每次成功写完一批,就更新这个值:
Db::connect('mysql')->execute(
"INSERT INTO sync_checkpoint (task_name, last_id) VALUES ('user_log_sync', ?)
ON DUPLICATE KEY UPDATE last_id = VALUES(last_id), updated_at = NOW()",
[$end]
);- 别用文件存 checkpoint——多进程跑时会覆盖;也别用 Redis,没持久化保障
-
ON DUPLICATE KEY UPDATE是关键,避免插入失败后整个同步卡死 - 如果同步任务要支持「重跑某天数据」,控制表里就得加
date_range字段,不能只靠单个last_id
字段映射与类型兼容性问题
两个库表结构看似一样,但字段类型可能差一点:主库用 TINYINT(1) 存布尔,备份库用 BOOLEAN(其实是 TINYINT(1) 别名),看着一样,但某些 TP 版本读出来是 0/1,某些是 true/false,写进去就报错 Data truncated for column 'status'。
最稳的方式是在取数后、写入前做一次显式转换:
$rows = Db::connect('mysql')->table('user_log')->where(...)->select();
foreach ($rows as &$row) {
// 强制转整型,避免布尔值被当成字符串或 null
$row['status'] = (int) $row['status'];
$row['created_at'] = date('Y-m-d H:i:s', (int)$row['created_at']); // 时间戳转格式化字符串
}
Db::connect('backup_db')->table('user_log')->insertAll($rows);- 别依赖 TP 的自动类型转换——它在不同版本、不同 PDO 驱动下行为不一致
- 如果备份库开了严格模式(
STRICT_TRANS_TABLES),NULL写进NOT NULL字段会直接报错,必须提前用isset()或array_key_exists()补默认值 - JSON 字段尤其危险:MySQL 5.7+ 支持原生 JSON 类型,但老版本只能用
TEXT,写入时记得json_encode(),读出时json_decode(),别让 TP 自己猜
跨库同步真正难的不是代码怎么写,而是得时刻想着「两边是不是真的能对上」——字段类型、时区、SQL 模式、甚至 MySQL 的 sql_mode 设置,差一点,insertAll() 就静默丢几条数据,你还以为成功了。











