高并发-【抢红包案例】之二:使用悲观锁方式修复红包超发的bug
文章目錄
- 概述
- 超發(fā)問題分析
- 使用數(shù)據(jù)庫(kù)鎖的解決方案
- 使用悲觀鎖(排它鎖 for update)
- 使用樂觀鎖(依靠表的設(shè)計(jì)和代碼)
- 總結(jié)
- 悲觀鎖(抽象的描述,不真實(shí)存在這個(gè)鎖)
- 共享鎖(S鎖)
- 排他鎖(X鎖)
- 代碼改造
- 分析
- RedPacketDao新增接口方法
- RedPacket.xml配置映射文件
- Service層調(diào)用新的Dao方法
- 還原數(shù)據(jù),部署測(cè)試
- 統(tǒng)計(jì)報(bào)告
- 注意事項(xiàng)
- 悲觀鎖導(dǎo)致性能下降的原因探究
- 代碼
概述
高并發(fā)–【搶紅包案例分析和代碼實(shí)現(xiàn)以及各種方案的優(yōu)缺點(diǎn)】之一中使用ssm+mysql實(shí)現(xiàn),存在并發(fā)超發(fā)問題,這里我們使用悲觀鎖的方式來解決這個(gè)邏輯錯(cuò)誤,并驗(yàn)證數(shù)據(jù)一致性和性能狀況。
超發(fā)問題分析
針對(duì)這個(gè)案例,用戶搶到紅包后,紅包總量應(yīng)-1,當(dāng)多個(gè)用戶同時(shí)搶紅包,此時(shí)多個(gè)線程同時(shí)讀得庫(kù)存為n,相應(yīng)的邏輯執(zhí)行后,最后將均執(zhí)update T_RED_PACKET set stock = stock - 1 where id = #{id} ,很明顯這是錯(cuò)誤的。
使用數(shù)據(jù)庫(kù)鎖的解決方案
使用悲觀鎖(排它鎖 for update)
使用樂觀鎖(依靠表的設(shè)計(jì)和代碼)
這樣,保證了修改的數(shù)據(jù)是和它查詢出來的數(shù)據(jù)是一致的,而其他線程并未進(jìn)行修改。當(dāng)然,如果更新失敗,表示在更新操作之前有其他線程已經(jīng)更新了該紅包數(shù),那么就可以嘗試重入機(jī)制來保證更新成功。
總結(jié)
- 1.悲觀鎖使用了排他鎖,當(dāng)程序獨(dú)占鎖時(shí),其他程序就連查詢都是不允許的,導(dǎo)致吞吐較低。如果在查詢較多的情況下,可使用樂觀鎖。
- 2.樂觀鎖更新有可能會(huì)失敗,甚至是更新幾次都失敗,這是有風(fēng)險(xiǎn)的。所以如果寫入較頻繁,對(duì)吞吐要求不高,可使用悲觀鎖。
悲觀鎖(抽象的描述,不真實(shí)存在這個(gè)鎖)
悲觀鎖是在操作數(shù)據(jù)時(shí),認(rèn)為此操作會(huì)出現(xiàn)數(shù)據(jù)沖突,所以在進(jìn)行每次操作時(shí)都要通過獲取鎖才能進(jìn)行對(duì)相同數(shù)據(jù)的操作,所以悲觀鎖需要耗費(fèi)較多的時(shí)間。另悲觀鎖是由數(shù)據(jù)庫(kù)自己實(shí)現(xiàn)了的,使用的時(shí)候,直接調(diào)用數(shù)據(jù)庫(kù)的相關(guān)語(yǔ)句即可。
由悲觀鎖涉及到的另外兩個(gè)鎖概念就出來了,它們就是共享鎖與排它鎖。共享鎖和排它鎖是悲觀鎖的不同的實(shí)現(xiàn),它倆都屬于悲觀鎖的范疇。
數(shù)據(jù)庫(kù)的增刪改操作默認(rèn)都會(huì)加排他鎖,而查詢不會(huì)加任何鎖。
共享鎖(S鎖)
共享鎖指的就是對(duì)于多個(gè)不同的事務(wù),對(duì)同一個(gè)資源共享同一個(gè)鎖.
對(duì)某一資源加共享鎖,自身可以讀該資源,其他人也可以讀該資源(也可以再繼續(xù)加共享鎖,即 共享鎖可多個(gè)共存),但無法修改。要想修改就必須等所有共享鎖都釋放完之后.
語(yǔ)法:
select * from table lock in share mode ;排他鎖(X鎖)
排它鎖與共享鎖相對(duì)應(yīng),就是指對(duì)于多個(gè)不同的事務(wù),對(duì)同一個(gè)資源只能有一把鎖。對(duì)某一資源加排他鎖,自身可以進(jìn)行增刪改查,其他人無法進(jìn)行任何操作。
與共享鎖類型,在需要執(zhí)行的語(yǔ)句后面加上for update就可以了
語(yǔ)法:
select * from table for update代碼改造
分析
為了不影響上個(gè)版本,我們新加個(gè)接口方法和Mapper映射。 因?yàn)楸^鎖是數(shù)據(jù)庫(kù)提供的功能,所以僅僅在Dao層修改Sql,Service層無需新增新的接口,只需要切換下調(diào)用的Dao層的方法即可。
RedPacketDao新增接口方法
/*** 獲取紅包信息. 悲觀鎖的實(shí)現(xiàn)方式* * @param id* --紅包id* @return 紅包具體信息*/public RedPacket getRedPacketForUpdate(Long id);RedPacket.xml配置映射文件
<!-- 查詢紅包具體信息 悲觀鎖的實(shí)現(xiàn)方式for update --><select id="getRedPacketForUpdate" parameterType="long"resultType="com.artisan.redpacket.pojo.RedPacket">select id, user_id as userId, amount, send_date as sendDate, total, unit_amount as unitAmount, stock, version, notefrom T_RED_PACKET where id = #{id} for update</select>悲觀鎖是一種利用數(shù)據(jù)庫(kù)內(nèi)部機(jī)制提供的鎖的方法,也就是對(duì)更新的數(shù)據(jù)加鎖,這樣在并發(fā)期間一旦有一個(gè)事務(wù)持有了數(shù)據(jù)庫(kù)記錄的鎖,其他的線程將不能再對(duì)數(shù)據(jù)進(jìn)行更新.
在 SQL 中加入的 for update 語(yǔ)句,意味著將持有對(duì)數(shù)據(jù)庫(kù)記錄的行更新鎖(因?yàn)檫@里使用主鍵查詢,所以只會(huì)對(duì)行加鎖。如果使用的是非主鍵查詢,要考慮是否對(duì)全表加鎖的問題,加鎖后可能引發(fā)其他查詢的阻塞〉,那就意味著在高并發(fā)的場(chǎng)景下 , 當(dāng)一條事務(wù)持有了這個(gè)更新鎖才能往下操作,其他的線程如果要更新這條記錄,都需要等待,這樣就不會(huì)出現(xiàn)超發(fā)現(xiàn)象引發(fā)的數(shù)據(jù)一致性問題了.
Service層調(diào)用新的Dao方法
還原數(shù)據(jù),部署測(cè)試
將T_RED_PACKET和T_USER_RED_PACKET中的數(shù)據(jù)還原為初始數(shù)據(jù)后,啟動(dòng)應(yīng)用,通過FireFox 訪問 http://localhost:8080/ssm_redpacket/grap.jsp
統(tǒng)計(jì)報(bào)告
一致性數(shù)據(jù)統(tǒng)計(jì):
SELECTa.id,a.amount,a.stock FROMT_RED_PACKET a WHEREa.id = 1 UNION ALLSELECTmax(b.user_id),sum(b.amount),count(*)FROMT_USER_RED_PACKET bWHEREb.red_packet_id = 1;這里已經(jīng)解決了超發(fā)的問題,所以結(jié)果是正確的,最起碼邏輯是正確的了。除了結(jié)果正確,我們還需要考慮性能問題,統(tǒng)計(jì)來看下
性能數(shù)據(jù)統(tǒng)計(jì):
SELECT(UNIX_TIMESTAMP(max(a.grab_time)) - UNIX_TIMESTAMP(min(a.grab_time)) ) AS lastTime FROMT_USER_RED_PACKET a;注意事項(xiàng)
不使用悲觀鎖時(shí),2萬個(gè)紅包190秒【主機(jī)配置很低】搶完(但存在超發(fā)現(xiàn)象),現(xiàn)在是275秒。 目前只是對(duì)數(shù)據(jù)庫(kù)加了一個(gè)鎖,當(dāng)加的鎖比較多的時(shí)候,數(shù)據(jù)庫(kù)的性能還會(huì)持續(xù)下降,所以要區(qū)分不同的業(yè)務(wù)場(chǎng)景,慎重使用。
悲觀鎖導(dǎo)致性能下降的原因探究
對(duì)于悲觀鎖來說,當(dāng)一條線程搶占了資源后,其他的線程將得不到資源,那么這個(gè)時(shí), CPU 就會(huì)將這些得不到資源的線程掛起,掛起的線程也會(huì)消耗 CPU 的資源尤其是在高并發(fā)的請(qǐng)求中。
只能有一個(gè)事務(wù)占據(jù)資源,其他事務(wù)被掛起等待持有資源的事務(wù)提交并釋放資源。當(dāng)此時(shí)就進(jìn)入了線程 2 , 線程 3……線程n,開始搶奪資源的步驟了,這里假設(shè)線程 3 搶到資源
一旦線程1 提交了事務(wù),那么鎖就會(huì)被釋放,這個(gè)時(shí)候被掛起的線程就會(huì)開始競(jìng)爭(zhēng)紅包資源,那么競(jìng)爭(zhēng)到的線程就會(huì)被 CPU 恢復(fù)到運(yùn)行狀態(tài),繼續(xù)運(yùn)行。
于是頻繁掛起,等待持有鎖線程釋放資源, 一旦釋放資源后,就開始搶奪,恢復(fù)線程,直至所有紅包資源搶完。
在高并發(fā)的過程中,使用悲觀鎖就會(huì)造成大量的線程被掛起和恢復(fù),這將十分消耗資源,這就是為什么使用悲觀鎖性能不佳的原因。
有些時(shí)候,我們也會(huì)把悲觀鎖稱為獨(dú)占鎖,畢竟只有一個(gè)線程可以獨(dú)占這個(gè)資源,或者稱為阻塞鎖,因?yàn)樗鼤?huì)造成其他線程的阻塞。無論如何它都會(huì)造成并發(fā)能力的下降,從而導(dǎo)致 CPU頻繁切換線程上下文,造成性能低下。
為了克服這個(gè)問題,提高并發(fā)的能力,避免大量線程因?yàn)樽枞麑?dǎo)致 CPU 進(jìn)行大量的上下文切換,目前比較普遍的是樂觀鎖機(jī)制。
代碼
https://github.com/yangshangwei/ssm_redpacket
總結(jié)
以上是生活随笔為你收集整理的高并发-【抢红包案例】之二:使用悲观锁方式修复红包超发的bug的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高并发-【抢红包案例】之一:SSM环境搭
- 下一篇: 高并发-【抢红包案例】之三:使用乐观锁方