ThinkPHP中lock(true)是InnoDB行锁,非表锁;必须在事务内使用才生效,否则立即释放;显式LOCK TABLES应避免在业务中出现,因其会阻塞全表读写引发雪崩。

ThinkPHP 事务里用 lock(true) 是行锁,不是表锁;显式 LOCK TABLES 几乎不该在业务代码里出现。
为什么 lock(true) 不等于锁表
很多人看到“锁”就默认是整张表被堵死,其实 lock(true) 底层发的是 SELECT ... FOR UPDATE,只锁定 WHERE 条件命中的行(InnoDB 行级锁)。只要查询带了有效索引,其他 ID 或范围的记录完全不受影响。而 LOCK TABLES xxx WRITE 是 MySQL 层面的 DDL 级别操作,会阻塞所有对该表的读写请求,哪怕只是查一条不相关的记录也会排队——这在 Web 请求场景下极易引发雪崩。
常见错误现象:SQLSTATE[HY000]: General error: 1205 Deadlock found when trying to get lock 或接口响应突然卡在 30 秒超时,往往是因为误用了全局锁或没释放。
-
lock(true)必须在事务内调用才有效,单独执行完就释放锁 - 若 WHERE 条件没走索引,InnoDB 会升级为表级扫描锁,效果等同于锁表
- 模型查询如
User::where('id', 1)->lock(true)->find()比Db::table('user')->where(...)->lock(true)->select()更可靠,因前者默认复用事务连接
Db::transaction() 里怎么安全加行锁
事务回调里用 lock(true) 是标准做法,但顺序和范围容易出错。核心原则:所有读取+修改必须在同一个事务闭包内完成,不能先在外面查一遍再传进回调——否则可能读到旧值,导致超扣、重复下单等问题。
立即学习“PHP免费学习笔记(深入)”;
正确示例:
Db::transaction(function () use ($orderData) {
// ✅ 在事务内查并加锁
$user = User::where('id', $orderData['user_id'])->lock(true)->find();
if (!$user || $user->balance < $orderData['amount']) {
throw new Exception('余额不足');
}
// ✅ 扣减和创建订单都在同一事务中
$user->dec('balance', $orderData['amount']);
Order::create($orderData);
});
- 不要在事务外做
User::find()再把对象传入回调——对象状态可能已过期 - 批量操作时,
WHERE id IN (1,3,2)的实际加锁顺序由 MySQL B+ 树扫描决定,建议提前sort()ID 数组或加ORDER BY id强制顺序 - 如果事务里混了 HTTP 请求(比如调支付回调)、Redis 操作或大文件写入,锁持有时间会被拉长,死锁概率指数上升
死锁报错 SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock 怎么处理
TP 的 Db::transaction() 不捕获也不重试死锁异常,它只做基础回滚。这个错误是 MySQL 主动终止冲突事务的结果,必须由业务层显式应对。
- 捕获条件必须精确:用
str_contains($e->getMessage(), 'Deadlock'),不能只 catchPDOException - 重试必须带延迟,例如
usleep(50000 * ($i + 1)),否则瞬间重试大概率再次撞上同一把锁 - 最大重试次数建议设为 3,超过就抛原异常,避免无限循环加重数据库压力
- 隔离级别影响极大:ThinkPHP 默认
REPEATABLE READ启用间隙锁,容易扩大锁范围;生产环境应统一设为READ COMMITTED,在database.php的'params'中加:PDO::ATTR_INIT_COMMAND => "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"
哪些操作会让事务锁住更久、更容易死锁
死锁不是锁本身的问题,而是事务生命周期被意外拉长。很多“高并发死锁”其实源于低效代码混进了事务体。
- 调用外部 API(如短信、支付回调):网络延迟不可控,锁可能持有一两秒
- 在事务里做 Excel 导出或大数组循环处理:CPU/IO 耗时与数据库无关,但锁一直挂着
- 嵌套调用另一个也开启事务的方法:ThinkPHP 不支持真嵌套事务,外层
startTrans()可能被内层覆盖,导致回滚失效 - 用
Db::query()手动 SQL 查询,却没指定连接实例,可能拿到非事务连接,造成读写不一致
真正需要锁的,通常只有“读取校验 + 更新”这两步;发通知、写日志、更新缓存这些,一律移出事务块。



















