欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Redis實現(xiàn)分布式鎖方法詳細

 更新時間:2021年12月30日 10:40:55   作者:止步前行  
在單體應(yīng)用中,如果我們對共享數(shù)據(jù)不進行加鎖操作,會出現(xiàn)數(shù)據(jù)一致性問題,我們的解決辦法通常是加鎖。在分布式架構(gòu)中,我們同樣會遇到數(shù)據(jù)共享操作問題。本文將介紹Redis實現(xiàn)分布式鎖的五種方式。需要的可以參考一下

在單體應(yīng)用中,如果我們對共享數(shù)據(jù)不進行加鎖操作,會出現(xiàn)數(shù)據(jù)一致性問題,我們的解決辦法通常是加鎖。

在分布式架構(gòu)中,我們同樣會遇到數(shù)據(jù)共享操作問題,本文章使用Redis來解決分布式架構(gòu)中的數(shù)據(jù)一致性問題。

1. 單機數(shù)據(jù)一致性

單機數(shù)據(jù)一致性架構(gòu)如下圖所示:多個可客戶訪問同一個服務(wù)器,連接同一個數(shù)據(jù)庫。

場景描述:客戶端模擬購買商品過程,在Redis中設(shè)定庫存總數(shù)剩100個,多個客戶端同時并發(fā)購買。

@RestController
public class IndexController1 {

    @Autowired
    StringRedisTemplate template;

    @RequestMapping("/buy1")
    public String index(){
        // Redis中存有g(shù)oods:001號商品,數(shù)量為100
        String result = template.opsForValue().get("goods:001");
        // 獲取到剩余商品數(shù)
        int total = result == null ? 0 : Integer.parseInt(result);
        if( total > 0 ){
            // 剩余商品數(shù)大于0 ,則進行扣減
            int realTotal = total -1;
            // 將商品數(shù)回寫數(shù)據(jù)庫
            template.opsForValue().set("goods:001",String.valueOf(realTotal));
            System.out.println("購買商品成功,庫存還剩:"+realTotal +"件, 服務(wù)端口為8001");
            return "購買商品成功,庫存還剩:"+realTotal +"件, 服務(wù)端口為8001";
        }else{
            System.out.println("購買商品失敗,服務(wù)端口為8001");
        }
        return "購買商品失敗,服務(wù)端口為8001";
    }
}

使用Jmeter模擬高并發(fā)場景,測試結(jié)果如下:

測試結(jié)果出現(xiàn)多個用戶購買同一商品,發(fā)生了數(shù)據(jù)不一致問題!

解決辦法:單體應(yīng)用的情況下,對并發(fā)的操作進行加鎖操作,保證對數(shù)據(jù)的操作具有原子性

  • synchronized
  • ReentrantLock
@RestController
public class IndexController2 {

    // 使用ReentrantLock鎖解決單體應(yīng)用的并發(fā)問題
    Lock lock = new ReentrantLock();

    @Autowired
    StringRedisTemplate template;

    @RequestMapping("/buy2")
    public String index() {

        lock.lock();
        try {
            String result = template.opsForValue().get("goods:001");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                int realTotal = total - 1;
                template.opsForValue().set("goods:001", String.valueOf(realTotal));
                System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001");
                return "購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001";
            } else {
                System.out.println("購買商品失敗,服務(wù)端口為8001");
            }
        } catch (Exception e) {
            lock.unlock();
        } finally {
            lock.unlock();
        }
        return "購買商品失敗,服務(wù)端口為8001";
    }
}

2. 分布式數(shù)據(jù)一致性

上面解決了單體應(yīng)用的數(shù)據(jù)一致性問題,但如果是分布式架構(gòu)部署呢,架構(gòu)如下:

提供兩個服務(wù),端口分別為8001、8002,連接同一個Redis服務(wù),在服務(wù)前面有一臺Nginx作為負載均衡

兩臺服務(wù)代碼相同,只是端口不同

將8001、8002兩個服務(wù)啟動,每個服務(wù)依然用ReentrantLock加鎖,用Jmeter做并發(fā)測試,發(fā)現(xiàn)會出現(xiàn)數(shù)據(jù)一致性問題!

3. Redis實現(xiàn)分布式鎖

3.1 方式一

取消單機鎖,下面使用redis的set命令來實現(xiàn)分布式加鎖

SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]

EX seconds ? 設(shè)置指定的到期時間(以秒為單位)

PX milliseconds ? 設(shè)置指定的到期時間(以毫秒為單位)

NX ? 僅在鍵不存在時設(shè)置鍵

XX ? 只有在鍵已存在時才設(shè)置

@RestController
public class IndexController4 {

    // Redis分布式鎖的key
    public static final String REDIS_LOCK = "good_lock";

    @Autowired
    StringRedisTemplate template;

    @RequestMapping("/buy4")
    public String index(){

        // 每個人進來先要進行加鎖,key值為"good_lock",value隨機生成
        String value = UUID.randomUUID().toString().replace("-","");
        try{
            // 加鎖
            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value);
            // 加鎖失敗
            if(!flag){
                return "搶鎖失??!";
            }
            System.out.println( value+ " 搶鎖成功");
            String result = template.opsForValue().get("goods:001");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                int realTotal = total - 1;
                template.opsForValue().set("goods:001", String.valueOf(realTotal));
                // 如果在搶到所之后,刪除鎖之前,發(fā)生了異常,鎖就無法被釋放,
                // 釋放鎖操作不能在此操作,要在finally處理
				// template.delete(REDIS_LOCK);
                System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001");
                return "購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001";
            } else {
                System.out.println("購買商品失敗,服務(wù)端口為8001");
            }
            return "購買商品失敗,服務(wù)端口為8001";
        }finally {
            // 釋放鎖
            template.delete(REDIS_LOCK);
        }
    }
}

上面的代碼,可以解決分布式架構(gòu)中數(shù)據(jù)一致性問題。但再仔細想想,還是會有問題,下面進行改進。

3.2 方式二(改進方式一)

在上面的代碼中,如果程序在運行期間,部署了微服務(wù)jar包的機器突然掛了,代碼層面根本就沒有走到finally代碼塊,也就是說在宕機前,鎖并沒有被刪除掉,這樣的話,就沒辦法保證解鎖

所以,這里需要對這個key加一個過期時間,Redis中設(shè)置過期時間有兩種方法:

  • template.expire(REDIS_LOCK,10, TimeUnit.SECONDS)
  • template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS)

第一種方法需要單獨的一行代碼,且并沒有與加鎖放在同一步操作,所以不具備原子性,也會出問題

第二種方法在加鎖的同時就進行了設(shè)置過期時間,所有沒有問題,這里采用這種方式

調(diào)整下代碼,在加鎖的同時,設(shè)置過期時間:

// 為key加一個過期時間,其余代碼不變
Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK,value,10L,TimeUnit.SECONDS);

這種方式解決了因服務(wù)突然宕機而無法釋放鎖的問題。但再仔細想想,還是會有問題,下面進行改進。

3.3 方式三(改進方式二)

方式二設(shè)置了key的過期時間,解決了key無法刪除的問題,但問題又來了

上面設(shè)置了key的過期時間為10秒,如果業(yè)務(wù)邏輯比較復(fù)雜,需要調(diào)用其他微服務(wù),處理時間需要15秒(模擬場景,別較真),而當10秒鐘過去之后,這個key就過期了,其他請求就又可以設(shè)置這個key,此時如果耗時15秒的請求處理完了,回來繼續(xù)執(zhí)行程序,就會把別人設(shè)置的key給刪除了,這是個很嚴重的問題!

所以,誰上的鎖,誰才能刪除

@RestController
public class IndexController6 {

    public static final String REDIS_LOCK = "good_lock";

    @Autowired
    StringRedisTemplate template;

    @RequestMapping("/buy6")
    public String index(){

        // 每個人進來先要進行加鎖,key值為"good_lock"
        String value = UUID.randomUUID().toString().replace("-","");
        try{
            // 為key加一個過期時間
            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);

            // 加鎖失敗
            if(!flag){
                return "搶鎖失??!";
            }
            System.out.println( value+ " 搶鎖成功");
            String result = template.opsForValue().get("goods:001");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                // 如果在此處需要調(diào)用其他微服務(wù),處理時間較長。。。
                int realTotal = total - 1;
                template.opsForValue().set("goods:001", String.valueOf(realTotal));
                System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001");
                return "購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001";
            } else {
                System.out.println("購買商品失敗,服務(wù)端口為8001");
            }
            return "購買商品失敗,服務(wù)端口為8001";
        }finally {
            // 誰加的鎖,誰才能刪除!?。?!
            if(template.opsForValue().get(REDIS_LOCK).equals(value)){
                template.delete(REDIS_LOCK);
            }
        }
    }
}

這種方式解決了因服務(wù)處理時間太長而釋放了別人鎖的問題。這樣就沒問題了嗎?

3.4 方式四(改進方式三)

在上面方式三下,規(guī)定了誰上的鎖,誰才能刪除,但finally快的判斷和del刪除操作不是原子操作,并發(fā)的時候也會出問題,并發(fā)嘛,就是要保證數(shù)據(jù)的一致性,保證數(shù)據(jù)的一致性,最好要保證對數(shù)據(jù)的操作具有原子性。

在Redis的set命令介紹中,最后推薦Lua腳本進行鎖的刪除,地址:https://redis.io/commands/set

@RestController
public class IndexController7 {

    public static final String REDIS_LOCK = "good_lock";

    @Autowired
    StringRedisTemplate template;

    @RequestMapping("/buy7")
    public String index(){

        // 每個人進來先要進行加鎖,key值為"good_lock"
        String value = UUID.randomUUID().toString().replace("-","");
        try{
            // 為key加一個過期時間
            Boolean flag = template.opsForValue().setIfAbsent(REDIS_LOCK, value,10L,TimeUnit.SECONDS);
            // 加鎖失敗
            if(!flag){
                return "搶鎖失??!";
            }
            System.out.println( value+ " 搶鎖成功");
            String result = template.opsForValue().get("goods:001");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                // 如果在此處需要調(diào)用其他微服務(wù),處理時間較長。。。
                int realTotal = total - 1;
                template.opsForValue().set("goods:001", String.valueOf(realTotal));
                System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001");
                return "購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001";
            } else {
                System.out.println("購買商品失敗,服務(wù)端口為8001");
            }
            return "購買商品失敗,服務(wù)端口為8001";
        }finally {
            // 誰加的鎖,誰才能刪除,使用Lua腳本,進行鎖的刪除

            Jedis jedis = null;
            try{
                jedis = RedisUtils.getJedis();

                String script = "if redis.call('get',KEYS[1]) == ARGV[1] " +
                        "then " +
                        "return redis.call('del',KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";

                Object eval = jedis.eval(script, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value));
                if("1".equals(eval.toString())){
                    System.out.println("-----del redis lock ok....");
                }else{
                    System.out.println("-----del redis lock error ....");
                }
            }catch (Exception e){

            }finally {
                if(null != jedis){
                    jedis.close();
                }
            }
        }
    }
}

3.5 方式五(改進方式四)

在方式四下,規(guī)定了誰上的鎖,誰才能刪除,并且解決了刪除操作沒有原子性問題。但還沒有考慮緩存續(xù)命,以及Redis集群部署下,異步復(fù)制造成的鎖丟失:主節(jié)點沒來得及把剛剛set進來這條數(shù)據(jù)給從節(jié)點,就掛了。所以直接上RedLock的Redisson落地實現(xiàn)。

@RestController
public class IndexController8 {

    public static final String REDIS_LOCK = "good_lock";

    @Autowired
    StringRedisTemplate template;

    @Autowired
    Redisson redisson;

    @RequestMapping("/buy8")
    public String index(){

        RLock lock = redisson.getLock(REDIS_LOCK);
        lock.lock();

        // 每個人進來先要進行加鎖,key值為"good_lock"
        String value = UUID.randomUUID().toString().replace("-","");
        try{
            String result = template.opsForValue().get("goods:001");
            int total = result == null ? 0 : Integer.parseInt(result);
            if (total > 0) {
                // 如果在此處需要調(diào)用其他微服務(wù),處理時間較長。。。
                int realTotal = total - 1;
                template.opsForValue().set("goods:001", String.valueOf(realTotal));
                System.out.println("購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001");
                return "購買商品成功,庫存還剩:" + realTotal + "件, 服務(wù)端口為8001";
            } else {
                System.out.println("購買商品失敗,服務(wù)端口為8001");
            }
            return "購買商品失敗,服務(wù)端口為8001";
        }finally {
            if(lock.isLocked() && lock.isHeldByCurrentThread()){
                lock.unlock();
            }
        }
    }
}

3.6 小結(jié)

分析問題的過程,也是解決問題的過程,也能鍛煉自己編寫代碼時思考問題的方式和角度。

上述測試代碼地址:

https://github.com/Hofanking/springboot-redis-example?

到此這篇關(guān)于Redis實現(xiàn)分布式鎖方法詳細的文章就介紹到這了,更多相關(guān)Redis分布式鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • redis?lua腳本解決高并發(fā)下秒殺場景

    redis?lua腳本解決高并發(fā)下秒殺場景

    這篇文章主要為大家介紹了redis?lua腳本解決高并發(fā)下秒殺場景,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-10-10
  • 如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性

    如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性

    這篇文章主要介紹了如何保證Redis與數(shù)據(jù)庫的數(shù)據(jù)一致性,文中舉了兩個場景例子介紹的非常詳細,需要的朋友可以參考下
    2023-05-05
  • windows平臺安裝部署Redis

    windows平臺安裝部署Redis

    Redis是一個開源、跨平臺的數(shù)據(jù)庫,因此Redis數(shù)據(jù)庫可以運行在Windows、Linux、Mac OS和BSD等多個平臺上,本文主要介紹了windows平臺安裝部署Redis,具有一定的參考價值,感興趣的可以了解一下
    2023-10-10
  • Redis 執(zhí)行性能測試

    Redis 執(zhí)行性能測試

    這篇文章主要介紹了Redis 執(zhí)行性能測試的方法,文中講解非常細致,幫助大家更好的理解和學(xué)習(xí)redis,感興趣的朋友可以了解下
    2020-08-08
  • Redis的共享session應(yīng)用實現(xiàn)短信登錄

    Redis的共享session應(yīng)用實現(xiàn)短信登錄

    本文主要介紹了Redis的共享session應(yīng)用實現(xiàn)短信登錄,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-08-08
  • redis中修改配置文件中的端口號 密碼方法

    redis中修改配置文件中的端口號 密碼方法

    今天小編就為大家分享一篇redis中修改配置文件中的端口號 密碼方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2018-05-05
  • Redis模仿發(fā)送手機驗證碼功能

    Redis模仿發(fā)送手機驗證碼功能

    這篇文章主要介紹了Redis模仿手機驗證碼發(fā)送功能,通過示例代碼給大家講解通過用戶輸入手機號以及驗證碼進行校驗,代碼簡單易懂,需要的朋友可以參考下
    2021-09-09
  • 基于redis+lua進行限流的方法

    基于redis+lua進行限流的方法

    這篇文章主要介紹了基于redis+lua進行限流,通過實例代碼詳細介紹了lua+redis進行限流的做法,開發(fā)環(huán)境使用idea+redis+lua,本文給大家介紹的非常詳細,需要的朋友可以參考下
    2022-07-07
  • jedis配置含義詳解

    jedis配置含義詳解

    這篇文章主要介紹了jedis配置含義詳解的相關(guān)資料,需要的朋友可以參考下
    2020-04-04
  • Redis 哨兵集群的實現(xiàn)

    Redis 哨兵集群的實現(xiàn)

    Sentinel是Redis 的高可用性解決方案,本文詳細的介紹了redis哨兵集群的實現(xiàn),非常具有實用價值,需要的朋友可以參考下
    2021-06-06

最新評論