Laravel 中 sharedLock 与 lockForUpdate 的区别

场景#

拼团功能,当 A 客户开团之后(两人团),如果 B 和 C 同时支付,如何规避两人同时将拼团人数增加。

Laravel 中 sharedLock 与 lockForUpdate 的区别#

  • sharedLock 对应的是 LOCK IN SHARE MODE
  • lockForUpdate 对应的是 FOR UPDATE

sharedLock 与 lockForUpdate 相同的地方是,都能避免同一行数据被其他 transaction 进行 update。

不同的地方是:

  • sharedLock 不会阻止其他 transaction 读取同一行
  • lockForUpdate 会阻止其他 transaction 读取同一行 (需要特别注意的是,普通的非锁定读取读取依然可以读取到该行,只有 sharedLock 和 lockForUpdate 的读取会被阻止。)

即 sharedLock locks only for write, lockForUpdate also prevents them from being selected

这样做是有意义的,例如,两个 transaction 要更新同一个计数器,如果不使用 lockForUpdate, 会导致两个 transaction 同时读到同一个初始值,然后在应用层逻辑中增加计数之后,提交到数据库中,后者的操作会覆盖掉前者的操作。

如何测试#

在 MySQL 命令行终端操作一个表

Copy Highlighter-hljs
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from users for update; +----+------+ | id | name | +----+------+ | 1 | tom | | 2 | bob | +----+------+

这时再开一个命令行终端

Copy Highlighter-hljs
mysql> select * from users for update; ^C^C -- query aborted ERROR 1317 (70100): Query execution was interrupted mysql> select * from users lock in share mode; ^C^C -- query aborted ERROR 1317 (70100): Query execution was interrupted

你会发现,无论是 for update 还是 lock in share mode 都无法读取到数据,更加确切地说是,查询被阻塞了。

只有在第一个终端执行

Copy Highlighter-hljs
commit;

第二个终端才能得到数据返回。

需要注意的是,发起者必须在 transaction 里上锁才有效,如果不是在 transaction 中,上锁是无效的。但是,第二个人无论是不是在 transaction 里,都会被锁。

我依然有几个疑问#

  • Laravel 如何设置数据库操作超时时间
  • 什么场景下适合使用 sharedLock 呢?
  • sharedLock,lockForUpdate 与 Pessimistic Locking 是什么关系
  • Pessimistic locking(悲观锁) 与 Optimistic locking(乐观锁)的区别

如何测试 Laravel#

A 用户,在浏览器里访问接口 (模拟支付回调),此时对数据表中某一行锁住,进行 30s 操作,然后提交事务。

B 用户,在浏览器里访问同一接口 (模拟支付回调),其无法修改该行。对应的返回是什么?

会一直 wait 到数据库操作超时。

那么问题来了,Laravel 如何设置数据库操作超时时间?

简单的测试方法,是在命令行中开两个 artisan tinker 窗口,分别执行

Copy Highlighter-hljs
DB::transaction(function () { echo 1; User::where('id', 33)->lockForUpdate()->get(); echo 2; sleep(10); });

你会发现第二个 tinker 窗口中的 get 操作,需要等到第一个 transaction 执行完毕之后,才能得到查询结果。

需要注意的是,不在 transaction 中的 lockForUpdate 操作,是没有锁效果的。

真实场景,防止用户重复提现

Copy Highlighter-hljs
DB::transaction(function () use ($user, &$user_award) { $user_award = UserAward::where([ ['user_id', $user->id], ['status', 0], ]) ->lockForUpdate() ->first(); if ($user_award) { $user_award->status = 1; // 提现中状态 $user_award->save(); } }); if (!is_null($user_award)) { $amount = $user_award->money * 100; }

事务与锁的关系#

事务中涉及的操作都会加上锁?

如果默认会加上锁,那么默认会加上什么锁呢?

事务中涉及的操作,不需要显式加锁?

要理清其中关系,就需要了解事务的四种隔离级别:

  • 未提交读(Read uncommitted)
  • 已提交读(Read committed)
  • 可重复读(Repeatable read)
  • 可串行化(Serializable )

MySQL 默认的是:可重复读(Repeatable read)

posted @   caibaotimes  阅读(541)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示
CONTENTS