高併發業務--------搶紅包模擬

(大概要講解的東西,待更新)

悲觀鎖

悲觀鎖,假定會發生併發衝突,在你開始改變此對象之前就將該對象給鎖住,直到更改之後再釋放鎖。

利用數據庫內部機制提供的鎖方法,也就是對更新的數據加鎖,這樣在併發期間一旦有一個事務持有了數據庫記錄的鎖,其他線程將不能對數據進行更新。

悲觀鎖的實現方式: 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服務器巖機也不會影響現有的其他業務,同時也可以使用備機等設備提高系統的高可用,保證網站的安全穩定。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章