yii2中forupdate()实现悲观锁必须在显式事务内使用,否则无效;需统一加锁顺序、批量锁定、设置合理超时与重试机制,并结合redis分布式锁应对高并发秒杀场景。

Yii2 查询构造器本身不直接支持 lock(如 FOR UPDATE),但可通过 ActiveRecord 或原生 Query 构建器配合事务显式调用 forUpdate() 实现悲观锁。关键不在“怎么写”,而在“怎么用才不引发死锁”——尤其在高并发库存扣减场景下,错误使用会直接导致超卖或大面积阻塞。
forUpdate() 必须包裹在事务中
forUpdate() 不是独立语句,它只是为 SQL 添加 FOR UPDATE 子句的标记。若脱离事务上下文,MySQL 会忽略该标记,锁根本不会生效。
- ✅ 正确写法:先开启事务,再执行带
forUpdate()的查询 - ❌ 错误写法:
Product::find()->where(['id'=>1])->forUpdate()->one()单独调用,无事务,等于没锁 - 事务必须由
$transaction = Yii::$app->db->beginTransaction()显式启动,并在成功后commit(),失败时rollBack()
加锁顺序统一 + 批量锁定减少锁粒度
多商品、多步骤(如扣库存+写订单+减余额)操作时,不同事务以不同顺序访问表,是死锁最常见根源。
- 所有涉及关联更新的业务,必须约定全局加锁顺序,例如固定为:product → order → user_account
- 避免逐个查再逐个锁(如循环里反复
findOne() + forUpdate()),应先收集 ID 列表,再一次性SELECT ... WHERE id IN (...) FOR UPDATE - 批量锁定能显著降低锁数量与持有时间,也便于 MySQL 优化锁管理
设置合理锁等待超时 + 应用层重试
MySQL 默认 innodb_lock_wait_timeout=50 秒,线上业务不可接受。阻塞太久不仅影响用户体验,还会堆积连接、拖垮数据库。
- MySQL 层建议设为
5~10秒:SET GLOBAL innodb_lock_wait_timeout = 5; - PHP 层必须手动实现重试逻辑:捕获
Exception中的错误码1213(Deadlock) - 最多重试 3 次,每次间隔
usleep(50000)(50ms),错开竞争窗口,防止雪崩式重试 - 注意:Yii2 的
save()、updateAll()等方法均不自动重试,必须自行封装
悲观锁不适合单独用于秒杀类场景
单纯依赖数据库行锁应对瞬时万级请求,会迅速触发锁争用、超时、死锁,系统吞吐急剧下降。
- 高并发库存扣减推荐组合策略:Redis 分布式锁(入口限流) + 数据库悲观锁(最终一致性校验)
- Redis 锁控制请求进入节奏(如每秒最多 500 个请求进 DB),避免数据库被压垮
- DB 层仍保留
forUpdate()+ 事务,用于兜底校验库存是否真实充足,防止 Redis 和 DB 状态不一致 - 乐观锁(
optimisticLock())在此类场景下不适用——冲突率极高,大量请求走到save()阶段才失败,用户体验差且无效消耗资源











