ThinkPHP 的 lock() 方法仅在事务中有效,锁的是数据库行,需配合 Db::transaction() 使用;lock(true) 为共享锁,lock(false) 为排他锁;乐观锁需手动实现版本号校验与更新。

ThinkPHP 的 lock() 方法到底锁的是什么
它只在查询语句末尾拼接 FOR UPDATE(MySQL)或 WITH (UPDLOCK)(SQL Server),**不开启事务时完全无效**。很多人调了 lock(true) 却发现数据还是被并发改乱,就是因为没手动开启事务。
- 必须搭配
Db::transaction()或$db->startTrans()使用,否则数据库根本不会执行行锁 -
lock(true)是共享锁(LOCK IN SHARE MODE),适合读多写少且需防止脏读的场景;lock(false)是排他锁(默认),写操作前必须用它 - PostgreSQL 不支持原生
lock(),需改用sharedLock()/lockForUpdate()手动构造
乐观锁在 ThinkPHP 里不是靠框架自动实现的
ThinkPHP 没有内置版本号校验机制,所谓“乐观锁”得自己加字段、自己写条件。常见错误是只在 where 里写 ['version' => $oldVersion],却忘了更新时要自增 version。
- 必须为数据表添加
version(整型)或updated_at(时间戳)字段 - 更新前查出当前
version值,更新时用where(['id' => $id, 'version' => $oldVersion])+inc('version') - 检查
Db::update()返回值:等于 1 表示更新成功;等于 0 说明已被别人抢先修改,需重试或报错 - 注意 MySQL 的
UPDATE ... SET version = version + 1是原子操作,但 PHP 层仍要处理失败分支
并发扣减库存时,lock() 和乐观锁的实际性能差异
高并发下,悲观锁会阻塞后续请求,乐观锁则靠重试——但重试本身也吃 CPU 和数据库连接。TP8 的 retry 方法能简化重试逻辑,但别无脑套用。
- 悲观锁适合「修改频率高、冲突概率大」的场景(如秒杀下单);乐观锁适合「读多写少、冲突少」(如用户资料修改)
- 用
lock()时,务必控制事务粒度:只锁必要字段,避免SELECT *,否则可能升级为表锁 - TP 自带的
Db::raw('version = version + 1')写法不能和inc()混用,后者生成的是SET version = version + 1,前者需手写完整 SQL 片段
最容易被忽略的:锁的生命周期和连接复用问题
ThinkPHP 默认使用长连接,但事务未提交或回滚时,锁一直持有,直到连接关闭或超时。线上常出现“锁表卡死”,其实只是某个请求崩溃后没释放事务。
- 所有
lock()操作必须包裹在try...catch中,确保commit()或rollback()必然执行 - 不要在循环里反复调用
lock()查同一批数据,容易形成锁等待链;应先查 ID 列表,再批量加锁 - MySQL 的
innodb_lock_wait_timeout默认 50 秒,TP 不会自动捕获Lock wait timeout exceeded错误,得自己 catchPDOException并判断错误码
事情说清了就结束。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










