会防脏读,因lock(true)在事务中生成select for update,对主键行加排他锁,阻塞其他事务的写和锁定读,普通select仍读快照不构成脏读;锁失效主因是未开启事务、无索引导致表锁或引擎非innodb。

不会。在 ThinkPHP 5.1 中,使用 lock(true) 配合事务,并**正确开启并保持事务未提交**时,其他并发请求不会读取到脏数据——但前提是:你用的是 SELECT FOR UPDATE(写锁),且查询走的是行级锁路径。
为什么“事务未提交”反而能防脏读?
脏读的本质是:事务 A 读到了事务 B **已修改但尚未提交**的数据。而 lock(true) 的作用不是“让别人读不到当前事务的修改”,而是阻止别人在你事务结束前读到/改到同一行——它靠的是数据库的排他锁(X锁)。
-
lock(true)生成的是SELECT ... FOR UPDATE,这不是普通查询,而是加写锁的读操作; - 该语句执行后,目标行被锁定,其他事务再执行
SELECT ... FOR UPDATE或UPDATE同一行时,会**阻塞等待**,直到你事务commit()或rollback(); - 普通
SELECT(无锁)仍可读取——但读到的是事务开始时的快照(MVCC),不是你未提交的修改,所以不构成脏读。
真正导致脏读的常见错误
不是“事务没提交”,而是锁根本没生效,让多个请求绕过了互斥控制:
- 没调用
startTrans()就直接lock(true)->find():MySQL 当作自动提交语句执行,锁秒级释放,后续请求立刻并发读取旧值; - WHERE 条件未命中主键或唯一索引:InnoDB 升级为间隙锁甚至表锁,看似“锁住了”,实则性能崩盘、逻辑错乱,且可能因锁范围过大引发死锁或误放行;
- 混用模型和 Db 类操作,或跨连接执行:事务指令发给连接 A,SQL 却跑在连接 B 上,锁和更新完全不在一个上下文里;
- 表引擎不是 InnoDB:MyISAM 不支持事务和行锁,
lock(true)形同虚设。
如何验证锁是否真正生效?
别只看 PHP 代码有没有 lock(true),要落到数据库层面确认:
- 执行
EXPLAIN SELECT * FROM goods WHERE id = 123 FOR UPDATE,检查type字段是否为const或eq_ref; - 在事务开启后、未提交前,另起一个会话执行相同
SELECT ... FOR UPDATE,观察是否卡住(show processlist显示Waiting for … lock); - 查
information_schema.INNODB_TRX,确认你的事务状态为RUNNING且有对应锁记录。
安全写法:事务 + 行锁 + 主键查询三要素缺一不可
以下是最小可靠模式(以扣库存为例):
- 确保
goods.id是主键或唯一索引; - 用
Db::transaction()闭包封装,避免手动事务的连接错位风险:
php
Db::transaction(function () use ($id, $num) {
$goods = Db::name('goods')
->where('id', $id)
->lock(true)
->find();
if (!$goods || $goods['stock'] throw new \Exception('库存不足');
}
Db::name('goods')
->where('id', $id)
->update(['stock' => $goods['stock'] - $num]);
});
?>
闭包内抛异常自动回滚,正常结束自动提交,锁由 find() 持有至事务结束,全程行级互斥。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











