redis實(shí)現(xiàn)分布式鎖代碼實(shí)踐和場景問題解決方案

Redis為什么性能高?
1、Redis基于內(nèi)存的
2、Redis基于單線程,較少線程上下文切換
3、Redis的基于NIO的多路復(fù)用機(jī)制
4、Redis底層多種數(shù)據(jù)結(jié)構(gòu),得益于數(shù)據(jù)存儲結(jié)構(gòu)

使用redis原子性命令解決分布式鎖問題刨析

1、保證加鎖LockKey唯一性
2、保證加鎖KEY和expire設(shè)置過期時間是一條原子性命令
3、finally {}語句塊中釋放鎖,保證釋放是當(dāng)前線程的Redis分布式鎖。在加鎖之前生成一個clientId
在最后再判斷當(dāng)前clientId是否一致,然后再釋放Redis分布式鎖
4、極限情況下,請查看如下代碼

finally {
            // 在解鎖前添加一個uuid判斷當(dāng)前刪除是否是當(dāng)前請求的鎖
            // (問題:判斷和刪除鎖不是原子性的,假設(shè)極限情況下key設(shè)置的過期時間是10秒)
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                // 剛好執(zhí)行完這個判斷時候9.9秒,突然線程阻塞或者垃圾回收,鎖過期了,這個時候其他線程并發(fā)仍然能加鎖成功
                // 此時線程接著執(zhí)行刪除邏輯,釋放的是下一個線程的鎖,所有的問題都和鎖過期時間有關(guān),所以需要鎖續(xù)命

                // 5、釋放鎖(問題:如果異常系統(tǒng)宕機(jī)無法釋放鎖,所以在加鎖前要設(shè)置過期時間)
                stringRedisTemplate.delete(lockKey);
            }

如果在刪除鎖之前,Redis的Key剛好過期,也可能導(dǎo)致釋放其他線程鎖。
這里就需要給鎖設(shè)置續(xù)命規(guī)則
引入Watch Dog看門狗程序:
通過定時任務(wù),每隔10秒檢查鎖是否失效,對鎖進(jìn)行續(xù)命
解決方案:Redisson框架的底層實(shí)現(xiàn)機(jī)制(下面代碼有示例)

總結(jié):Redis分布式鎖所有場景問題都和Key的過期時間有關(guān)聯(lián)
   /**
     * 使用redis原子性命令解決分布式鎖
     */
    @PostMapping("/buyProduct1")
    public String buyProduct1() {

        // 1、key代表商品唯一標(biāo)識,保證永遠(yuǎn)只會有一個請求成功獲取鎖
        final String lockKey = "lockKey_product1";
        final String clientId = UUID.randomUUID().toString();

        // 2、redis的setNx原子命令解決分布式鎖
        // redis的setNx原子命令解決分布式鎖,保證加鎖和過期時間設(shè)置原子性
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId);
        // 給當(dāng)前鎖設(shè)置過期時間,避免死鎖(問題:加鎖和設(shè)置過期時間不是原子操作)
        stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);

        if (Boolean.FALSE.equals(flag)) {
            return null;
        }

        // 3、返回 true代表獲取分布式鎖成功 (問題:假設(shè)過期時間設(shè)置過段,導(dǎo)致業(yè)務(wù)邏輯未執(zhí)行完畢,stringRedisTemplate.delete刪除別的請求線程鎖)
        // 15s-->10s --> 5s / 8s-->5s --> 3s/ 6s-->5s --> 1s 假設(shè)多線程高并發(fā)請求,后續(xù)線程鎖會一直被釋放,導(dǎo)致鎖失效
        // 問題:自己加的鎖,被別的請求刪掉了(解決方案,在解鎖前添加一個uuid判斷當(dāng)前刪除是否是當(dāng)前請求的鎖)
        try {
            // 從Redis獲取庫存
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock <= 0) {
                System.out.println("扣減庫存失敗,庫存不足");
                return null;
            }

            // 4、扣減完畢直接更新庫存
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
            System.out.println("扣減庫存成功,剩余庫存realStock = " + realStock);

        } catch (NumberFormatException e) {
            e.printStackTrace();
        } finally {
            // 在解鎖前添加一個uuid判斷當(dāng)前刪除是否是當(dāng)前請求的鎖
            // (問題:判斷和刪除鎖不是原子性的,假設(shè)極限情況下key設(shè)置的過期時間是10秒)
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                // 剛好執(zhí)行完這個判斷時候9.9秒,突然線程阻塞或者垃圾回收,鎖過期了,這個時候其他線程并發(fā)仍然能加鎖成功
                // 此時線程接著執(zhí)行刪除邏輯,釋放的是下一個線程的鎖,所有的問題都和鎖過期時間有關(guān),所以需要鎖續(xù)命

                // 5、釋放鎖(問題:如果異常系統(tǒng)宕機(jī)無法釋放鎖,所以在加鎖前要設(shè)置過期時間)
                stringRedisTemplate.delete(lockKey);
            }
        }
        return null;
    }

Redisson實(shí)現(xiàn)分布式鎖解決鎖續(xù)命問題

 /**
     * 使用redis原子性命令解決分布式鎖
     */
    @PostMapping("/buyProduct1")
    public String buyProduct1() {

        // 1、key代表商品唯一標(biāo)識,保證永遠(yuǎn)只會有一個請求成功獲取鎖
        final String lockKey = "lockKey_product1";

        // 2、獲取鎖對象
        RLock redissonLock = redisson.getLock(lockKey);
        redissonLock.lock();
        try {
            // 從Redis獲取庫存
            int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
            if (stock <= 0) {
                System.out.println("扣減庫存失敗,庫存不足");
                return null;
            }
            // 扣減完畢直接更新庫存
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
            System.out.println("扣減庫存成功,剩余庫存realStock = " + realStock);

        } catch (NumberFormatException e) {
            e.printStackTrace();
        } finally {
            // 3、釋放鎖
            redissonLock.unlock();
        }
        return null;
    }

Redisson實(shí)現(xiàn)分布式鎖原理

Redis分布式鎖

1、加鎖機(jī)制
線程去獲取鎖,獲取成功: 執(zhí)行 lua腳本,保存數(shù)據(jù)到 redis數(shù)據(jù)庫。
2、watch dog自動延期機(jī)制
在一個分布式環(huán)境下,假如一個線程獲得鎖后,突然服務(wù)器宕機(jī)了,那么這個時候在一定時間后這個鎖會自動釋放,你也可以設(shè)置鎖的有效時間(不設(shè)置默認(rèn)30秒),這樣的目的主要是防止死鎖的發(fā)生。

但在實(shí)際開發(fā)中會有下面一種情況:

 1 //設(shè)置鎖1秒過去
 2 redissonLock.lock("redisson", 1);
 3 /**
 4  * 業(yè)務(wù)邏輯需要咨詢2秒
 5  */
 6 redissonLock.release("redisson");
 7 
 8 /**
 9 * 線程1 進(jìn)來獲得鎖后,線程一切正常并沒有宕機(jī),但它的業(yè)務(wù)邏輯需要執(zhí)行2秒,這就會有個問題,在 線程1 執(zhí)行1秒后,這個鎖就自動過期了,
10 * 那么這個時候 線程2 進(jìn)來了。那么就存在 線程1和線程2 同時在這段業(yè)務(wù)邏輯里執(zhí)行代碼,這當(dāng)然是不合理的。
11 * 而且如果是這種情況,那么在解鎖時系統(tǒng)會拋異常,因?yàn)榻怄i和加鎖已經(jīng)不是同一線程了
12 */

所以這個時候看門狗就出現(xiàn)了,它的作用就是 線程1 業(yè)務(wù)還沒有執(zhí)行完,時間就過了,線程1 還想持有鎖的話,就會啟動一個 watch dog后臺線程,不斷的延長鎖 key的生存時間。
注意:正常這個看門狗線程是不啟動的,還有就是這個看門狗啟動后對整體性能也會有一定影響,所以不建議開啟看門狗。

Redis分布式鎖提升性能的方案

1、通過控制代碼層加鎖的粒度,提升性能
2、通過分段鎖提升性能。

Redis分段鎖
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容