天天看點

【分布式鎖的演化】手撕redis分布式鎖,隔壁張小帥都看懂了!

還不會用redis實作分布式鎖?滴滴~快上車~

前言

上一篇老貓和小夥伴們分享了為什麼要使用分布式鎖以及分布式鎖的實作思路原理,目前我們主要采用第三方的元件作為分布式鎖的工具。上一篇運用了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 [email protected]
 * @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 [email protected]
 * @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;
}

           

原理

【分布式鎖的演化】手撕redis分布式鎖,隔壁張小帥都看懂了!

老貓上文中自己實作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的實作。後續老貓會整理并且分享給大家,敬請期待。

當然更多技術幹貨也歡迎大家搜尋關注公衆号“程式員老貓”

熱愛技術,熱愛産品,熱愛生活,一個懂技術,懂産品,懂生活的程式員~

更多精彩内容,可以關注公衆号“程式員老貓”。

一起讨論技術,探讨一下點子,研究研究賺錢!

繼續閱讀