日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

分布式锁和mysql事物扣库存_浅谈库存扣减和锁

發布時間:2023/12/10 数据库 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 分布式锁和mysql事物扣库存_浅谈库存扣减和锁 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

先說場景:

物品W現在庫存剩余1個, ?用戶P1,P2同時購買.則只有1人能購買成功.(前提是不允許超賣)

秒殺也是類似的情況, 只有1件商品,N個用戶同時搶購,只有1人能搶到..

這里不談秒殺設計,不談使用隊列等使請求串行化,就談下怎么用鎖來保證數據正確.

常見的實現方案有以下幾種:

1.代碼同步, 例如使用 synchronized ,lock 等同步方法

2.不查詢,直接更新 ?update table set surplus = (surplus - buyQuantity) where id = xx and (surplus - buyQuantity) > 0

3.使用CAS, update table set surplus = aa where id = xx and version = y

4.使用數據庫鎖, select xx for update

5.使用分布式鎖(zookeeper,redis等)

下面就針對這幾種方案來分析下;

1.代碼同步, 例如使用 synchronized ,lock 等同步方法

面試的時候,我經常會問這個問題,很大一部分人都會回答用這個方案來實現.

偽代碼如下:

public synchronized void buy(String productName, Integer buyQuantity) {

// 其他校驗...

// 校驗剩余數量

Product product = 從數據庫查詢出記錄;

if (product.getSurplus < buyQuantity) {

return "庫存不足";

}

// set新的剩余數量

product.setSurplus(product.getSurplus() - quantity);

// 更新數據庫

update(product);

// 記錄日志...

// 其他業務...

}

在方法聲明加上synchronized關鍵字,實現同步,這樣2個用戶同時購買,到buy方法時候同步執行,第2個用戶執行的時候,會庫存不足.

嗯.. 看著挺合理的,以前我也是這么干的. 所以現在碰到別人這樣回答,我就會在心里默默的想.小伙子你是沒踩過這坑啊.

先說下這個方案的前提配置:

1).使用spring 聲明式事務管理

2).事務傳播機制使用默認的(PROPAGATION_REQUIRED)

3).項目分層為controller-service-dao 3層, 事務管理在service層

這個方案不可行,主要是因為以下幾點:

1).synchronized 作用范圍是單個jvm實例, 如果做了集群,分布式等,就沒用了

2).synchronized是作用在對象實例上的,如果不是單例,則多個實例間不會同步(這個一般用spring管理bean,默認就是單例)

3).單個jvm時,synchronized也不能保證多個數據庫事務的隔離性. 這與代碼中的事務傳播級別,數據庫的事務隔離級別,加鎖時機等相關.

3-1).先說隔離級別,常用的是 Read Committed 和 Repeatable Read ,另外2種不常用就不說了

3-1-1)RR(Repeatable Read)級別.mysql默認的是RR,事務開啟后,不會讀取到其他事務提交的數據

根據前面的前提,我們知道在buy方法時會開啟事務.

假設現在有線程T1,T2同時執行buy方法.假設T1先執行,T2等待.

spring的事務開啟和提交等是通過aop(代理)實現的,所以執行buy方法前,就會開啟事務.

這時候T1,T2是兩個事務,當T1執行完后,T2執行,讀取不到T1提交的數據,所以會出問題.

3-1-2).RC(Read Committed)級別.事務開啟后,可以讀取到其他事務提交的數據

看起來這個級別可以解決上面的問題.T2執行時,可以讀取到T1提交的結果.

但是問題是,T2執行的時候, T1的事務提交了嗎?

事務和鎖的流程如下

1.開啟事務(aop)

2.加鎖(進入synchronized方法)

3.釋放鎖(退出synchronized方法)

4.提交事務(aop)

可以看出是先釋放鎖,再提交事務.所以T2執行查詢,可能還是未讀到T1提交的數據,還會出問題

3-2).根據3-1中的問題,發現主要矛盾是事務開啟和提交的時機與加鎖解鎖時機不一致.有小伙伴們可能就想到了解決方案.

3-2-1).在事務開啟前加鎖,事務提交后解鎖.

確實是可以,這相當于事務串行化.拋開性能不談,來談談怎么實現.

如果使用默認的事務傳播機制,那么要保證事務開啟前加鎖,事務提交后解鎖,就需要把加鎖,解鎖放在controller層.

這樣就有個潛在問題,所有操作庫存的方法,都要加鎖,而且要是同一把鎖,寫起來挺累的.

而且這樣還是不能跨jvm.

3-2-2).將查詢庫存,扣減庫存這2步操作,單獨提取個方法,單獨使用事務,并且事務隔離級別設置為RC.

這個其實和上面的3-2-1異曲同工,最終都是講加解鎖放在了事務開啟提交外層.

比較而言優點是入口少了. controller不用處理.

缺點除了上面的不能跨jvm,還有就是 單獨的這個方法,需要放到另外的service類中.

因為使用spring,同一個bean的內部方法調用,是不會被再次代理的,所以配置的單獨事務等需要放到另外的service bean 中

2.不查詢,直接更新

看完第一種方案,有小伙伴就說了. 你說的那么復雜,那么多問題,不就是因為查詢的數據不是最新的嗎?

我們不查詢,直接更新不就行啦.

偽代碼如下:

public synchronized void buy(String productName, Integer buyQuantity) {

// 其他校驗...

int 影響行數 = update table set surplus = (surplus - buyQuantity) where id = 1 ;

if (result < 0) {

return "庫存不足";

}

// 記錄日志...

// 其他業務...

}

測試后發現庫存變成-1了, 繼續完善下

public synchronized void buy(String productName, Integer buyQuantity) {

// 其他校驗...

int 影響行數 = update table set surplus = (surplus - buyQuantity) where id = 1 and (surplus - buyQuantity) > 0 ;

if (result < 0) {

return "庫存不足";

}

// 記錄日志...

// 其他業務...

}

測試后,功能OK;

這樣確實可以實現,不過有一些其他問題:

1). 不具備通用性,例如add操作

2). 庫存操作一般要記錄操作前后的數量等,這樣沒法記錄

3). 其他...

但是根據這個方案,可以引出方案3.

3.使用CAS, update table set surplus = aa where id = xx and yy = y

CAS是指compare/check and swap/set 意思都差不多,不必太糾結是哪個單詞

我們將上面的sql修改一下:

int 影響行數 = update table set surplus = newQuantity where id = 1 and surplus = oldQuantity ;

這樣,線程T1執行完后,線程T2去更新,影響行數=0,則說明數據被更新, 重新查詢判斷執行.偽代碼如下:

public void buy(String productName, Integer buyQuantity) {

// 其他校驗...

Product product = getByDB(productName);

int 影響行數 = update table set surplus = (surplus - buyQuantity) where id = 1 and surplus = 查詢的剩余數量 ;

while (result == 0) {

product = getByDB(productName);

if (查詢的剩余數量 > buyQuantity) {

影響行數 = update table set surplus = (surplus - buyQuantity) where id = 1 and surplus = 查詢的剩余數量 ;

} else {

return "庫存不足";

}

}

// 記錄日志...

// 其他業務...

}

看到重新查詢幾個字,小伙伴們應該就又想到事務隔離級別問題了.

沒錯,所以上面代碼中的getByDB方法,必須單獨事務(注意同一個bean內單獨事務不生效哦),而且數據庫的事務隔離級別必須是RC,

否則上面的代碼就會是死循環了.

上面的方案,可能會出現一個CAS中經典問題. ABA的問題.

ABA是指:

線程T1 查詢,庫存剩余 ?100

線程T2 查詢,庫存剩余 ?100

線程T1 執行subupdate t set surplus = 90 where id = x and surplus = 100;

線程T3 查詢, 庫存剩余 90

線程T3 執行add ?update t set surplus = 100 where id = x and surplus = 90;

線程T2 執行subupdate t set surplus = 90 where id = x and surplus = 100;

這里線程T2執行的時候,庫存的100已經不是查詢到的100了,但是對于這個業務是不影響的.

一般的設計中CAS會使用version來控制.

update t set surplus = 90 ,version = version+1 where id = x and version = oldVersion ;

這樣,每次更新version在原基礎上+1,就可以了.

使用CAS要注意幾點,

1)失敗重試次數,是否需要限制

2)失敗重試對用戶是透明的

4.使用數據庫鎖, select xx for update

方案3種的cas,是樂觀鎖的實現, 而select for udpate 則是悲觀鎖. 在查詢數據的時候,就將數據鎖住.

偽代碼如下:

public void buy(String productName, Integer buyQuantity) {

// 其他校驗...

Product product = select * from table where name = productName for update;

if (查詢的剩余數量 > buyQuantity) {

影響行數 = update table set surplus = (surplus - buyQuantity) where name = productName ;

} else {

return "庫存不足";

}

// 記錄日志...

// 其他業務...

}

線程T1 進行sub , 查詢庫存剩余 100

線程T2 進行sub , 這時候,線程T1事務還未提交,線程T2阻塞,直到線程T1事務提交或回滾才能查詢出結果.

所以線程T2查詢出的一定是最新的數據.相當于事務串行化了,就解決了數據一致性問題.

對于select for update,需要注意的有2點.

1) 統一入口:所有庫存操作都需要統一使用 select for update ,這樣才會阻塞, 如果另外一個方法還是普通的select, 是不會被阻塞的

2) 加鎖順序:如果有多個鎖,那么加鎖順序要一致,否則會出現死鎖.

5.使用分布式鎖(zookeeper,redis等)

使用分布式鎖,原理和方案1種的synchronized是一樣的.只不過synchronized的flag只有jvm進程內可見,而分布式鎖的flag則是全局可見.方案4種的select for update 的flag 也是全局可見.

分布式鎖的實現方案有很多:基于redis,基于zookeeper,基于數據庫等等.前面一篇博客寫了基于redis的簡易實現

基于redis setnx的簡易分布式鎖

需要注意,使用分布式鎖和synchronized鎖有同樣的問題,就是鎖和事務的順序,這個在方案1里面已經講過.不再重復.

做個簡單總結:

方案1:?synchronized等jvm內部鎖不適合用來保證數據庫數據一致性,不能跨jvm

方案2: 不具備通用性,不能記錄操作前后日志

方案3: 推薦使用.但是如果數據競爭激烈,則自動重試次數會急劇上升,需要注意.

方案4: 推薦使用.最簡單的方案,但是如果事務過大,會有性能問題.操作不當,會有死鎖問題

方案5: 和方案1類似,只是能跨jvm

總結

以上是生活随笔為你收集整理的分布式锁和mysql事物扣库存_浅谈库存扣减和锁的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。