1. 程式人生 > >高併發業務--------搶紅包模擬

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

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

悲觀鎖

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

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

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