手撕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地址://github.com/maoba/kd-distribute。代碼已經完成了更新。

Redisson分佈式鎖

介紹和使用

那麼Redisson究竟為何物呢?Redisson 是架設在Redis基礎上的一個Java駐內存數據網格(In-Memory Data Grid)。 充分的利用了Redis鍵值數據庫提供的一系列優勢,基於Java實用工具包中常用接口,為使用者提供了一系列具有分佈式特性的常用工具類。使得原本作為協調單機多線程並發程序的工具包獲得了協調分佈式多機多線程並發系統的能力,大大降低了設計和研發大規模分佈式系統的難度。同時結合各富特色的分佈式服務,更進一步簡化了分佈式環境中程序相互之間的協作。 (摘自redisson官網://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;
}

原理

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的實現。後續老貓會整理並且分享給大家,敬請期待。

當然更多技術乾貨也歡迎大家搜索關注公眾號「程序員老貓」