mysql乐观锁更新失败时affected_rows为0,需通过$mysqli->affected_rows或$stmt->rowcount()判断;重试前必须重新select获取最新version/updated_at,推荐指数退避延时且最多3–5次,关键业务应避免盲目重试。

更新失败时 affected_rows 为 0 怎么判断
MySQL 乐观锁通常靠 WHERE version = ? 或 WHERE updated_at = ? 实现,更新失败不会报错,只是 affected_rows 返回 0。很多人卡在这一步——以为 SQL 执行了就成功了,结果数据没变、业务逻辑还往下走。
- 用 MySQLi 时检查
$mysqli->affected_rows;PDO 则看$stmt->rowCount() - 不要只依赖异常捕获,
UPDATE成功但影响行数为 0 就是典型的乐观锁冲突 - 注意:某些 ORM(如 Laravel Eloquent)的
update()方法默认返回布尔值,需手动查affectedRows或启用returning模式
重试逻辑里要不要 sleep
盲目重试会放大数据库压力,尤其在高并发场景下,所有线程同时读-改-写,大概率集体撞墙。加短延时不是“等运气”,而是错峰 + 给其他事务释放锁的时间窗口。
- 推荐指数退避:
usleep(100 * pow(2, $retryCount)),最多重试 3–5 次 - 避免固定 sleep(100),否则容易形成“重试节拍同步”,反而加剧冲突
- 如果业务允许,首次失败后直接返回
409 Conflict让前端刷新再试,比服务端死磕更轻量
重试前必须重新 SELECT 吗
必须。乐观锁的本质是“基于旧状态做校验”,重试时若还拿着第一次查出来的 version 或 updated_at,等于拿过期快照去比对,必然再失败。
- 每次重试前,都要执行一次
SELECT ... FOR UPDATE(或至少SELECT当前行最新值) - 如果用了
SELECT ... FOR UPDATE,注意它本身会加行锁,别在事务外滥用,否则可能引发死锁 - 部分场景可改用无锁方案:比如用
INSERT ... ON DUPLICATE KEY UPDATE替代UPDATE,避开版本比对逻辑
哪些操作不适合用乐观锁重试
不是所有更新都适合套重试模板。比如涉及金额扣减、库存预占、幂等性敏感的操作,重复执行可能引发资损或超卖。
- 账户余额变更、订单状态跃迁(如
created → paid)、分布式唯一号生成等,应优先用悲观锁或状态机 + 唯一索引约束 - 乐观锁重试只适用于“最终一致即可、失败可接受、业务能容忍短暂延迟”的场景,例如文章阅读数自增、用户最后上线时间更新
- 如果重试 3 次仍失败,别硬扛,记录日志并触发告警——这时候大概率是设计问题,不是代码问题
真正难的不是写几行重试代码,而是想清楚这一笔更新背后的数据一致性边界在哪里。版本字段谁来维护、冲突后用户感知如何、下游系统是否依赖这次更新的即时性——这些比 while (true) 循环重要得多。











