使用 Laravel sharedLock 与 lockForUpdate 进行数据表行锁

更新日期: 2019-07-07 阅读次数: 19649 分类: Laravel

场景

拼团功能,当 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 命令行终端操作一个表

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from users for update;
+----+------+
| id | name |
+----+------+
|  1 | tom  |
|  2 | bob  |
+----+------+

这时再开一个命令行终端

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 都无法读取到数据,更加确切地说是,查询被阻塞了。

只有在第一个终端执行

commit;

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

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

我依然有几个疑问

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

如何测试 Laravel

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

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

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

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

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

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

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

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

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

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)

参考

谈笑风生

你会后悔吗

从你的网站我知道了我用的原来是电信网

爱评论不评论

近期节日

2019年07月23日 大暑
2019年07月30日 非洲妇女日
2019年08月01日 八一建军节
2019年08月06日 国际电影节
2019年08月07日 七夕
2019年08月08日 立秋
2019年08月15日 日本投降日
2019年08月15日 中元节
2019年08月23日 处暑
2019年09月03日 抗日胜利纪念日
2019年09月08日 白露
2019年09月08日 国际扫盲日
查看更多节日