高併發業務--------搶紅包模擬
(大概要講解的東西,待更新)
悲觀鎖
悲觀鎖,假定會發生併發衝突,在你開始改變此物件之前就將該物件給鎖住,直到更改之後再釋放鎖。
利用資料庫內部機制提供的鎖方法,也就是對更新的資料加鎖,這樣在併發期間一旦有一個事務持有了資料庫記錄的鎖,其他執行緒將不能對資料進行更新。
悲觀鎖的實現方式: SQL + FOR UPDATE
<!--悲觀鎖--> <select id="getRedPacketForUpdate" parameterType="int" resultType="com.demo.entity.RedPacket"> select id, user_id as userId, amount, send_date as sendDate, total, unit_amount as unitAmount,stock, version, note from t_red_packet where id = #{id} for update </select>
根據加鎖的粒度,當對主鍵查詢進行加鎖時,意味著將持有對資料庫記錄的行更新鎖(因為這裡使用主鍵查詢,所以只會對行加鎖。如果使用的是非主鍵查詢,要考慮是否對全表加鎖的問題,加鎖後可能引發其他查詢的阻塞〉,那就意味著在高併發的場景下,當一條事務持有了這個更新鎖才能往下操作,其他的執行緒如果要更新這條記錄,都需要等待,這樣就不會出現超發現象引發的資料一致性問題了。
對於悲觀鎖來說,當一條執行緒搶佔了資源後,其他的執行緒將得不到資源,那麼這個時候, CPU 就會將這些得不到資源的執行緒掛起,掛起的執行緒也會消耗CPU 的資源,尤其是在高井發的請求中。
一旦執行緒l 提交了事務,那麼鎖就會被釋放,這個時候被掛起的執行緒就會開始競爭資源,那麼競爭到的執行緒就會被CPU 恢復到執行狀態,繼續執行。
於是頻繁掛起,等待持有鎖執行緒釋放資源,一旦釋放資源後,就開始搶奪,恢復執行緒,周而復始直至所有紅包資源搶完。試想在高併發的過程中,使用悲觀鎖就會造成大量的執行緒被掛起和恢復,這將十分消耗資源,這就是為什麼使用悲觀鎖效能不佳的原因。有些時候,我們也會把悲觀鎖稱為獨佔鎖,畢竟只有一個執行緒可以獨佔這個資源,或者稱為阻塞鎖,因為它會造成其他執行緒的阻塞。無論如何它都會造成併發能力的下降,從而導致CPU頻繁切換執行緒上下文,造成效能低下。為了克服這個問題,提高併發的能力,避免大量執行緒因為阻塞導致CPU進行大量的上下文切換,程式設計大師們提出了樂觀鎖機制,樂觀鎖已經在企業中被大量應用了。
樂觀鎖
樂觀鎖是一種不會阻塞其他執行緒併發的機制,它不會使用資料庫的鎖進行實現,它的設計裡面由於不阻塞其他執行緒,所以並不會引發執行緒頻繁掛起和恢復,這樣便能夠提高井發能力,所以也有人把它稱為非阻塞鎖。使用了CAS原理
實現方法:
1、樂觀鎖,無重入
讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提 交資料的版本資料與資料庫表對應記錄的當前版本資訊進行比對,如果提交的資料 版本號大於資料庫表當前版本號,則予以更新,否則認為是過期資料。
<!--樂觀鎖-->
<update id="decreaseRedPacketByVersion">
update t_red_packet
set
stock = stock - 1,
version = version + 1
where
id = #{id}
and version = #{version}
</update>
但是,僅僅這樣是不行的,在高併發的情景下,由於版本不一致的問題,存在大量紅包爭搶失敗的問題。為了提高搶紅包的成功率,我們加入重入機制。
2、樂觀鎖,通過時間戳重入
- 按時間戳重入(比如100ms時間內)
示例程式碼:
// 記錄開始的時間
long start = System.currentTimeMillis();
// 無限迴圈,當搶包時間超過100ms或者成功時退出
while(true) {
// 迴圈當前時間
long end = System.currentTimeMillis();
// 如果搶紅包的時間已經超過了100ms,就直接返回失敗
if(end - start > 100) {
return FAILED;
}
....
}
3、樂觀鎖,通過重試次數提高搶紅包成功率
- 按次數重入(比如3次機會之內)
示例程式碼:
// 允許使用者重試搶三次紅包
for(int i = 0; i < 3; i++) {
// 獲取紅包資訊, 注意version資訊
RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
// 如果當前的紅包大於0
if(redPacket.getStock() > 0) {
// 再次傳入執行緒儲存的version舊值給SQL判斷,是否有其他執行緒修改過資料
int update = redPacketDao.decreaseRedPacketByVersion(redPacketId, redPacket.getVersion());
// 如果沒有資料更新,說明已經有其他執行緒修改過資料,則繼續搶紅包
if(update == 0) {
continue;
}
....
}
...
}
使用Redis
總結
悲觀鎖使用了資料庫的鎖機制,可以消除資料不一致性,對於開發者而言會十分簡單,但是,使用悲觀鎖後,資料庫的效能有所下降,因為大量的執行緒都會被阻塞,而且需要有大量的恢復過程,需要進一步改變演算法以提高系統的井發能力。
使用樂觀鎖有助於提高併發效能,但是由於版本號衝突,樂觀鎖導致多次請求服務失敗的概率大大提高,而我們通過重入(按時間戳或者按次數限定)來提高成功的概率,這樣對於樂觀鎖而言實現的方式就相對複雜了,其效能也會隨著版本號衝突的概率提升而提升,並不穩定。使用樂觀鎖的弊端在於, 導致大量的SQL被執行,對於資料庫的效能要求較高,容易引起資料庫效能的瓶頸,而且對於開發還要考慮重入機制,從而導致開發難度加大。
使用Redis去實現高併發,消除了資料不一致性,並且在整個過程中儘量少的涉及資料庫。但是這樣使用的風險在於Redis的不穩定性,因為其事務和儲存都存在不穩定的因素,所以更多的時候,建議使用獨立Redis伺服器做高併發業務,一方面可以提高Redis的效能,另一方面即使在高併發的場合,Redis伺服器巖機也不會影響現有的其他業務,同時也可以使用備機等裝置提高系統的高可用,保證網站的安全穩定。