必须开启事务:lock(true)仅在db::starttrans()后生效,否则锁立即释放导致超卖;需在事务内完成查询锁定、校验、更新,最后commit或rollback。

ThinkPHP里用lock(true)前必须开事务
不启动事务就调用lock(true),锁根本不会生效。MySQL 的 SELECT ... FOR UPDATE 只在事务中才持有行锁,语句一执行完就释放,等于没锁。
常见错误是只写:Db::name('goods')->where('id', 1)->lock(true)->find(),却忘了Db::startTrans();结果多个请求仍会同时读到库存为 1,全部通过校验后扣减,库存变负。
- 必须先调用
Db::startTrans() - 查询 + 锁定、校验、更新必须在同一个事务块内完成
- 成功后调用
Db::commit(),失败必须Db::rollback() - 别依赖框架自动 commit —— ThinkPHP 不会在控制器结束时自动提交事务
悲观锁和乐观锁怎么选:看冲突概率和性能敏感度
悲观锁(lock(true))适合库存、订单号生成、秒杀资格等“高冲突+强一致性”场景;乐观锁适合用户资料更新、文章阅读数等“读多写少+冲突低”的操作。
乐观锁要自己加字段,比如 version 或 updated_at,然后在更新时带上条件:WHERE id = ? AND version = ?。ThinkPHP 可直接写进 where():
Db::name('goods')
->where(['id' => $id, 'version' => $oldVersion])
->update(['stock' => $newStock, 'version' => $oldVersion + 1]);
如果 affected_rows === 0,说明已被别人抢先更新,此时该重试还是返回失败,得由业务决定。
- 悲观锁吞吐量低,但逻辑简单、结果确定
- 乐观锁无数据库锁等待,但需要处理更新失败的分支
- 别在乐观锁里用
SELECT MAX(id)+1这类逻辑 —— 它本身就不满足原子性
别把setInc()当万能解药
setInc('stock') 看似原子,但它只解决“无条件自增”,不解决“先查再判再扣”的业务逻辑。比如“库存 ≥ 购买数才扣减”,setInc() 做不了判断,强行用会导致超卖。
它底层是 UPDATE ... SET stock = stock + 1,确实避免了 PHP 层读-改-写竞争,但无法替代带业务规则的锁控制。
- 适用于计数器类场景(如阅读数、点赞数)
- 不适用于需前置校验的业务(如库存、余额、状态流转)
- 注意:InnoDB 对
UPDATE ... SET x = x + N是行级锁,但锁的是匹配的行,不是整张表
Redis 分布式锁不能只靠 setnx
直接用 $redis->setnx($key, $value) 加锁,看似简单,但漏掉三个关键点:锁过期、锁误删、值校验。一旦消费者进程卡住或崩溃,锁永远不释放,后续所有请求被阻塞。
正确做法是:加锁时设 TTL(如 10 秒),释放锁必须用 Lua 脚本比对 value 后再删:
eval "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 mutex:order:123 "abc123"
- value 必须是唯一随机串(如
uniqid('', true)),不能是固定字符串或时间戳 - 锁 key 命名要有业务上下文,避免不同模块互相干扰
- 别在 try-catch 里释放锁 —— catch 到异常不代表锁还持有,可能早已过期被别的进程续上了
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











