在yii框架中实现数据库并发一致性需区分读写锁:用forupdate()加行级排他锁配合事务防止“读取→判断→更新”竞态;确保查询走索引避免锁升级;读多写少场景用乐观锁(version字段+staleobjectexception处理);慎用lock tables。

在Yii框架中对数据库记录进行并发查询并确保数据一致性时,必须明确区分读操作与写操作的锁需求——单纯SELECT不加锁无法阻止其他事务修改数据,而错误地使用表级锁又会严重拖慢系统吞吐量。
用forUpdate实现行级排他锁
当你要基于某条记录做“读取→判断→更新”三步操作(如扣减库存),必须在读取阶段就锁定该行,防止其他事务同时修改。
第一步:开启事务 → 执行带forUpdate()的ActiveRecord查询 → 获取对象后立即操作
这一步不能省略事务包裹,否则forUpdate语句在自动提交模式下执行完即释放锁,起不到保护作用。Yii的forUpdate()方法本质是生成SELECT ... FOR UPDATE语句,仅对InnoDB引擎有效。
示例代码中需调用$transaction = Yii::$app->db->beginTransaction(),再执行$query = Product::find()->where(['id' => $id])->forUpdate()->one(),此时若另一事务已持有该行锁,当前请求将阻塞等待,直到innodb_lock_wait_timeout超时或对方释放锁。
【务必在save(false)前完成所有业务逻辑判断】 因为save(false)跳过验证但不跳过乐观锁校验,若你同时启用了optimisticLock(),它会在WHERE条件中追加version = ?,与FOR UPDATE形成双重保障——但前提是version字段值未被其他事务抢先更新。
避免全表扫描导致的隐式锁升级
MySQL在某些条件下会将行锁升级为表锁,最常见诱因是查询未命中索引。
确保WHERE条件中的字段已建立合适索引,尤其是用于forUpdate()查询的主键或唯一键字段。
如果使用非索引字段(如status = 'pending')执行forUpdate(),InnoDB可能对整个聚簇索引加锁,等效于锁表,此时并发能力归零。
用EXPLAIN分析查询执行计划,确认type列为const、eq_ref或range,且key列显示实际使用的索引名称。
读多写少场景下启用乐观锁
适用于编辑商品信息、用户资料等冲突概率低的业务,不适用于秒杀、抢购类高频写场景。
方法一:在数据表中添加BIGINT类型version字段,默认值设为0
方法二:模型类重写optimisticLock()方法,返回字符串'version'
方法三:视图中加入Html::activeHiddenInput($model, 'version'),使提交时携带原始版本号
控制器中捕获StaleObjectException异常,提示用户刷新页面重试——这一步不可跳过,否则前端会静默失败。
【version字段必须为BIGINT且默认0,tinyint或int在高并发下易溢出】
手动执行LOCK TABLES写锁(慎用)
仅在批量导入、日志归档、结构迁移等低频维护操作中考虑,日常业务严禁使用。
第一步:执行Yii::$app->db->createCommand("LOCK TABLES {{%order}} WRITE")->execute()
第二步:执行批量INSERT或UPDATE语句
第三步:执行Yii::$app->db->createCommand("UNLOCK TABLES")->execute()
注意:LOCK TABLES会阻塞所有其他线程对该表的读写,包括SELECT。一旦PHP进程崩溃未执行UNLOCK,锁将持续存在,需DBA介入清除。











