關於庫存超賣問題,悲觀鎖和樂觀鎖的不同實現

木大大發表於2021-04-30

準備資料

id name num version
1 張三 100 0

庫存超賣問題是因為併發過程中,多個程式在併發時獲取的庫存資料是一致的,然後減庫存的操作又是同時進行的,從而導致庫存資料出現混亂。

當前mysql環境5.7 innodb 儲存引擎,事務隔離級別 可重複讀。

樂觀鎖

概念理解:假設併發過程中不存在,衝突的情況,而在出現衝突之後再進行處理

在事務中查詢資料的同時,並查詢一個版本號資料,然後在更新庫存的時候,根據 id + 已查出的版本號 作為查詢條件,來更新數量,並對版本號 +1。

此時如果是同時(併發)多個請求進來,那麼只有一個程式會更新資料,其它程式因為在更新資料時因為版本號不一致而無法對資料做修改,從而更新失敗。因為支援只有一個程式能修改資料,修改資料之後,其餘的程式只能走減庫存失敗的邏輯。從而避免庫存超賣。

程式碼如下:(laravel框架)

public function index()
    {
        DB::transaction(function () {
            $data=DB::table('stu')
                ->where('id',1)
                ->first(['num','version']);

info(microtime(true).'=='.$data->num.'='.$data->version.'===');

           $res = DB::table('stu')
               ->where('id',1)
               ->where('version',$data->version)
               ->update(['num'=>$data->num-2,'version'=>$data->version+1]);

           if($res) info(microtime(true)."==".($data->num-2).'='.($data->version+1)."\n");

          //  DB::table('tea')->where('id',1)->decrement('num');

        }, 5);

        echo "good!\n";
    }

使用ab壓測工具做10個併發測試
ab -n 10 -c 10 http://local.laravel-test.com/test

列印的日誌如下

[2021-04-29 16:09:08] local.INFO: 1619712548.8403==100=0===  
[2021-04-29 16:09:08] local.INFO: 1619712548.8652==98=1

[2021-04-29 16:09:13] local.INFO: 1619712553.4087==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4302==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4144==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4239==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4401==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4148==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4308==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.4475==98=1===  
[2021-04-29 16:09:13] local.INFO: 1619712553.5869==96=2

[2021-04-29 16:09:13] local.INFO: 1619712553.826==96=2===  
[2021-04-29 16:09:13] local.INFO: 1619712553.8441==94=3

悲觀鎖

概念理解:假設併發就會出現衝突,在業務邏輯前就做阻塞處理

程式碼實現,通過在讀取庫存數量的時候進行加鎖,在事務結束後再解鎖,這樣的話,再加鎖過程中其它程式無法讀取庫存資料只能等待。從而在減庫存的操作上依序執行來保證庫存不被超賣。

使用悲觀鎖的話就是需要 version 欄位了

ab -n 10 -c 10 http://local.laravel-test.com/test

  DB::transaction(function () {
            $num=DB::table('stu')->where('id',1)
                ->lockForUpdate()
                ->value('num');

            info(microtime(true).'=='.$num.'===');
            $res = DB::table('stu')->where('id',1)
                ->update(['num'=>$num-2]);

            if($res) info(microtime(true)."==".($num-2).'='."\n");

            //  DB::table('tea')->where('id',1)->decrement('num');

        }, 5);

        echo "good!\n";

列印日誌如下

[2021-04-30 01:36:03] local.INFO: 1619746563.4187==100===  
[2021-04-30 01:36:03] local.INFO: 1619746563.4493==98=

[2021-04-30 01:36:08] local.INFO: 1619746568.1882==98===  
[2021-04-30 01:36:08] local.INFO: 1619746568.2638==96=

[2021-04-30 01:36:08] local.INFO: 1619746568.2679==96===  
[2021-04-30 01:36:08] local.INFO: 1619746568.3162==94=

[2021-04-30 01:36:08] local.INFO: 1619746568.3225==94===  
[2021-04-30 01:36:08] local.INFO: 1619746568.36==92=

[2021-04-30 01:36:08] local.INFO: 1619746568.3668==92===  
[2021-04-30 01:36:08] local.INFO: 1619746568.4083==90=

[2021-04-30 01:36:08] local.INFO: 1619746568.4168==90===  
[2021-04-30 01:36:08] local.INFO: 1619746568.4562==88=

[2021-04-30 01:36:08] local.INFO: 1619746568.4608==88===  
[2021-04-30 01:36:08] local.INFO: 1619746568.5036==86=

[2021-04-30 01:36:08] local.INFO: 1619746568.5095==86===  
[2021-04-30 01:36:08] local.INFO: 1619746568.5532==84=

[2021-04-30 01:36:08] local.INFO: 1619746568.5573==84===  
[2021-04-30 01:36:08] local.INFO: 1619746568.5913==82=

[2021-04-30 01:36:08] local.INFO: 1619746568.5959==82===  
[2021-04-30 01:36:08] local.INFO: 1619746568.6296==80=

關於樂觀鎖與悲觀鎖

樂觀鎖在併發時,因為是在出現衝突之後進行處理,好處就是可以提高併發量,壞處也顯而易見,因為併發時最終只有一個程式會修改資料成功,那麼其它併發進來的程式,就只能走失敗的業務邏輯,那就意味著實際業務中不能保證每個人都下單成功。

悲觀鎖,與樂觀鎖相反,因為是阻塞執行,那麼併發能力就不足,但是每個程式在伺服器負載內都能正常下單成功。

其它,序列化隔離級別測試

修改mysql隔離級別為序列化

# 修改全域性隔離級別為序列化
SET Global TRANSACTION ISOLATION LEVEL SERIALIZABLE;

# 修改全域性隔離級別為可重複讀
SET global TRANSACTION ISOLATION LEVEL REPEATABLE READ;

# 檢視隔離級別
select @@global.tx_isolation;

ab工具併發執行如下程式碼
ab -n 10 -c 10 http://local.laravel-test.com/test1

DB::transaction(function () {
            $num=DB::table('stu')->where('id',1)
                //->lockForUpdate()
                ->value('num');

            info(microtime(true).'=='.$num.'===');
            $res = DB::table('stu')->where('id',1)
                ->update(['num'=>$num-2]);

            if($res) info(microtime(true)."==".($num-2).'='."\n");

            //  DB::table('tea')->where('id',1)->decrement('num');

        }, 5);

        echo "good!\n";

以為可以解決庫存超賣為題,沒想到laravel 日誌報錯,出現死鎖。

據說序列化隔離級別會將事務序列化執行,既然序列化執行了,怎麼還會出現死鎖,不解中。

希望來個大佬留言指點一二,小弟感激

ocal.ERROR: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction (SQL: update `stu` set `num` = 88 where `id` = 1) {"exception":"[object] (Illuminate\\Database\\QueryException(code: 40001): SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction (SQL: update `stu` set `num` = 88 where `id` = 1) at /mnt/hgfs/Centos7/laravel8/test/vendor/laravel/framework/src/Illuminate/Database/Connection.php:678) [stacktrace] 
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章