CakePHP 2.8.4循环执行查询序列出现重复插入问题求助
看起来你遇到了典型的竞争条件问题——虽然你给每次循环套了事务,但两次独立操作(查询A中未在B的记录 + 插入到B)之间存在时间窗口,并发请求会同时读到同一条A的记录,然后都插入到B里,事务隔离级别默认的「读已提交」根本挡不住这种情况。下面给你几个递进的解决方案,从兜底到根治:
1. 先加数据库层面的唯一约束(最稳妥的兜底)
不管代码逻辑怎么调整,直接给表B的关键字段加唯一索引,从数据库层面彻底阻止重复插入。比如假设你的关键字段是unique_key_field:
ALTER TABLE `B` ADD UNIQUE INDEX `unique_key_idx` (`unique_key_field`);
这样哪怕代码层面有漏洞,数据库都会抛出重复键错误,不会产生脏数据。
2. 把查询+插入合并成原子SQL操作(消除时间窗口)
你原来的两步操作(先查后插)是分开的,哪怕在事务里,中间还是有间隙。把它们合并成一条INSERT ... SELECT语句,让数据库在单个原子操作里完成查询和插入,彻底消除竞争窗口。
在你的模型里新增一个方法:
public function insertFromAToB() { $dataSource = $this->getDataSource(); // 替换成你实际需要的字段和条件 $sql = "INSERT INTO `B` (unique_key_field, useful_field1, useful_field2) SELECT A.unique_key_field, A.useful_field1, A.useful_field2 FROM `A` WHERE A.unique_key_field NOT IN (SELECT unique_key_field FROM `B`) LIMIT 1"; // 对应你原来循环每次插入1条的逻辑 return $dataSource->execute($sql); }
然后把原来的循环改成调用这个方法:
for ($i = 0; $i < $number ; $i++){ $dataSource = $this->getDataSource(); try { $dataSource->begin(); $this->insertFromAToB(); $dataSource->commit(); } catch (PDOException $e) { $dataSource->rollback(); // MySQL的唯一约束错误码是1062,其他数据库请对应调整 if ($e->getCode() === 1062) { // 遇到重复,跳过这次循环继续下一个 continue; } // 其他异常正常抛出 throw $e; } }
这种方式下,查询和插入是数据库层面的原子操作,完全不会有并发读取的问题。
3. 调整事务隔离级别(辅助优化)
如果坚持要用原来的两步操作,可以把事务隔离级别改成「可重复读」(CakePHP 2默认是「读已提交」),这样在一个事务里,第一次查询的结果会被锁定,不会看到其他事务插入的新数据。
在开启事务前设置隔离级别:
$dataSource = $this->getDataSource(); // 设置可重复读隔离级别(MySQL支持,其他数据库请对应语法) $dataSource->execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ"); $dataSource->begin(); // 后续的查询和插入操作 $dataSource->commit();
不过注意,「可重复读」只能解决不可重复读,没法完全避免幻读,所以还是不如合并SQL靠谱。
为什么原来的事务没用?
你原来的事务只是保证了单次循环里的「查+插」要么全成要么全败,但没法阻止其他并发事务在你的getRecord和insertRecord之间插入数据——比如两个请求同时执行到getRecord,都查到了同一条A的记录,然后各自插入到B,事务对此无能为力。
内容的提问来源于stack exchange,提问作者Disu

