首頁 >資料庫 >Redis >聊聊分散式鎖原理及Redis如何實現分散式鎖

聊聊分散式鎖原理及Redis如何實現分散式鎖

藏色散人
藏色散人轉載
2023-01-27 07:30:011201瀏覽

本篇文章為大家帶來了關於redis的相關知識,其中主要介紹了關於分散式鎖定是什麼? Redis又是怎麼實現分散式鎖定的?需要滿足什麼條件?下面一起來看看吧,希望對需要的朋友有幫助。

#一、分散式鎖定基本原理

分散式鎖定:滿足分散式系統或叢集模式下多進程可見且互斥的鎖定。

分散式鎖定應該滿足的條件:

#
  • 可見性:多個執行緒都能看到相同的結果,注意:這個地方說的可見性並不是並發程式設計中指的記憶體可見性,只是說多個進程之間都能感知到變化的意思
  • 互斥:互斥是分散式鎖的最基本的條件,使得程式串列執行
  • 高可用:程式不易崩潰,時時刻刻都保證較高的可用性
  • 高效能:由於加鎖本身就讓效能降低,所有對於分散式鎖定本身需要他就較高的加鎖效能和釋放鎖定效能
  • 安全性:安全性也是程式中不可或缺的一環

常見的分散式鎖定有三種:

  • Mysql:mysql本身就帶有鎖定機制,但由於mysql效能本身一般,所以採用分散式鎖的情況下,其實使用mysql作為分散式鎖比較少見

  • Redis:redis作為分散式鎖是非常常見的一種使用方式,現在企業級開發中基本上都使用redis或zookeeper作為分散式鎖,利用setnx這個方法,如果插入key成功,則表示獲得到了鎖,如果有人插入成功,其他人插入失敗則表示無法獲得到鎖,利用這套邏輯來實現分散式鎖定

  • Zookeeper:zookeeper也是企業級開發中較好的一個實現分散式鎖定的方案

聊聊分散式鎖原理及Redis如何實現分散式鎖

二、基於Redis實作分散式鎖定

實作分散式鎖定時需要實作的兩個基本方法:

  • #取得鎖定:

    • 互斥:確保只能有一個執行緒取得鎖定
    • 非阻塞:嘗試一次,成功回傳true,失敗回傳false
  • 釋放鎖定:

    • 手動釋放
    • #逾時釋放:取得鎖定時新增一個逾時時間

基於Redis實作分散式鎖定原理:

SET resource_name my_random_value NX PX 30000
  • resource_name:資源名稱,可根據不同的業務區分不同的鎖定
  • my_random_value:隨機值,每個執行緒的隨機值都不同,用於釋放鎖定時的校驗
  • NX:key不存在時設定成功,key存在則設定不成功
  • PX:自動失效時間,出現異常情況,鎖可以過期失效

利用NX的原子性,多個執行緒並發時,只有一個執行緒可以設定成功,設定成功表示獲得鎖,可以執行後續的業務處理;如果發生異常,過了鎖的有效期,鎖自動釋放;

版本一

##1、定義ILock介面

public interface ILock extends AutoCloseable {
    /**
     * 尝试获取锁
     *
     * @param timeoutSec 锁持有的超时时间,过期后自动释放
     * @return true代表获取锁成功;false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     * @return
     */
    void unLock();
}

2、基於Redis實作分散式鎖定—RedisLock

public class SimpleRedisLock {
    private final StringRedisTemplate stringRedisTemplate;
    private final String name;

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    private static final String KEY_PREFIX = "lock:";

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        String threadId = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        //通过del删除锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }

    @Override
    public void close() {
        unLock();
    }
}

鎖定誤刪問題

問題說明:

持有鎖的線程1在鎖的內部出現了阻塞,這時鎖超時自動釋放,這時線程2嘗試獲得鎖,然後線程2在持有鎖執行過程中,線程1反應過來,繼續執行,走到了刪除鎖定邏輯,此時就會把本來應該屬於執行緒2的鎖進行刪除,這就是鎖定誤刪的情況。

解決方案:

在存入鎖時,放入自己執行緒的標識,在刪除鎖定時,判斷目前這把鎖的標識是不是自己存入的,如果是,則進行刪除,如果不是,則不進行刪除。

版本二:解決鎖定誤刪問題

public class SimpleRedisLock {
    private final StringRedisTemplate stringRedisTemplate;
    private final String name;

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        // 获取线程标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁中的标示
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        // 判断标示是否一致
        if(threadId.equals(id)) {
            // 释放锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }

    @Override
    public void close() {
        unLock();
    }
}

鎖定釋放的原子性問題

問題分析:

上述釋放鎖的程式碼依然有鎖誤刪問題,當執行緒1取得鎖中的執行緒標識,並根據標識判斷是自己的鎖,這時鎖到期自動釋放,恰好執行緒2嘗試取得鎖,並拿到了鎖,此時執行緒1仍執行釋放鎖的操作,就導致誤刪了執行緒2持有的鎖。

原因在於,由java程式實現的釋放鎖定流程不是原子操作,存在線程安全問題。

解決方案:

Redis提供了Lua腳本功能,在一個腳本中寫多條Redis指令,可以確保多條指令執行時的原子性。

版本三:呼叫Lua腳本改造分散式鎖定

public class SimpleRedisLock implements ILock {
    private final StringRedisTemplate stringRedisTemplate;
    private final String name;

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        String script = "if redis.call("get",KEYS[1]) == ARGV[1] then\n" +
                " return redis.call("del",KEYS[1])\n" +
                "else\n" +
                " return 0\n" +
                "end";
        //通过执行lua脚本实现锁删除,可以校验随机值
        RedisScript<Boolean> redisScript = RedisScript.of(script, Boolean.class);
        stringRedisTemplate.execute(redisScript,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }

    @Override
    public void close() {
        unLock();
    }
}

推薦學習:《

Redis影片教學

#

以上是聊聊分散式鎖原理及Redis如何實現分散式鎖的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除