(大概要講解的東西,待更新)
悲觀鎖
悲觀鎖,假定會發生併發衝突,在你開始改變此對象之前就將該對象給鎖住,直到更改之後再釋放鎖。
利用數據庫內部機制提供的鎖方法,也就是對更新的數據加鎖,這樣在併發期間一旦有一個事務持有了數據庫記錄的鎖,其他線程將不能對數據進行更新。
悲觀鎖的實現方式: 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服務器巖機也不會影響現有的其他業務,同時也可以使用備機等設備提高系統的高可用,保證網站的安全穩定。