hyperf 的 version 字段需手动实现乐观锁:必须用单条带 where version = ? 的 update 语句校验并原子更新,返回影响行为 0 时重试;多表操作须包裹 db::transaction(),单表可不加;禁止先查后改、忽略返回值或依赖自动机制。

Hyperf 本身不自动处理更新丢失,version 字段必须由你显式设计、读取、校验和更新,否则并发写入照样覆盖彼此。
为什么 version 字段不加 WHERE 条件就等于没锁
常见错误是:先 SELECT 出 version=1,再用 $model->version = 2; $model->save(); —— 这两步之间存在竞态窗口,第二个请求可能已把 version 改成 2,而你还按旧值覆盖过去。
- 必须把“读旧值 + 校验 + 更新”压进**单条 UPDATE 语句**,例如:
Db::table('orders')->where('id', $id)->where('version', $oldVersion)->update(['status' => 'paid', 'version' => DB::raw('version + 1')]) - 检查返回值:
$affected === 0表示更新失败(别人抢先改了),此时应重试或抛RuntimeException,不能忽略 -
version字段类型必须是INT或BIGINT,数据库默认值设为0(不是NULL)
Db::transaction() 是悲观锁的前提,但不是乐观锁的必需项
乐观锁靠的是 SQL 层的原子条件更新,不依赖事务阻塞,所以 version 方案可以不用事务包裹 —— 但要注意:如果你的业务逻辑还涉及其他表变更(比如扣库存+写订单),那就必须套 Db::transaction(),否则部分成功会破坏一致性。
- 纯单表状态变更(如节点上线/下线):可只用带
WHERE version = ?的update() - 多表联动(如创建订单 + 扣减商品库存):必须包在
Db::transaction()里,且所有操作都得走同一连接 - Hyperf 默认 PDO 开启
AUTOCOMMIT=1,若手动调beginTransaction()后忘了commit()或rollback(),会导致连接卡死
Hyperf 模型层的 $casts 对 version 字段没影响
version 是整数字段,PHP 自动转 int 没问题,不需要在模型里配 $casts = ['version' => 'integer']。真正容易出错的是——你用了 Model::find() 或 Model::first() 之后直接改属性再 save(),这会绕过乐观锁逻辑。
- 不要用
$order->version+++$order->save(),这是典型的丢失更新温床 - 也不要依赖框架自动注入 version(比如 Laravel 的
@Version注解),Hyperf 没内置类似机制,全靠手写 SQL 或封装工具方法 - 如果真想复用逻辑,建议封装一个
safeUpdate()方法,接收主键、更新数据和旧 version,内部拼 WHERE 条件并检查影响行数
version 字段不是开关,而是契约:你每次更新都必须校验它,漏一次,就可能让两个请求同时认为“自己是最后一个”,然后互相覆盖。别指望框架替你记住这点。











