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

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 运维知识 > 数据库 >内容正文

数据库

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

發(fā)布時(shí)間:2023/12/10 数据库 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 分布式锁和mysql事物扣库存_浅谈库存扣减和锁 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

先說(shuō)場(chǎng)景:

物品W現(xiàn)在庫(kù)存剩余1個(gè), ?用戶P1,P2同時(shí)購(gòu)買.則只有1人能購(gòu)買成功.(前提是不允許超賣)

秒殺也是類似的情況, 只有1件商品,N個(gè)用戶同時(shí)搶購(gòu),只有1人能搶到..

這里不談秒殺設(shè)計(jì),不談使用隊(duì)列等使請(qǐng)求串行化,就談下怎么用鎖來(lái)保證數(shù)據(jù)正確.

常見(jiàn)的實(shí)現(xiàn)方案有以下幾種:

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.使用數(shù)據(jù)庫(kù)鎖, select xx for update

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

下面就針對(duì)這幾種方案來(lái)分析下;

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

面試的時(shí)候,我經(jīng)常會(huì)問(wèn)這個(gè)問(wèn)題,很大一部分人都會(huì)回答用這個(gè)方案來(lái)實(shí)現(xiàn).

偽代碼如下:

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

// 其他校驗(yàn)...

// 校驗(yàn)剩余數(shù)量

Product product = 從數(shù)據(jù)庫(kù)查詢出記錄;

if (product.getSurplus < buyQuantity) {

return "庫(kù)存不足";

}

// set新的剩余數(shù)量

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

// 更新數(shù)據(jù)庫(kù)

update(product);

// 記錄日志...

// 其他業(yè)務(wù)...

}

在方法聲明加上synchronized關(guān)鍵字,實(shí)現(xiàn)同步,這樣2個(gè)用戶同時(shí)購(gòu)買,到buy方法時(shí)候同步執(zhí)行,第2個(gè)用戶執(zhí)行的時(shí)候,會(huì)庫(kù)存不足.

嗯.. 看著挺合理的,以前我也是這么干的. 所以現(xiàn)在碰到別人這樣回答,我就會(huì)在心里默默的想.小伙子你是沒(méi)踩過(guò)這坑啊.

先說(shuō)下這個(gè)方案的前提配置:

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

2).事務(wù)傳播機(jī)制使用默認(rèn)的(PROPAGATION_REQUIRED)

3).項(xiàng)目分層為controller-service-dao 3層, 事務(wù)管理在service層

這個(gè)方案不可行,主要是因?yàn)橐韵聨c(diǎn):

1).synchronized 作用范圍是單個(gè)jvm實(shí)例, 如果做了集群,分布式等,就沒(méi)用了

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

3).單個(gè)jvm時(shí),synchronized也不能保證多個(gè)數(shù)據(jù)庫(kù)事務(wù)的隔離性. 這與代碼中的事務(wù)傳播級(jí)別,數(shù)據(jù)庫(kù)的事務(wù)隔離級(jí)別,加鎖時(shí)機(jī)等相關(guān).

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

3-1-1)RR(Repeatable Read)級(jí)別.mysql默認(rèn)的是RR,事務(wù)開(kāi)啟后,不會(huì)讀取到其他事務(wù)提交的數(shù)據(jù)

根據(jù)前面的前提,我們知道在buy方法時(shí)會(huì)開(kāi)啟事務(wù).

假設(shè)現(xiàn)在有線程T1,T2同時(shí)執(zhí)行buy方法.假設(shè)T1先執(zhí)行,T2等待.

spring的事務(wù)開(kāi)啟和提交等是通過(guò)aop(代理)實(shí)現(xiàn)的,所以執(zhí)行buy方法前,就會(huì)開(kāi)啟事務(wù).

這時(shí)候T1,T2是兩個(gè)事務(wù),當(dāng)T1執(zhí)行完后,T2執(zhí)行,讀取不到T1提交的數(shù)據(jù),所以會(huì)出問(wèn)題.

3-1-2).RC(Read Committed)級(jí)別.事務(wù)開(kāi)啟后,可以讀取到其他事務(wù)提交的數(shù)據(jù)

看起來(lái)這個(gè)級(jí)別可以解決上面的問(wèn)題.T2執(zhí)行時(shí),可以讀取到T1提交的結(jié)果.

但是問(wèn)題是,T2執(zhí)行的時(shí)候, T1的事務(wù)提交了嗎?

事務(wù)和鎖的流程如下

1.開(kāi)啟事務(wù)(aop)

2.加鎖(進(jìn)入synchronized方法)

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

4.提交事務(wù)(aop)

可以看出是先釋放鎖,再提交事務(wù).所以T2執(zhí)行查詢,可能還是未讀到T1提交的數(shù)據(jù),還會(huì)出問(wèn)題

3-2).根據(jù)3-1中的問(wèn)題,發(fā)現(xiàn)主要矛盾是事務(wù)開(kāi)啟和提交的時(shí)機(jī)與加鎖解鎖時(shí)機(jī)不一致.有小伙伴們可能就想到了解決方案.

3-2-1).在事務(wù)開(kāi)啟前加鎖,事務(wù)提交后解鎖.

確實(shí)是可以,這相當(dāng)于事務(wù)串行化.拋開(kāi)性能不談,來(lái)談?wù)勗趺磳?shí)現(xiàn).

如果使用默認(rèn)的事務(wù)傳播機(jī)制,那么要保證事務(wù)開(kāi)啟前加鎖,事務(wù)提交后解鎖,就需要把加鎖,解鎖放在controller層.

這樣就有個(gè)潛在問(wèn)題,所有操作庫(kù)存的方法,都要加鎖,而且要是同一把鎖,寫起來(lái)挺累的.

而且這樣還是不能跨jvm.

3-2-2).將查詢庫(kù)存,扣減庫(kù)存這2步操作,單獨(dú)提取個(gè)方法,單獨(dú)使用事務(wù),并且事務(wù)隔離級(jí)別設(shè)置為RC.

這個(gè)其實(shí)和上面的3-2-1異曲同工,最終都是講加解鎖放在了事務(wù)開(kāi)啟提交外層.

比較而言優(yōu)點(diǎn)是入口少了. controller不用處理.

缺點(diǎn)除了上面的不能跨jvm,還有就是 單獨(dú)的這個(gè)方法,需要放到另外的service類中.

因?yàn)槭褂胹pring,同一個(gè)bean的內(nèi)部方法調(diào)用,是不會(huì)被再次代理的,所以配置的單獨(dú)事務(wù)等需要放到另外的service bean 中

2.不查詢,直接更新

看完第一種方案,有小伙伴就說(shuō)了. 你說(shuō)的那么復(fù)雜,那么多問(wèn)題,不就是因?yàn)椴樵兊臄?shù)據(jù)不是最新的嗎?

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

偽代碼如下:

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

// 其他校驗(yàn)...

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

if (result < 0) {

return "庫(kù)存不足";

}

// 記錄日志...

// 其他業(yè)務(wù)...

}

測(cè)試后發(fā)現(xiàn)庫(kù)存變成-1了, 繼續(xù)完善下

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

// 其他校驗(yàn)...

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

if (result < 0) {

return "庫(kù)存不足";

}

// 記錄日志...

// 其他業(yè)務(wù)...

}

測(cè)試后,功能OK;

這樣確實(shí)可以實(shí)現(xiàn),不過(guò)有一些其他問(wèn)題:

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

2). 庫(kù)存操作一般要記錄操作前后的數(shù)量等,這樣沒(méi)法記錄

3). 其他...

但是根據(jù)這個(gè)方案,可以引出方案3.

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

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

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

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

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

public void buy(String productName, Integer buyQuantity) {

// 其他校驗(yàn)...

Product product = getByDB(productName);

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

while (result == 0) {

product = getByDB(productName);

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

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

} else {

return "庫(kù)存不足";

}

}

// 記錄日志...

// 其他業(yè)務(wù)...

}

看到重新查詢幾個(gè)字,小伙伴們應(yīng)該就又想到事務(wù)隔離級(jí)別問(wèn)題了.

沒(méi)錯(cuò),所以上面代碼中的getByDB方法,必須單獨(dú)事務(wù)(注意同一個(gè)bean內(nèi)單獨(dú)事務(wù)不生效哦),而且數(shù)據(jù)庫(kù)的事務(wù)隔離級(jí)別必須是RC,

否則上面的代碼就會(huì)是死循環(huán)了.

上面的方案,可能會(huì)出現(xiàn)一個(gè)CAS中經(jīng)典問(wèn)題. ABA的問(wèn)題.

ABA是指:

線程T1 查詢,庫(kù)存剩余 ?100

線程T2 查詢,庫(kù)存剩余 ?100

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

線程T3 查詢, 庫(kù)存剩余 90

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

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

這里線程T2執(zhí)行的時(shí)候,庫(kù)存的100已經(jīng)不是查詢到的100了,但是對(duì)于這個(gè)業(yè)務(wù)是不影響的.

一般的設(shè)計(jì)中CAS會(huì)使用version來(lái)控制.

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

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

使用CAS要注意幾點(diǎn),

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

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

4.使用數(shù)據(jù)庫(kù)鎖, select xx for update

方案3種的cas,是樂(lè)觀鎖的實(shí)現(xiàn), 而select for udpate 則是悲觀鎖. 在查詢數(shù)據(jù)的時(shí)候,就將數(shù)據(jù)鎖住.

偽代碼如下:

public void buy(String productName, Integer buyQuantity) {

// 其他校驗(yàn)...

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

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

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

} else {

return "庫(kù)存不足";

}

// 記錄日志...

// 其他業(yè)務(wù)...

}

線程T1 進(jìn)行sub , 查詢庫(kù)存剩余 100

線程T2 進(jìn)行sub , 這時(shí)候,線程T1事務(wù)還未提交,線程T2阻塞,直到線程T1事務(wù)提交或回滾才能查詢出結(jié)果.

所以線程T2查詢出的一定是最新的數(shù)據(jù).相當(dāng)于事務(wù)串行化了,就解決了數(shù)據(jù)一致性問(wèn)題.

對(duì)于select for update,需要注意的有2點(diǎn).

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

2) 加鎖順序:如果有多個(gè)鎖,那么加鎖順序要一致,否則會(huì)出現(xiàn)死鎖.

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

使用分布式鎖,原理和方案1種的synchronized是一樣的.只不過(guò)synchronized的flag只有jvm進(jìn)程內(nèi)可見(jiàn),而分布式鎖的flag則是全局可見(jiàn).方案4種的select for update 的flag 也是全局可見(jiàn).

分布式鎖的實(shí)現(xiàn)方案有很多:基于redis,基于zookeeper,基于數(shù)據(jù)庫(kù)等等.前面一篇博客寫了基于redis的簡(jiǎn)易實(shí)現(xiàn)

基于redis setnx的簡(jiǎn)易分布式鎖

需要注意,使用分布式鎖和synchronized鎖有同樣的問(wèn)題,就是鎖和事務(wù)的順序,這個(gè)在方案1里面已經(jīng)講過(guò).不再重復(fù).

做個(gè)簡(jiǎn)單總結(jié):

方案1:?synchronized等jvm內(nèi)部鎖不適合用來(lái)保證數(shù)據(jù)庫(kù)數(shù)據(jù)一致性,不能跨jvm

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

方案3: 推薦使用.但是如果數(shù)據(jù)競(jìng)爭(zhēng)激烈,則自動(dòng)重試次數(shù)會(huì)急劇上升,需要注意.

方案4: 推薦使用.最簡(jiǎn)單的方案,但是如果事務(wù)過(guò)大,會(huì)有性能問(wèn)題.操作不當(dāng),會(huì)有死鎖問(wèn)題

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

總結(jié)

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

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。