updatecounters() 传负数可原子自减,避免竞态;但并发需加事务、严格 where 条件并校验返回行数,否则易超卖或静默失败。

直接用 updateCounters() 传负数就能自减,不用写 SQL 或先查后改;但并发场景下漏掉事务或条件错配,会引发计数错乱甚至超卖。
updateCounters() 自减的正确写法
它本质是原子执行 SET field = field + N,N 为负数即实现自减。不依赖当前值读取,避免竞态。
-
Post::updateAllCounters(['view_count' => -1], ['id' => 100]):对单条记录减 1 -
User::updateAllCounters(['balance' => -50], ['status' => 'active', 'balance' >= 50]):批量减,且带安全条件(余额足够才扣) - 注意第二个参数是 WHERE 条件,不是主键 ID 列表;如需按主键更新多条,得用数组:
['id' => [1, 2, 3]]
别用 save() 或 updateAll() 替代 updateCounters()
这两个方法走的是“读 → 改 → 写”流程,在高并发下极易出错:
-
$model->balance -= 10; $model->save():若两个请求同时读到 balance=100,都会写回 90,实际应为 80 -
User::updateAll(['balance' => 90], ['id' => 1]):硬编码值,无法表达“在当前值基础上减”,完全失去原子性 - 只有
updateCounters()能让数据库自己算:UPDATE user SET balance = balance - 10 WHERE ...
并发扣减必须加事务和条件校验
比如优惠券核销、库存扣减,不能只靠 updateCounters() 就完事。
- 必须包裹在事务里:
Yii::$app->db->beginTransaction()→ 执行updateCounters()→ 检查影响行数 → 成功则commit(),失败则rollback() - WHERE 条件要包含业务约束,例如:
['and', ['coupon_id' => $id], ['status' => 'unused'], ['remaining_count' => 1]],确保只在还有余量时才扣 - 执行后务必检查返回值:
if (Coupon::updateAllCounters(['remaining_count' => -1], $where) === 0) { throw new Exception('库存不足'); }
updateAllCounters() 和 updateCounters() 的区别
前者是静态方法,不加载模型实例,适合批量操作;后者是实例方法,只能作用于已查出的单个模型,实际极少用。
- 推荐统一用
Model::updateAllCounters(),性能更好、语义更清晰 -
$model->updateCounters()内部仍会生成 UPDATE 语句,但会额外触发验证和事件,没必要 - 两者都绕过 AR 验证,不调用
beforeSave(),所以字段合法性得靠数据库约束或手动检查
最常被忽略的一点:updateAllCounters() 不抛异常,也不返回具体错误信息,只返回影响行数。线上一旦 WHERE 条件写错(比如字段名拼错、类型不匹配),它就静默返回 0 —— 看似成功,实则没更新任何数据。上线前务必用日志或断言验证返回值是否符合预期。











