使用 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 命令行終端操作一個表

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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章