商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > PHP怎么处理Eloquent Locking悲观与乐观锁_Laravel并发控制【技巧】

PHP怎么处理Eloquent Locking悲观与乐观锁_Laravel并发控制【技巧】

  发布于2026-07-19 阅读(0)

扫一扫,手机访问

先说几个关键点:悲观锁的 selectForUpdate() 确实会阻塞,但仅限于数据库行级锁,而且必须在事务内生效。脱离 DB::transaction(),或者没有关闭自动提交,它就可能静默失效,甚至退化成普通查询。乐观锁则完全不依赖数据库锁,靠 version 字段做并发校验,但需要自己拼 where 条件。至于 sharedLock(),它允许多个事务同时读,但会阻塞写操作——不过它防不住“丢失更新”这类问题。下面逐一拆解。

PHP怎么处理Eloquent Locking悲观与乐观锁_Lara vel并发控制【技巧】

悲观锁:selectForUpdate() 在事务中真的会阻塞吗

会,但前提是它确实在事务内运行,并且 where 条件命中了索引。否则,要么锁失效,要么直接锁表。一个经典错误是:SQLSTATE[HY000]: General error: 1205 Deadlock found when trying to get lock —— 多个请求同时对同一组记录加锁,但顺序不一致,触发 MySQL 死锁。

  • 务必用 DB::transaction() 包裹整个读-改-写流程,不能只给 selectForUpdate() 加事务
  • 锁的范围由 where 条件决定:无索引字段会导致表锁,有复合索引但未覆盖查询条件也会升格为间隙锁
  • MySQL 中 InnoDB 的 SELECT ... FOR UPDATE 在可重复读(RR)隔离级别下默认加临键锁(Next-Key Lock),可能锁住不存在但“应该存在”的间隙
  • 示例:
    DB::transaction(function () {    $user = User::where('id', 123)->lockForUpdate()->first();    $user->balance -= 100;    $user->sa ve();});

乐观锁:用 increment() 和 where version=xxx 避免覆盖写

乐观锁的核心是“先查后验”,不依赖数据库锁,而是靠业务字段(比如 versionupdated_at)做并发校验。Lara vel 自带的 increment()decrement() 是原子操作,但本身不带版本检查——你得手动拼 where 条件。

容易踩的坑:$model->version++ + $model->sa ve() 不是原子的,两个请求可能读到相同 version,都写成了 version=2,结果一次更新被覆盖。

  • 正确做法是用 whereRaw('version = ?', [$oldVersion]) 做更新前置校验,再用 update() 一次性完成
  • 若返回影响行数为 0,说明已被其他请求抢先更新,应重试或抛异常
  • increment() 本身安全,但仅适用于纯数值累加场景(如计数器),无法替代带业务逻辑的“读-改-写”
  • 示例:
    $affected = User::where('id', 123)    ->where('version', $expectedVersion)    ->update(['balance' => DB::raw('balance - 100'), 'version' => DB::raw('version + 1')]);

selectForUpdate() 和 sharedLock() 的实际区别在哪

selectForUpdate() 加排他锁(X Lock),阻止其他事务读写该行;sharedLock() 加共享锁(S Lock),允许其他事务加 S 锁(即并发读),但会阻塞 X 锁(写操作)。两者都只在事务中有效,且都要求 where 条件命中索引。

一个典型误用:把 sharedLock() 当成“读不阻塞”的万能方案——但它并不能防止 A 读、B 写、A 再写导致的覆盖问题(即丢失更新),它只防“写-写冲突”,不防“读-写-写”。

  • 想防止别人修改当前行 → 用 selectForUpdate()
  • 只想防止别人修改,但允许多人同时读(比如生成报表前确认数据未被编辑)→ 用 sharedLock()
  • 二者都不解决幻读;如需锁定范围(如“所有 status=0 的订单”),必须配合 WHERE 精确命中索引,否则可能锁表
  • MySQL 中 sharedLock() 对应 SELECT ... LOCK IN SHARE MODE,注意某些存储引擎(如 MyISAM)不支持

高并发下 update() 带 where 条件为什么有时像乐观锁

因为 InnoDB 的 UPDATE 语句在执行时会先定位记录并加 X 锁,再判断 WHERE 条件是否满足;若不满足,锁会立刻释放。所以多个请求同时执行 User::where('balance', '>=', 100)->decrement('balance', 100),最终只有第一个成功扣减,其余因条件不满足而跳过——这看起来像乐观锁行为,但底层仍是悲观锁机制。

关键点在于:这不是“无锁重试”,而是“锁了再判,不满足就放”。如果业务需要明确知道是否被抢,不能只靠 affected rows 为 0 就认为失败,得结合具体逻辑判断(比如余额是否真不足)。

  • 这种模式适合幂等性要求高、失败可接受的场景(如优惠券核销)
  • 不适合需要严格顺序或必须成功一次的场景(如库存预占),此时仍需显式加锁或队列削峰
  • 注意 decrement() 不会触发模型事件(sa ving/sa ved),如有审计日志等需求,得手动补全

实际用哪一种,取决于你能否容忍“重试”、数据库是否支持行锁、以及业务对一致性的容忍边界。悲观锁容易死锁,乐观锁容易重试失败,而看似简单的 update() + where 其实暗藏锁生命周期细节——这些地方,线上出问题时往往最先崩。

本文转载于:https://www.php.cn/faq/2314023.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注