day07-優惠券秒殺03

来源:https://www.cnblogs.com/liyuelian/archive/2023/04/27/17360219.html
-Advertisement-
Play Games

功能03-優惠券秒殺03 4.功能03-優惠券秒殺 4.6Redisson的分散式鎖 Redis分散式鎖—Redisson+RLock可重入鎖實現篇 4.6.1基於setnx實現的分散式鎖問題 我們在4.5自己實現的分散式鎖,主要使用的是redis的setnx命令,它仍存在如下問題: 4.6.2Re ...


功能03-優惠券秒殺03

4.功能03-優惠券秒殺

4.6Redisson的分散式鎖

Redis分散式鎖—Redisson+RLock可重入鎖實現篇

4.6.1基於setnx實現的分散式鎖問題

我們在4.5自己實現的分散式鎖,主要使用的是redis的setnx命令,它仍存在如下問題:

image-20230426162358885

4.6.2Redisson基本介紹

Redisson是一個在Redis基礎上實現的Java駐記憶體數據網格(In-Memory Data Grid)。它不僅提供了一系列的分散式的Java常用對象,還提供了許多分散式服務,其中就包括了各種分散式鎖的實現。

一句話:Redisson是一個在Redis基礎上實現的分散式工具的集合。

據Redisson官網的介紹,Redisson是一個Java Redis客戶端,與Spring 提供給我們的 RedisTemplate 工具沒有本質的區別,可以把它看做是一個功能更強大的客戶端

官網地址: https://redisson.org

GitHub地址: https://github.com/redisson/redisson

中文文檔:目錄 · redisson/redisson Wiki (github.com)

image-20230426165355989

4.6.3Redisson快速入門

image-20230426165951083 image-20230426165957517

代碼實現

(1)修改pom.xml,添加依賴

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

(2)配置Redisson

package com.hmdp.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 李
 * @version 1.0
 */
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        //配置
        Config config = new Config();
        //redis單節點模式,設置redis伺服器的地址,埠,密碼
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456");
        //創建RedissonClient對象
        return Redisson.create(config);
    }
}

配置了之後,就可以在任意地方去使用Redisson了:比如說去改造之前的業務,使用Redisson的分散式鎖

(3)修改VoucherOrderServiceImpl.java,使用Redisson的分散式鎖

註入RedissonClient對象:

image-20230426172512742

使用RedissonClient提供的鎖:

image-20230426172923552

(4)使用jemeter測試

分別向埠為8081、8082的伺服器發送200個請求(使用同一個用戶的token)

image-20230426173542561 image-20230426174359784

資料庫中只下了一單:

image-20230426174644108

說明解決了集群下的一人一單問題。

4.6.4Redisson可重入鎖原理(Reentrant Lock)

可重入鎖:字面意思是“可以重新進入的鎖”,即允許同一個線程多次獲取同一把鎖

比如一個遞歸函數里有加鎖操作,遞歸過程中這個鎖會阻塞自己嗎?如果不會,那麼這個鎖就是可重入鎖(因為這個原因可重入鎖也叫做遞歸鎖)。

Lock鎖藉助於底層一個voaltile的state變數來記錄重入狀態。如果當前沒有線程持有這把鎖,那麼state=0,假如有線程持有這把鎖,那麼state=1,如果持有這把鎖的線程再次持有這把鎖,那麼state就會+1 。

對於synchronized而言,它在c語言代碼中會有一個count,原理和state類似,也是重入一次就加一,釋放一次就減一 ,直到減少成零時,表示當前這把鎖沒有被人持有。


Redisson也支持可重入鎖。Redisson在分散式鎖中,採用redis的hash結構用來存儲鎖,其中key表示這把鎖是否存在,用field表示當前這把鎖被哪個線程持有,value記錄重入的次數(鎖計數)。當獲取鎖的線程釋放鎖前,先對鎖計數-1,然後判斷鎖計數0,如果是0,就釋放鎖。

image-20230426182822283

使用Redis的string類型的setnx命令,可以實現互斥性,ex可以設置過期時間。但如果使用hash結構,該結構中沒有類似的組合命令,因此只能將之前的邏輯拆開。先判斷是否存在,然後手動設置過期時間,邏輯如下:

image-20230426185252337

可以看到,無論是獲取鎖還是釋放鎖,都比使用setnx實現的分散式鎖複雜得多,而且實現需要有多個步驟。

因此,需要採用lua腳本來確保獲取鎖和釋放鎖的原子性:

  • 獲取鎖的lua腳本
local key = KEYS[1]; -- 鎖的key
local threadId = ARGV[1]; -- 線程唯一標識
local releaseTime = ARGV[2]; -- 鎖的自動釋放時間
-- 判斷是否存在
-- 鎖不存在
if(redis.call('exists', key) == 0) then
    -- 不存在, 獲取鎖
    redis.call('hset', key, threadId, '1'); 
    -- 設置有效期
    redis.call('expire', key, releaseTime); 
    return 1; -- 返回結果
end;
-- 鎖已經存在,判斷threadId是否是自己
if(redis.call('hexists', key, threadId) == 1) then
    -- 如果是自己, 獲取鎖,重入次數+1
    redis.call('hincrby', key, threadId, '1'); -- hincrby命令是對哈希表指定的field對應的value增長指定步長
    -- 重新設置有效期
    redis.call('expire', key, releaseTime); 
    return 1; -- 返回結果
end;
return 0; -- 代碼走到這裡,說明獲取鎖的不是自己,獲取鎖失敗
  • 釋放鎖的腳本
local key = KEYS[1]; -- 鎖的key
local threadId = ARGV[1]; -- 線程唯一標識
local releaseTime = ARGV[2]; -- 鎖的自動釋放時間
-- 判斷當前鎖是否還是被自己持有
if (redis.call('HEXISTS', key, threadId) == 0) then
    return nil; -- 如果已經不是自己,則直接返回,不進行操作
end;
-- 是自己的鎖,則重入次數-1
local count = redis.call('HINCRBY', key, threadId, -1);
-- 然後判斷重入次數是否已經為0 
if (count > 0) then
-- 大於0,說明不能釋放鎖,重置有效期然後返回
    redis.call('EXPIRE', key, releaseTime);
    return nil;
else  -- 等於0,說明可以釋放鎖,直接刪除
    redis.call('DEL', key);
    return nil;
end;

代碼測試

我們來測試一下Redisson的可重入鎖:

package com.hmdp;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @author 李
 * @version 1.0
 */
@Slf4j
@SpringBootTest
class RedissonTest {
    @Resource
    private RedissonClient redissonClient;

    private RLock lock;

    @BeforeEach
    void setUp() {
        lock = redissonClient.getLock("order");
    }

    @Test
    void method1() throws InterruptedException {
        // 嘗試獲取鎖
        boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
        if (!isLock) {
            log.error("獲取鎖失敗 .... 1");
            return;
        }
        try {
            log.info("獲取鎖成功 .... 1");
            method2();
            log.info("開始執行業務 ... 1");
        } finally {
            log.warn("準備釋放鎖 .... 1");
            lock.unlock();
        }
    }

    void method2() {
        // 嘗試獲取鎖
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("獲取鎖失敗 .... 2");
            return;
        }
        try {
            log.info("獲取鎖成功 .... 2");
            log.info("開始執行業務 ... 2");
        } finally {
            log.warn("準備釋放鎖 .... 2");
            lock.unlock();
        }
    }
}

在method1()的boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);旁打上斷點:

image-20230426192522190

點擊step over,顯示獲取鎖成功:

image-20230426193043977

打開redis,可以看到對應的hash數據,value記錄的是線程重入鎖的次數,此時value=1:

image-20230426201416602

當前線程在method1()中調用method2()後,在method2()中重新獲取鎖,此時value記錄的次數+1,value=2:

image-20230426201442415

當method2()釋放鎖的時候,鎖重入次數-1,value=1:

image-20230426201523274

當執行到method1()釋放鎖的時候,鎖重入次數-1,此時發現鎖重入次數value=0,因此刪除對應的key,真正釋放鎖。

image-20230426201831797

我們進入RedissonLock的源碼,發現裡面也寫了相關的lua腳本,這裡的腳本和上面我們寫的基本一致:

獲取鎖的腳本:

image-20230426202915098

釋放鎖的腳本:

image-20230426203133946

4.6.5Redisson的鎖重試和WatchDog機制

(1)為什麼需要WatchDog機制?

如果拿到分散式鎖的節點宕機,且這個鎖正好處於鎖住的狀態時,就會出現死鎖問題。為了避免這種情況的發生,我們通常都會給鎖設置一個過期時間。但隨之而來又產生了新的問題:假如一個線程拿到了鎖並設置了30s超時,但在30s後這個線程的業務沒有執行完畢,鎖已經超時釋放了。可能會導致其他線程搶到鎖,然後出現多線程併發的問題。

為瞭解決這種兩難的境地:Redisson提供了watch dog 自動延期機制。

(2)WatchDog的自動延期機制

redisson中的看門狗機制總結

Redisson提供了一個監控鎖的看門狗,它的作用是在Redisson實例被關閉前,不斷的延長鎖的有效期。也就是說,如果一個拿到鎖的線程一直沒有完成邏輯,那麼看門狗會幫助線程不斷的延長鎖超時時間,鎖不會因為超時而被釋放。如果獲取到分散式鎖的節點宕機了,看門狗就無法延長鎖的有效期,也避免了死鎖的可能。

watchDog 只有在未指定加鎖時間(leaseTime)時才會生效

預設情況下,看門狗的續期時間是30s,也可以通過修改Config.lockWatchdogTimeout來另行指定。另外Redisson 還提供了可以指定leaseTime參數的加鎖方法來指定加鎖的時間。超過這個時間後鎖便自動解開了,不會延長鎖的有效期。


(3)鎖重試機制:利用信號量和PubSub功能實現等待、喚醒,獲取鎖失敗的重試機制

(4)Redisson實例獲取鎖和釋放鎖的流程

(4.1)獲取鎖的邏輯:

  • 首先去嘗試獲取鎖,如果返回的ttl為null,則說明成功獲取鎖,然後需要判斷是否走看門狗機制:
    • 如果我們自己設置了leaseTime,就不會開啟watchDog機制,直接返回true;
    • 如果設置的leaseTime=-1,則開啟watchDog,不停地更新有效期,然後返回true
  • 如果返回的ttl不為null,說明獲取鎖失敗。需要重試獲取,在重試之前要先判斷線程剩餘的等待時間:
    • 如果剩餘等待時間<=0,說明該線程沒有機會獲取鎖了,直接返回false;
    • 如果如果剩餘時間>0,就可以去嘗試重新獲取鎖了。但是不是立即吃重試獲取,需要去等待鎖釋放的信號
      • 如果在等待中,等待時間大於了剩餘等待時間,則直接返回false;
      • 如果收到了釋放鎖的信號,並且如果等待時間小於剩餘等待時間,就重新開始嘗試獲取鎖

重覆上述所有步驟。最終線程要麼成功獲取鎖,要麼超時返回。

(4.2)釋放鎖的邏輯

嘗試釋放鎖:

  • 如果失敗,記錄異常,結束
  • 如果成功,向等待的其他線程發送釋放鎖信息。然後取消watchDog機制,結束
image-20230427164719850

4.6.6Redisson分散式鎖總結

Redisson分散式鎖原理:

  • 可重入:利用hash結構記錄線程id和重入次數
  • 可重試:利用信號量和PubSub功能實現等待、喚醒,獲取鎖失敗的重試機制
  • 超時續約:利用watchDog,每隔一段時間(releaseTime/3),重置超時時間

4.6.7Redisson的聯鎖原理(multiLock)

上面我們已經介紹了Redisson分散式鎖如何實現鎖的可重入,鎖獲取時的重試,以及鎖釋放時間的自動續約。

現在來分析一下Redisson怎麼解決主從一致性問題。要解決這個問題,主從一致性問題產生:

(1)主從一致性問題

為避免單節點的redis服務宕機,從而影響依賴於redis的業務的執行(如分散式鎖),在實際開發中,我們往往會搭建Redis的主從模式。

什麼叫做Redis的主從模式?

有多台Redis,將其中一臺Redis伺服器作為主節點,其他的作為從節點。一般主節點負責寫入數據,從節點負責讀取數據,當主節點伺服器寫入數據時會同步到從節點的伺服器上。

一文讀懂Redis的四種模式,單機、主從、哨兵、集群

但主從節點畢竟不是在同一臺機器上,它們之間的數據同步會有一定的延時,主從一致性問題正是由於這樣的延時而導致的:

假設有一個Java應用現在要來獲取鎖,它向主節點間發送了一個寫命令:set lock thread1 nx ex 10,主節點上保存了這個鎖的標識,然後主節點向從節點同步數據,但就在這時主節點宕機了。也就是說同步未完成,但主節點已經宕機了。

image-20230427184016533

redis中的哨兵監控著整個集群的狀態,它發現主節點宕機之後,首先斷開與客戶端的連接,然後在Redis Slave中選擇一個當做新的主節點。

但是由於之前的主從同步未完成——也就是說鎖已經丟失了。所以,此時我們的Java應用再來訪問這個新的主節點時就會發現,鎖已經沒有了(鎖失效了)。那麼此時再有其他線程來獲取鎖也能獲取成功,因此就會出現線程的併發安全問題——這就是主從一致性問題導致的鎖失效問題

image-20230427184745271

(2)MultiLock鎖

既然主從關係是一致性問題發生的原因,那麼就不要使用主從節點了。我們將所有的節點都變為獨立的redis節點,相互之間沒有任何關係,都可以去做讀寫,每個節點的地位都是一樣的。

此時我們獲取鎖的方式就改變了:獲取鎖時,要把加鎖的邏輯寫入到每一個獨立的Redis節點上,只有所有的伺服器都寫入成功,此時才是加鎖成功。


  1. 假設現在某個節點掛了,那麼去獲得鎖的時候,只要有一個節點拿不到,都不能算是加鎖成功,保證了加鎖的可靠性。

  2. 因為沒有主從節點,也就不會出現一致性問題;其次,隨著redis節點的增多,redis可用性也提高了。

  3. 為了提高可用性,我們也可以對所有獨立的Redis Node分別建立主從關係,讓它們去做主從同步。

    那麼獨立的Redis Node的主從關係會不會導致鎖失效呢?

    我們假設此時有一個Redis Node宕機了,並且它的數據沒有同步到它的從節點。這時如果有其他線程想去獲取鎖,因為在其他Redis Node上不能拿到鎖,因此不算是獲取鎖成功。也就是說,只要有任意一個節點在存活著,那麼其他線程就不能趁機拿到鎖,解決了鎖失效問題。

這樣的方案保留了既主從同步機制,確保了Redis集群高可用的特性,同時也避免了主從一致引發的鎖失效問題。這套方案在Redisson中被稱為MultiLock鎖(聯鎖):redisson中的MultiLock,可以把一組鎖當作一個鎖來加鎖和釋放。

image-20230427205114898

那麼MutiLock 加鎖原理是什麼呢?筆者畫了一幅圖來說明

當我們去設置了多個鎖時,redission會將多個鎖添加到一個集合中,然後用while迴圈去不停去嘗試拿鎖,但是會有一個總共的加鎖時間,這個時間是用需要加鎖的個數 * 1500ms ,假設有3個鎖,那麼時間就是4500ms,假設在這4500ms內,所有的鎖都加鎖成功, 那麼此時才算是加鎖成功,如果在4500ms有線程加鎖失敗,則會再次去進行重試

1653553093967

4.7Redis優化秒殺

4.8Redis消息隊列實現非同步秒殺


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一般方法 System.Windows.Forms.Screen類 // 獲取當前主屏幕解析度 int screenWidth = Screen.PrimaryScreen.Bounds.Width; int screenHeight = Screen.PrimaryScreen.Bounds.He ...
  • 本文演示通過PowerShell+Docker Desktop for Windows 一鍵部署Sitecore10.3(即Sitecore最新版)Docker開發/測試/演示 環境。 官方參考 SitecoreXP 10.3.0 Developer Workstation Deployment W ...
  • (網路相關命令) 前言 這期呢主要說一說Linux中與網路相關命令,一共包含19個命令 測試主機之間網路是否聯通 1、簡介 ping 命令不管是在Windows還是Linux都是比較常用的命令。命令用於測試主機之間的網路連通性 2、語法格式 ping [參數選項] [目標主機] 3、參數說明 | 參 ...
  • 哈嘍大家好,我是鹹魚 今天我們來學習一下 Linux 操作系統核心之一:記憶體 跟 CPU 一樣,記憶體也是操作系統最核心的功能之一,記憶體主要用來存儲系統和程式的指令、數據、緩存等 關於記憶體的學習,我會儘量以通俗易懂的方式且分成多篇文章去講解 那麼今天在 pt.1 文章中,我們來學習一下 Linux 中 ...
  • 本文分享自天翼雲開發者社區《RPM常用命令以及組合使用場景》,作者:鄔祥釗 當涉及到管理基於 Red Hat 系的 Linux 系統時,RPM (Red Hat Package Manager) 是一個常用的軟體包管理器。以下是一些常用的 RPM 命令以及它們的組合使用場景: 常用命令: 1. rp ...
  • 本人習慣了Linux環境的du命令,在HP-UX下,發現du命令真的非常難用,有種讓人很難受的感覺。主要是因為HP-UX下的du命令參數比Linux平臺du命令參數要少很多,尤其是沒有-h這個參數,它只能以kb形式顯示文件/文件夾的大小,對於我來說,看起來非常不直觀。下麵是工作中,HP-UX平臺使用 ...
  • 環境: 工具:Firefox 84版本 或者 360游覽器-某特殊版本 系統版本:Windows 10 問題描述: 描述:進入某需要Flash插件的管理網站,使用edge等最新主流游覽器均無法調用Flash,開啟IE模式也沒有做用,網上的其他教程也因為時效性已經無法生效。 問題解釋: 解釋:各大主流 ...
  • 環境: 系統版本:Windows 10 家庭中文版 問題描述: 描述:按下Win+G後彈出提示框,需要使用新應用以打開此 ms-gamingoverlay 鏈接 問題解釋: 誤將Xbox game bar應用程式刪除了,無法找到啟動的程式。 這個程式我當時以為就是個打游戲的,我就刪除了,沒想到錄屏會 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...