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

前言

上一篇老貓和小夥伴們分享了爲何要使用分佈式鎖以及分佈式鎖的實現思路原理,目前咱們主要採用第三方的組件做爲分佈式鎖的工具。上一篇運用了Mysql中的select ...for update實現了分佈式鎖,可是咱們說這種實現方式並不經常使用,由於當大併發量的時候,會給數據庫帶來比較大的壓力。固然也有小夥伴給老貓留言說「 在quartz的集羣模式中,就是使用了基於mysql的分佈式鎖,select for update 」。沒錯,其實quartz的集羣模式中,任務執行的節點個數是可預知的,並且沒有那麼大的量級,因此是沒有問題的。可是若是像千萬級別的併發秒殺場景的狀況下,那麼這種方案實際上是不可行的。由於mysql操做是須要IO的,IO的速度比內存速度慢,所以mysql若是在那種場景下使用的話是會存在系統瓶頸的。因此本篇就和小夥伴們分享基於內存操做的比較經常使用的分佈式鎖——redis分佈式鎖。java

手擼Redis分佈式鎖

實現原理

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

  1. resource_name:資源名稱,能夠根據不一樣的業務區分不一樣的鎖。(其實就是對應咱們上一篇myql鎖中的business_code)。
  2. my_random_value:隨機值,每一個線程的隨機值都不相同,主要用於釋放鎖的時候用來校驗。
  3. NX:key不存在的時候設置成功,key存在則設置不成功。
  4. PX:自動失效時間,若是出現異常狀況,鎖能夠過時實現,所以達到了自動釋放。

那麼爲何可使用這個思路呢?其實很簡單,主要就是利用了set nx的原子性,在多個線程併發執行時,只有一個線程能夠設置成功,若是設置成功,那麼就表明着得到了鎖,就能夠執行後續的業務。若是出現了異常,過了鎖的有效期,鎖會自動釋放,釋放鎖主要採用了redis的delete命令,釋放鎖以前會校驗當前redis存儲的隨機數,只有當前的隨機數和存儲的隨機數一致的時候才容許釋放。具體的redis的刪除,咱們能夠經過lua腳本進行刪除,具體Lua腳本以下:git

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

那麼咱們爲何要採用這種方式釋放鎖呢?其實使用這種方式釋放鎖能夠避免刪除別的客戶端獲取成功的鎖 。程序員

以下圖:github

redis釋放鎖

客戶端A取得資源鎖,可是緊接着被一個其餘操做阻塞了,當客戶端A運行完畢其餘操做後要釋放鎖時,原來的鎖早已超時而且被Redis自動釋放,而且在這期間資源鎖又被客戶端B再次獲取到。若是僅使用DEL命令將key刪除,那麼這種狀況就會把客戶端B的鎖給刪除掉。使用Lua腳本就不會存在這種狀況,由於腳本僅會刪除value等於客戶端A的value的key(value至關於客戶端的一個簽名)(說明:其實這些例子在redis的官網都有介紹)。redis

代碼實現方式

老貓對redis鎖機制進行了相關的抽取,而且封裝成了工具類,核心工具類代碼以下:sql

/**
 * @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的實現。後續老貓會整理而且分享給你們,敬請期待。

固然更多技術乾貨也歡迎你們搜索關注公衆號「程序員老貓」

相關文章
相關標籤/搜索