默认不会自动回滚,需手动try/catch捕获throwable并显式调用db::rollback(),否则事务挂起;验证不能替代唯一索引,重试须新建模型实例并限制次数。

ThinkPHP 模型 save() 报 Integrity constraint violation 时事务不自动回滚?
默认不会。ThinkPHP 的 save() 或 insert() 执行失败,只抛出异常,但事务是否回滚取决于你有没有捕获并主动调用 rollback() —— 框架不会替你做这个决定。
常见错误现象:在 Db::transaction() 匿名函数里插入重复唯一字段,报 SQLSTATE[23000]: Integrity constraint violation,但后续语句仍执行,数据部分写入。
- 必须手动
try/catch捕获Throwable(不能只 catchException,PDO 异常可能是Error子类) - catch 中必须显式调用
Db::rollback(),否则事务挂起直到超时或脚本结束 - 模型操作需确保走的是同一个数据库连接(避免模型用默认连接、手动 Db 用另一个连接)
用 unique 验证规则 + startTrans() 能预防冲突吗?
不能。验证只查一次,查完到真正 save() 之间存在时间窗口,高并发下照样撞车。验证是业务层防护,不是数据库约束替代品。
使用场景:适合低频、管理后台等对一致性要求不苛刻的写入;不适合秒杀、注册、订单号生成等强唯一性场景。
-
validate(['name' => 'require|unique:user,name'])是“尽力而为”,不是原子保证 - 验证通过后,
UserModel::create()仍可能因并发插入触发唯一索引报错 - 若真要靠验证兜底,得配合
SELECT ... FOR UPDATE(但 ThinkPHP 模型不直接支持行锁语法)
唯一索引冲突后怎么安全重试?
核心思路:把“冲突 → 修正 → 重试”逻辑包进事务内,且每次重试前清掉上一轮残留状态(比如模型实例的主键、更新时间等)。
参数差异:不要复用同一个模型对象反复 save(),它内部会根据是否有主键走 update 或 insert,容易误更新。
- 每次重试都新建模型实例:
$user = new UserModel(); $user->data([...]) - 用
insertGetId()替代save(),明确表达“只插不更”,失败即退出 - 重试次数建议限制在 3 次内,避免死循环;超过则抛出业务异常(如
UserCreateRetryExhausted)
for ($i = 0; $i insertGetId(['username' => $name, 'email' => $email]);
Db::commit();
return $id;
} catch (\PDOException $e) {
if (strpos($e->getMessage(), '23000') !== false) {
Db::rollback();
// 可在此处微调字段,如追加随机后缀
$name = $baseName . '_' . mt_rand(100, 999);
continue;
}
throw $e;
}
}
为什么用 REPLACE INTO 或 INSERT ... ON DUPLICATE KEY UPDATE 不推荐?
ThinkPHP 原生不支持这些语法的链式构造,硬写原生 SQL 会绕过模型事件、自动时间戳、类型转换等机制,维护成本陡增。
性能影响:看似一条语句搞定,但 ON DUPLICATE KEY UPDATE 在冲突时实际执行了 delete + insert(InnoDB 下),还可能触发额外的触发器或外键检查。
- 除非你明确需要“存在就更新”的语义,否则唯一索引冲突应视为错误,而非流程分支
- 用原生 SQL 时,
Db::execute()返回值不是自增 ID,需额外SELECT LAST_INSERT_ID() - 事务中混用模型方法和原生 SQL,要注意连接和事务上下文是否一致
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










