詳解redis分佈式鎖(優化redis分佈式鎖的過程及Redisson使用)

1. redis在實際的應用中

不僅可以用來緩存數據,在分佈式應用開發中,經常被用來當作分佈式鎖的使用,為什麼要用到分佈式鎖呢?

在分佈式的開發中,以電商庫存的更新功能進行講解,在實際的應用中相同功能的消費者是有多個的,假如多個消費者同一時刻要去消費一條數據,假如業務邏輯處理邏輯是查詢出redis中的商品庫存,而如果第一個進來的消費的消費者獲取到庫存瞭,還沒進行減庫存操作,相對晚來的消費者就獲取瞭商品的庫存,這樣就導致數據會出錯,導致消費的數據變多瞭。

例如:消費者A和消費者B分別去消費生產者C1和生產者C2的數據,而生產者都是使用同一個redis的數據庫的,如果生產者C1接收到消費者A的消息後,先進行查詢庫存,然後當要進行減庫存的時候,因為生產者C2接收到消費者B的消息後,也去查詢庫存,而因為生產者C1還沒有進行庫存的更新,導致生產者C2獲取到的庫存數是臟數據,而不是生產者C1更新後的數據,導致業務出錯。

在這裡插入圖片描述

如果不是分佈式的應用,可以使用synchronized進行防止庫存更新的問題的產生,但是synchronized隻是基於JVM層面的,如果在不同的JVM中,就不能實現這樣的功能。

   @GetMapping("getInt0")
    public String test() {
        synchronized (this) {
            //獲取當前商品的數量
            int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
            //然後對商品進行出庫操作,即進行減1
            /*
             * a業務邏輯
             *
             * */
            if (productNum > 0) {
                stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
                int productNumNow = productNum - 1;
            } else {
                return "product=0";
            }
            int productNumNow = productNum - 1;
            return "success=" + productNumNow;
        }
    }

2.如何使用redis的功能進行實現分佈式鎖

2.1 redis分佈式鎖思想

如果對redis熟悉的話,我們能夠想到redis中具有setnx的命令,該命令的功能宇set功能類似,但是setnx的命令在進行存數據前,會檢查redis中是否已經存在相同的key,如存在的話就返回false,反之則返回true,因此我們可以使用該命令的功能,設計一個分佈式鎖。

2.1.1設計思想:

  • 在請求相同功能的接口時,使用redis的setnx命令,如果使用setnx命令後返回的是為true,說明此時沒有其他的調用這個接口,就相當於獲取到鎖瞭,然後就可以繼續執行接下來的業務邏輯瞭。當執行完業務邏輯後,在返回數據前,就把key刪除瞭,然後其他的請求就能獲取到鎖瞭。
  • 如果使用setnx命令,返回的是false,說明此時有其他的消費者正在調用這個接口,因此需要等待其他消費者順利消費完成後,才能獲取到分佈式的鎖。

2.1.2 根據上面的設計思想進行代碼實現

代碼片段【1】

  @GetMapping("getInt1")
    public String fubushisuo(){
        //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經存在相同的key,則返回false
        String lockkey = "yigehaimeirumengdechengxuyuan";
        String lockvalue = "yigehaimeirumengdechengxuyuan";
        boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue);
        //如果能夠成功的設置lockkey,這說明當前獲取到分佈式鎖
        if (!opsForSet){
            return "false";
        }
        //獲取當前商品的數量
        int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
        //然後對商品進行出庫操作,即進行減1
        /*
        * a業務邏輯
        *
        * */
        if (productNum>0){
            stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
            int productNumNow = productNum - 1;
        }else {
            return "product=0";
        }
        //然後進行釋放鎖
        stringRedisTemplate.delete(lockkey);
        int productNumNow = productNum-1;
        return "success="+productNumNow;
    }


2.1.2.1反思代碼片段【1】

如果使用這種方式,會產生死鎖的方式:
死鎖發生的情況:
(1) 如果在a業務邏輯出現錯誤時,導致不能執行delete()操作,使得其他的請求不能獲取到分佈式鎖,業務lockkey一直存在於reids中,導致setnx操作一直失敗,所以不能獲取到分佈式鎖
(2) 解決方法,使用對業務代碼進行try…catch操作,如果出現錯誤,那麼使用finally對key進行刪除

優化代碼【2】

     @GetMapping("getInt2")
    public String fubushisuo2(){
        //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經存在相同的key,則返回false
        String lockkey = "yigehaimeirumengdechengxuyuan";
        String lockvalue = "yigehaimeirumengdechengxuyuan";
        boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue);
        int productNumNow = 0;
        //如果能夠成功的設置lockkey,這說明當前獲取到分佈式鎖
        if (!opsForSet){
            return "false";
        }
        try {
            //獲取當前商品的數量
            int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
            //然後對商品進行出庫操作,即進行減1
            /*
             * b業務邏輯
             * */
            if (productNum>0){
                stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
                productNumNow = productNum-1;
            }else {
                return "product=0";
            }

        }catch (Exception e){
            System.out.println(e.getCause());
        }finally {
                //然後進行釋放鎖
            stringRedisTemplate.delete(lockkey);
        }

        return "success="+productNumNow;
    }
2.1.2.2反思代碼【2】

出現問題的情況:
如果這種情況也有會產生的情況,如果此時有多臺服務器都在運行該方法,
其中有一個方法獲取到瞭分佈式鎖,而在運行下面的業務代碼時,此時該服務器突然宕機瞭,導致其他的不能獲取到分佈式鎖,

解決方法:加上過期時間,但又服務宕機瞭,過瞭設置的時間後,redis會可以把key給刪除,這樣其他的的服務器就可以正常的進行上鎖瞭。

優化代碼【3】

 @GetMapping("getInt3")
    public String fubushisuo3(){
        //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經存在相同的key,則返回false
        String lockkey = "yigehaimeirumengdechengxuyuan";
        String lockvalue = "yigehaimeirumengdechengxuyuan";
       //[01] boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey,lockvalue);
        //設置過期時間為10秒,但是如果使用該命令,沒有原子性,可能執行expire前宕機瞭,而不是設置過期時間,
       //[02] stringRedisTemplate.expire(lockkey, Duration.ofSeconds(10));
        //使用setIfAbsent(lockkey,lockvalue,10,TimeUnit.SECONDS);代碼代替上面[01],[02]行代碼
        Boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, lockvalue, 10, TimeUnit.SECONDS);
        int productNumNow = 0;
        //如果能夠成功的設置lockkey,這說明當前獲取到分佈式鎖
        if (!opsForSet){
            return "false";
        }
        try {
            //獲取當前商品的數量
            int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
            //然後對商品進行出庫操作,即進行減1
            /*
             * c業務邏輯
             * */
            if (productNum>0){
                stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
                productNumNow = productNum-1;
            }else {
                return "product=0";
            }

        }catch (Exception e){
            System.out.println(e.getCause());
        }finally {
        
        //然後進行釋放鎖
            stringRedisTemplate.delete(lockkey);
        }
        
        return "success="+productNumNow;
    }

2.1.2.3 反思優化代碼【3】

出現問題的情況:
如果c業務邏輯持續超過瞭設置時間,導致redis中的lockkey過期瞭,
而其他的用戶此時訪問該方法時獲取到鎖瞭,而在此時,之前的的c業務邏輯也執行完成瞭,但是他會執行delete,把lcokkey刪除瞭。導致分佈式鎖出錯。
例子:在12:01:55的時刻,有一個A來執行該getInt3方法,並且成功獲取到鎖,但是A執行瞭10秒後還不能完成業務邏輯,導致redis中的鎖過期瞭,而在11秒的時候有B來執行getint3方法,因為key被A刪除瞭,導致B能夠成功的獲取redis鎖,而在B獲取鎖後,A因為執行完成瞭,然後把reids中的key給刪除瞭,但是我們註意的是,A刪除的鎖是B加上去的,而A的鎖是因為過期瞭,才被redis自己刪除瞭,因此這導致瞭C如果此時來時也能獲取redis分佈式鎖

解決方法:使用UUID,產生一個隨機數,當要進行delete(刪除)redis中key時,判斷是不是之前自己設置的UUID

代碼優化【4】

  @GetMapping("getInt4")
    public String fubushisuo4(){
        //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經存在相同的key,則返回false
        String lockkey = "yigehaimeirumengdechengxuyuan";
        //獲取UUID
        String lockvalue = UUID.randomUUID().toString();
        Boolean opsForSet = stringRedisTemplate.opsForValue().setIfAbsent(lockkey, lockvalue, 10, TimeUnit.SECONDS);
        int productNumNow = 0;
        //如果能夠成功的設置lockkey,這說明當前獲取到分佈式鎖
        if (!opsForSet){
            return "false";
        }
        try {
            //獲取當前商品的數量
            int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
            //然後對商品進行出庫操作,即進行減1
            /*
             * c業務邏輯
             * */

            if (productNum>0){
                stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
                productNumNow = productNum-1;
            }else {
                return "product=0";
            }

        }catch (Exception e){
            System.out.println(e.getCause());
        }finally {
        
        //進行釋放鎖
            if (lockvalue==stringRedisTemplate.opsForValue().get(lockkey)){
                stringRedisTemplate.delete(lockkey);
            }
        }
        return "success="+productNumNow;
    }
2.1.2.4 反思優化代碼【4】

出現問題的情況:
此時該方法是比較完美的,一般並發不是超級大的情況下都可以進行使用,但是關於key的過期時間需要根據業務執行的時間,進行設置,防止在業務還沒執行完時,key就過期瞭.

解決方法:目前有很多redis的分佈式鎖的框架,其中redisson用的是比較多的

2.2 使用redisson進行實現分佈式鎖

先添加redisson的maven依賴

        <dependency>
			<groupId>org.redisson</groupId>
			<artifactId>redisson</artifactId>
			<version>3.11.1</version>
		</dependency>

redisson的bean配置

@Configuration
public class RedissonConfigure {
    @Bean
    public Redisson redisson(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://27.196.106.42:6380").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }
}

實現分佈式鎖代碼如下

 @GetMapping("getInt5")
    public String fubushisuo5(){
        //setIfAbsent的指令功能和redis命令中的setNx功能一樣,如果redis中已經存在相同的key,則返回false
        String lockkey = "yigehaimeirumengdechengxuyuan";
        //獲取UUID
        RLock lock = redisson.getLock(lockkey);
        lock.lock();
        int productNumNow = 0;
        //如果能夠成功的設置lockkey,這說明當前獲取到分佈式鎖

        try {
            //獲取當前商品的數量
            int productNum = Integer.parseInt(stringRedisTemplate.opsForValue().get("product"));
            //然後對商品進行出庫操作,即進行減1
            /*
             * c業務邏輯
             * */
            if (productNum>0){
            stringRedisTemplate.opsForValue().set("product", String.valueOf(productNum - 1));
            productNumNow = productNum-1;
            }else {
                return "product=0";
            }
        }catch (Exception e){
            System.out.println(e.getCause());
        }finally {
           lock.unlock();
            }

        //然後進行釋放鎖
        return "success="+productNumNow;
    }

從面就能看到,redisson實現分佈式鎖是非常簡單的,隻要簡單的幾條命令就能實現分佈式鎖的功能的。
redisson實現分佈式鎖的隻要原理如下:
redisson使用瞭Lua腳本語言使得命令既有原子性,redisson獲取鎖時,會給key設置30秒的過期是按,同時redisson會記錄當前請求的線程編號,然後定時的去檢查該線程的狀態,如果還處於執行狀態的話,而且key差不多要超期過時時,redisson會修改key的過期時間,一般增加10秒。這樣就可以動態的設置key的過期時間瞭,彌補瞭優化代碼【4】的片段

到此這篇關於redis分佈式鎖詳解(優化redis分佈式鎖的過程及Redisson使用)的文章就介紹到這瞭,更多相關redis分佈式鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: