手撕redis分散式鎖,隔壁張小帥都看懂了!

程式設計師老貓發表於2021-01-09

前言

上一篇老貓和小夥伴們分享了為什麼要使用分散式鎖以及分散式鎖的實現思路原理,目前我們主要採用第三方的元件作為分散式鎖的工具。上一篇運用了Mysql中的select ...for update實現了分散式鎖,但是我們說這種實現方式並不常用,因為當大併發量的時候,會給資料庫帶來比較大的壓力。當然也有小夥伴給老貓留言說“ 在quartz的叢集模式中,就是使用了基於mysql的分散式鎖,select for update ”。沒錯,其實quartz的叢集模式中,任務執行的節點個數是可預知的,而且沒有那麼大的量級,所以是沒有問題的。但是如果像千萬級別的併發秒殺場景的情況下,那麼這種方案其實是不可行的。因為mysql操作是需要IO的,IO的速度比記憶體速度慢,因此mysql如果在那種場景下使用的話是會存在系統瓶頸的。所以本篇就和小夥伴們分享基於記憶體操作的比較常用的分散式鎖——redis分散式鎖。

手擼Redis分散式鎖

實現原理

redis分散式鎖實現原理其實也是比較簡單的,主要是依賴於redis的 set nx命令,我們來看一下完整的設定redis的命令:“Set resource_name my_random_value NX PX 30000”。看到這串命令,瞭解redis的小夥伴應該都看得懂這條命令是在redis中存入一個帶有過期時間的值。具體上述設值語句解釋如下:

  1. resource_name:資源名稱,可以根據不同的業務區分不同的鎖。(其實就是對應我們上一篇myql鎖中的business_code)。
  2. my_random_value:隨機值,每個執行緒的隨機值都不相同,主要用於釋放鎖的時候用來校驗。
  3. NX:key不存在的時候設定成功,key存在則設定不成功。
  4. PX:自動失效時間,如果出現異常情況,鎖可以過期實現,因此達到了自動釋放。

那麼為什麼可以使用這個思路呢?其實很簡單,主要就是利用了set nx的原子性,在多個執行緒併發執行時,只有一個執行緒可以設定成功,如果設定成功,那麼就代表著獲得了鎖,就可以執行後續的業務。如果出現了異常,過了鎖的有效期,鎖會自動釋放,釋放鎖主要採用了redis的delete命令,釋放鎖之前會校驗當前redis儲存的隨機數,只有當前的隨機數和儲存的隨機數一致的時候才允許釋放。具體的redis的刪除,我們可以通過lua指令碼進行刪除,具體Lua指令碼如下:

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

那麼我們為什麼要採用這種方式釋放鎖呢?其實使用這種方式釋放鎖可以避免刪除別的客戶端獲取成功的鎖 。

如下圖:

redis釋放鎖

客戶端A取得資源鎖,但是緊接著被一個其他操作阻塞了,當客戶端A執行完畢其他操作後要釋放鎖時,原來的鎖早已超時並且被Redis自動釋放,並且在這期間資源鎖又被客戶端B再次獲取到。如果僅使用DEL命令將key刪除,那麼這種情況就會把客戶端B的鎖給刪除掉。使用Lua指令碼就不會存在這種情況,因為指令碼僅會刪除value等於客戶端A的value的key(value相當於客戶端的一個簽名)(說明:其實這些例子在redis的官網都有介紹)。

程式碼實現方式

老貓對redis鎖機制進行了相關的抽取,並且封裝成了工具類,核心工具類程式碼如下:

/**
 * @author kdaddy@163.com
 * @date 2021/1/7 22:36
 * 公眾號“程式設計師老貓”
 */
@Service
public class RedisLockUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    private String value = UUID.randomUUID().toString();

    public Boolean lock(String key){
        RedisCallback<Boolean> redisCallback = redisConnection -> {
            //表示set nx 存在key的話就不設定,不存在則設定
            RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
            //設定過期時間
            Expiration expiration = Expiration.seconds(30);
            byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
            byte[] redisValue = redisTemplate.getKeySerializer().serialize(value);
            Boolean result = redisConnection.set(redisKey,redisValue,expiration,setOption);
            return result;
        };
        //獲取分散式鎖
        Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
        return lock;
    }
    //釋放分散式鎖
    public Boolean releaseLock(String key){
        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";
        RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
        List<String> keys = Arrays.asList(key);

        boolean result = (Boolean) redisTemplate.execute(redisScript,keys,value);
        return result;
    }
}

當然相關的業務程式碼,老貓還是使用了之前併發扣減庫存的例子,在此相關的程式碼以及最終執行的結果也不一一進行舉例。小夥伴們可以自行去老貓的github獲取相關的示例原始碼資訊,然後執行一下即可。github地址:https://github.com/maoba/kd-distribute。程式碼已經完成了更新。

Redisson分散式鎖

介紹和使用

那麼Redisson究竟為何物呢?Redisson 是架設在Redis基礎上的一個Java駐記憶體資料網格(In-Memory Data Grid)。 充分的利用了Redis鍵值資料庫提供的一系列優勢,基於Java實用工具包中常用介面,為使用者提供了一系列具有分散式特性的常用工具類。使得原本作為協調單機多執行緒併發程式的工具包獲得了協調分散式多機多執行緒併發系統的能力,大大降低了設計和研發大規模分散式系統的難度。同時結合各富特色的分散式服務,更進一步簡化了分散式環境中程式相互之間的協作。 (摘自redisson官網:https://redisson.org/)

下面我們來看一下具體用redisson實現分散式鎖實戰,其實是相當簡單的,redisson已經給我們進行了相關的封裝,我們開箱即用。

/**
 * @author kdaddy@163.com
 * @date 2021/1/9 14:23
 * @公眾號“程式設計師老貓”
 */
public  Integer createOrder() throws Exception{
    log.info("進入了方法");
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("ktdaddy");
    RedissonClient redissonClient = Redisson.create(config);
    RLock rlock = redissonClient.getLock(ORDER_KEY);
    rlock.lock(30, TimeUnit.SECONDS);

    try {
        log.info("拿到了鎖");
        //....具體可以參考老貓的github
        return order.getId();
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        rlock.unlock();
    }
    return null;
}

原理

redisson簡單架構

老貓上文中自己實現redis鎖的時候用到了lua指令碼,redisson實現的時候其實所有的指令都是通過lua指令碼去實現的。上述為redisson的簡單架構圖,畫的比較粗糙。老貓稍微作一下解釋。上圖中有個看門狗(watchdog)概念。其實這就是一個定時任務,線上程獲取鎖之後,它會每隔10s幫忙將key的超時時間設定為30s,這樣就不會出現執行緒一直持有鎖從而影響其他執行緒獲取鎖的問題。小夥伴們可以發現該功能其實就是set px,只是換成了定時任務去實現。當然看門狗的存在保證了出現死鎖的情況下會自動釋放。

以上只是針對redisson做了一個簡單的應用介紹,redisson其實是相當強大的,首先說配置,老貓上述連線redis的方式其實很簡單,由於搭建的是單機redis,所以就使用了單機redis的連線方式,當然redisson還支援主從、哨兵、叢集等等連線方式;當然鎖的種類也相當豐富,以上老貓提供的是可重入鎖的流程。其實還包括公平鎖、聯鎖、紅鎖、讀寫鎖等等,另外的redisson對分散式的容器、佇列等等進行了特有的封裝,包括分散式的Blocking Queue、分散式Map、分散式Set、分散式List等等。redisson的強大之處老貓在此不一一列舉,有興趣的小夥伴可以深入研究一下。

缺陷

redis鎖可以比較完美地解決高併發的時候分散式系統的執行緒安全性的問題,但是這種鎖機制也並不是完美的。在哨兵模式下,客戶端對master節點加了鎖,此時會非同步複製給slave節點,此時如果master發生當機,主備切換,slave變成了master。因為之前是非同步複製,所以此時正好又有個執行緒來嘗試加鎖的時候,就會導致多個客戶端對同一個分散式鎖完成了加鎖操作,這時候業務上會出現髒資料了。關於redis的相關知識,大家可以訪問老貓之前的一些文章,包括redis的哨兵模式、持久化等等。

寫在最後

本篇主要和小夥伴們分享了redis鎖,從老貓自己實現的乞丐版的redis鎖到大牛實現的redisson。相信大家也會有一定的收貨。其實關於分散式鎖,出了redis鎖之外還有基於zookeeper的實現。後續老貓會整理並且分享給大家,敬請期待。

當然更多技術乾貨也歡迎大家搜尋關注公眾號“程式設計師老貓”

相關文章