基於redis的分佈式鎖

爲什麼要使用分佈式鎖?

因爲服務器使用了集羣方案。詞窮。。。

怎麼使用分佈式鎖?

需求

實現一個查詢數據庫,在大於0的情況下減庫存這樣小小的功能。

測試:模擬100併發並看結果

基礎代碼

沒有任何鎖


    @RequestMapping("/reduce_stock")
    public String reduceStock() {
        //查數據庫(redis)中庫存數量
        Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        //判斷庫存
        if (stock > 0) {
            System.out.println("消費庫存成功--->" + stock);
            //更新庫存
            stock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
        } else {
            System.out.println("消費庫存失敗。。。");
        }


        return "helloworld";
    }

用測壓工具測壓結果:出現併發問題

有鎖:給方法添加synchronized關鍵字

    @RequestMapping("/reduce_stock")
    public synchronized    String  reduceStock() {
        //查數據庫(redis)中庫存數量
        Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        //判斷庫存
        if (stock > 0) {
            System.out.println("消費庫存成功--->" + stock);
            //更新庫存
            stock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
        } else {
            System.out.println("消費庫存失敗。。。");
        }
        return "helloworld";
    }

單機測試結果:沒有問題

分佈式測試結果:出現線程安全問題

分析,如下圖所示:

兩個微服務,synchronized關鍵字只能鎖住一個微服務,跨微服務是鎖不住的。

就像你家的屋子A複製一份爲B,A是否鎖門和B是否鎖門是沒有關係的。

基於redis的分佈式鎖(理論+實操)

理論

基於redis的setnx命令實現分佈式鎖

setnx命令的特點是:當你第一次設置的時候會返回1,後面在設置的時候就會返回0(即修改失敗),如下圖所示

手寫基於redis分佈式鎖(此處邏輯、理論大於實操)

一代代碼

分析

---邏輯:先獲取鎖,如果獲取鎖,就繼續;否則就不執行

---問題:容易出現死鎖。如果我獲取鎖成功後在執行業務邏輯的過程中出現異常,則釋放鎖的過程就沒有了,不釋放鎖就會引起死鎖

   @RequestMapping("/reduce_stock")
    public String reduceStock() {

        //key的名稱
        String lockKey = "lock";
        //setnx key value    加鎖邏輯
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1");
        if (!aBoolean){
            return "fail";
        }

        //查數據庫(redis)中庫存數量
        Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
        //判斷庫存
        if (stock > 0) {
            System.out.println("消費庫存成功--->" + stock);
            //更新庫存
            stock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
        } else {
            System.out.println("消費庫存失敗。。。");
        }

        // del key  釋放鎖邏輯
        stringRedisTemplate.delete(lockKey);

        return "helloworld";
    }

二代代碼

分析:

---優點:在finally中釋放鎖,解決了死鎖的問題

---問題:引起鎖失效問題。看下面的代碼,先加鎖,如果加鎖失敗,返回,但是此時代碼也會去執行finally中釋放鎖的功能,從而使別人加的鎖失效。

    @RequestMapping("/reduce_stock")
    public String reduceStock() {

        //key的名稱
        String lockKey = "lock";

        try {
            //setnx key value    加鎖邏輯
            Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1");
            if (!aBoolean) {
                return "fail";
            }

            //查數據庫(redis)中庫存數量
            Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            //判斷庫存
            if (stock > 0) {
                System.out.println("消費庫存成功--->" + stock);
                //更新庫存
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
            } else {
                System.out.println("消費庫存失敗。。。");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            // del key  釋放鎖邏輯
            stringRedisTemplate.delete(lockKey);
        }


        return "helloworld";
    }

 三代代碼

分析:

--優點:解決了鎖失效問題

--問題:沒有解決因爲宕機而引起的死鎖,如下圖所示,微服務8082獲取鎖後在執行業務邏輯時系統宕機後就會引起死鎖

    @RequestMapping("/reduce_stock")
    public String reduceStock() {

        //key的名稱
        String lockKey = "lock";


        //setnx key value    加鎖邏輯
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1");
        if (!aBoolean) {
            return "fail";
        }

        try {


            //查數據庫(redis)中庫存數量
            Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            //判斷庫存
            if (stock > 0) {
                System.out.println("消費庫存成功--->" + stock);
                //更新庫存
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
            } else {
                System.out.println("消費庫存失敗。。。");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            // del key  釋放鎖邏輯
            stringRedisTemplate.delete(lockKey);
        }


        return "helloworld";
    }

四代代碼

分析:

--優點:加鎖邏輯時設置過期時間,可以解決三代代碼的死鎖問題,系統中斷了我到時間就自動釋放鎖

--問題:我設置的時間是30秒(隨機想的一個數),假設我的業務邏輯是35,那會引起鎖失效。如下圖所示

@RequestMapping("/reduce_stock")
    public String reduceStock() {

        //key的名稱
        String lockKey = "lock";


        //setnx key value    加鎖邏輯
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1",30, TimeUnit.SECONDS);
        if (!aBoolean) {
            return "fail";
        }

        try {


            //查數據庫(redis)中庫存數量
            Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            //判斷庫存
            if (stock > 0) {
                System.out.println("消費庫存成功--->" + stock);
                //更新庫存
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
            } else {
                System.out.println("消費庫存失敗。。。");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            // del key  釋放鎖邏輯
            stringRedisTemplate.delete(lockKey);
        }


        return "helloworld";
    }

 五代代碼

分析:

--優點:解決了四代代碼的鎖失效問題

--缺點:如下圖所示,如果我設置失效時間是30,而我業務邏輯時間是35,在30-35之間是有兩個線程同時訪問,這與獨佔鎖是矛盾的,所以此處存在問題。

@RequestMapping("/reduce_stock")
    public String reduceStock() {

        //key的名稱
        String lockKey = "lock";

        //value的值
        String clientId = UUID.randomUUID().toString();


        //setnx key value    加鎖邏輯
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
        if (!aBoolean) {
            return "fail";
        }

        try {


            //查數據庫(redis)中庫存數量
            Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            //判斷庫存
            if (stock > 0) {
                System.out.println("消費庫存成功--->" + stock);
                //更新庫存
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
            } else {
                System.out.println("消費庫存失敗。。。");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            //只能釋放自己加的鎖
            if (clientId.equals(stringRedisTemplate.opsForValue().get("lock"))) {
                // del key  釋放鎖邏輯
                stringRedisTemplate.delete(lockKey);
            }
        }


        return "helloworld";
    }

瓶頸

我們現在的瓶頸就是超時時間的設置。

如果設置短了會出現五代代碼的問題;如果設置長了,你不能保證業務邏輯一定會比你設置的時間短,就算你設置的時間長,10分鐘,那萬一系統中斷10分鐘內不能有業務處理,也是不可取的。

如果我們能動態修改這個超時時間,那就無敵了 

其實還有一個問題,這短代碼的邏輯是獲取鎖失敗後直接返回,其實應該繼續嘗試獲取

基於redisson的分佈式鎖

原理

實踐

<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.10.0</version>
</dependency>
@Bean
    public Redisson redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.10.30.146:6379").setDatabase(0).setPassword("123456");
        return (Redisson) Redisson.create(config);
    }

 

@RestController
public class DistributedLockController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private Redisson redisson;


    @RequestMapping("/reduce_stock")
    public String reduceStock() {

        //key的名稱
        String lockKey = "lock";

        RLock lock = redisson.getLock(lockKey);

        lock.lock();
        try {
            //查數據庫(redis)中庫存數量
            Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
            //判斷庫存
            if (stock > 0) {
                System.out.println("消費庫存成功--->" + stock);
                //更新庫存
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", String.valueOf(stock));
            } else {
                System.out.println("消費庫存失敗。。。");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }


        return "helloworld";
    }


}

問題

向redis集羣寫數據的步驟是: 

1)向master節點寫數據

2) master節點返回

3)master節點同步到子節點

 

如果 線程t1 獲取鎖,寫入一個數據    1)  2)成功後 此時master 節點掉線了, 在子節點中選一個master,但是這個master是沒有t1寫的數據,此時此刻t2是可以獲取到鎖的,這個是redis做分佈式鎖的瑕疵。

redis是高性能分佈式鎖,zk是高可靠分佈式鎖,看你看重性能還是一致性了。

壓測工具

Apache JMeter  https://jmeter.apache.org/

安裝  https://www.cnblogs.com/telescope11/p/9848106.html

使用 https://www.cnblogs.com/monjeo/p/9330464.html

 

參考

https://ke.qq.com/course/455755?term_id=100545359&taid=4035731660403787

發佈了96 篇原創文章 · 獲贊 115 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章