準備資料
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 協議》,轉載必須註明作者和本文連結