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