1. 程式人生 > 其它 >第七章 【高階篇】分散式鎖之Redis6+Lua指令碼實現原生分散式鎖

第七章 【高階篇】分散式鎖之Redis6+Lua指令碼實現原生分散式鎖

第1集 分散式核心技術-關於高併發下分散式鎖你知道多少?

簡介:分散式鎖核心知識介紹和注意事項

  • 背景

    • 就是保證同一時間只有一個客戶端可以對共享資源進行操作

    • 案例:優惠券領劵限制張數、商品庫存超賣

    • 核心

      • 為了防止分散式系統中的多個程序之間相互干擾,我們需要一種分散式協調技術來對這些程序進行排程
      • 利用互斥機制來控制共享資源的訪問,這就是分散式鎖要解決的問題
  • 避免共享資源併發操作導致資料問題

    • 加鎖

      • 本地鎖:synchronize、lock等,鎖在當前程序內,叢集部署下依舊存在問題
      • 分散式鎖:redis、zookeeper等實現,雖然還是鎖,但是多個程序共用的鎖標記,可以用Redis、Zookeeper、Mysql等都可以
  • 設計分散式鎖應該考慮的東西

    • 排他性

      • 在分散式應用叢集中,同一個方法在同一時間只能被一臺機器上的一個執行緒執行
    • 容錯性

      • 分散式鎖一定能得到釋放,比如客戶端奔潰或者網路中斷
    • 滿足可重入、高效能、高可用

    • 注意分散式鎖的開銷、鎖粒度

第2集 基於Redis實現分散式鎖的幾種坑你是否踩過《上》

簡介:基於Redis實現分散式鎖的幾種坑

  • 實現分散式鎖 可以用 Redis、Zookeeper、Mysql資料庫這幾種 , 效能最好的是Redis且是最容易理解

    • 分散式鎖離不開 key - value 設定
key 是鎖的唯一標識,一般按業務來決定命名,比如想要給一種優惠券活動加鎖,key 命名為 “coupon:id” 。value就可以使用固定值,比如設定成1
  • 基於redis實現分散式鎖,文件:http://www.redis.cn/commands.html#string

    • 加鎖 SETNX key value
    setnx 的含義就是 SET if Not Exists,有兩個引數 setnx(key, value),該方法是原子性操作
    如果 key 不存在,則設定當前 key 成功,返回 1;
    如果當前 key 已經存在,則設定當前 key 失敗,返回 0
    • 解鎖 del (key)
    得到鎖的執行緒執行完任務,需要釋放鎖,以便其他執行緒可以進入,呼叫 del(key)
    • 配置鎖超時 expire (key,30s)
    客戶端奔潰或者網路中斷,資源將會永遠被鎖住,即死鎖,因此需要給key配置過期時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間後自動釋放
    • 綜合虛擬碼
    methodA(){
      String key = "coupon_66"
      if(setnx(key,1) == 1){
        expire(key,30,TimeUnit.MILLISECONDS)
        try {
          //做對應的業務邏輯
          //查詢使用者是否已經領券
          //如果沒有則扣減庫存
          //新增領劵記錄
        } finally {
          del(key)
        }
      }else{
       //睡眠100毫秒,然後自旋呼叫本方法
        methodA()
      }
    }
    • 存在哪些問題,大家自行思考下

第3集 基於Redis實現分散式鎖的幾種坑你是否踩過《下》

簡介:手把手教你徹底掌握分散式鎖+原生程式碼編寫

  • 存在什麼問題?

    • 多個命令之間不是原子性操作,如setnxexpire之間,如果setnx成功,但是expire失敗,且宕機了,則這個資源就是死鎖
    使用原子命令:設定和配置過期時間  setnx / setex
    如: set key 1 ex 30 nx
    java裡面 redisTemplate.opsForValue().setIfAbsent("seckill_1","success",30,TimeUnit.MILLISECONDS)
    • 業務超時,存在其他執行緒勿刪,key 30秒過期,假如執行緒A執行很慢超過30秒,則key就被釋放了,其他執行緒B就得到了鎖,這個時候執行緒A執行完成,而B還沒執行完成,結果就是執行緒A刪除了執行緒B加的鎖
    可以在 del 釋放鎖之前做一個判斷,驗證當前的鎖是不是自己加的鎖, 那 value 應該是存當前執行緒的標識或者uuid
    String key = "coupon_66"
    String value = Thread.currentThread().getId()
    if(setnx(key,value) == 1){
       expire(key,30,TimeUnit.MILLISECONDS)
       try {
         //做對應的業務邏輯
       } finally {
         //刪除鎖,判斷是否是當前執行緒加的
         if(get(key).equals(value)){
              //還存在時間間隔
              del(key)
         }
       }
    }else{
    
    
      //睡眠100毫秒,然後自旋呼叫本方法
    }
    • 進一步細化誤刪

      • 當執行緒A獲取到正常值時,返回帶程式碼中判斷期間鎖過期了,執行緒B剛好重新設定了新值,執行緒A那邊有判斷value是自己的標識,然後呼叫del方法,結果就是刪除了新設定的執行緒B的值
      • 核心還是判斷和刪除命令 不是原子性操作導致
    • 總結

      • 加鎖+配置過期時間:保證原子性操作
      • 解鎖: 防止誤刪除、也要保證原子性操作
    • 那如何解決呢?下集講解

第4集 手把手教你徹底掌握分散式鎖lua指令碼+redis原生程式碼編寫

簡介:手把手教你徹底掌握分散式鎖+原生程式碼編寫

  • 前面說了redis做分散式鎖存在的問題

    • 核心是保證多個指令原子性,加鎖使用setnx setex 可以保證原子性,那解鎖使用 判斷和刪除怎麼保證原子性
    • 文件:http://www.redis.cn/commands/set.html
    • 多個命令的原子性:採用 lua指令碼+redis, 由於【判斷和刪除】是lua指令碼執行,所以要麼全成功,要麼全失敗
    //獲取lock的值和傳遞的值一樣,呼叫刪除操作返回1,否則返回0
    String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
    //Arrays.asList(lockKey)是key列表,uuid是引數
    Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid);
    • 全部程式碼
    /**
    * 原生分散式鎖 開始
    * 1、原子加鎖 設定過期時間,防止宕機死鎖
    * 2、原子解鎖:需要判斷是不是自己的鎖
    */
    @RestController
    @RequestMapping("/api/v1/coupon")
    public class CouponController {
       @Autowired
       private StringRedisTemplate redisTemplate;
       @GetMapping("add")
       public JsonData saveCoupon(@RequestParam(value = "coupon_id",required = true) int couponId){
         //防止其他執行緒誤刪
         String uuid = UUID.randomUUID().toString();
         String lockKey = "lock:coupon:"+couponId;
         lock(couponId,uuid,lockKey);
         return JsonData.buildSuccess();
       }
       private void lock(int couponId,String uuid,String lockKey){
         //lua指令碼
         String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
         Boolean nativeLock = redisTemplate.opsForValue().setIfAbsent(lockKey,uuid,Duration.ofSeconds(30));
         System.out.println(uuid+"加鎖狀態:"+nativeLock);
         if(nativeLock){
           //加鎖成功
           try{
             //TODO 做相關業務邏輯
             TimeUnit.SECONDS.sleep(10L);
           } catch (InterruptedException e) {
           } finally {
             //解鎖
             Long result = redisTemplate.execute( new DefaultRedisScript<>(script,Long.class),Arrays.asList(lockKey),uuid);
             System.out.println("解鎖狀態:"+result);
           }
         }else {
           //自旋操作
           try {
             System.out.println("加鎖失敗,睡眠5秒 進行自旋");
             TimeUnit.MILLISECONDS.sleep(5000);
           } catch (InterruptedException e) { }
           //睡眠一會再嘗試獲取鎖
           lock(couponId,uuid,lockKey);
         }
       }
    }
    • 遺留一個問題,鎖的過期時間,如何實現鎖的自動續期 或者 避免業務執行時間過長,鎖過期了?

      • 原生方式的話,一般把鎖的過期時間設定久一點,比如10分鐘時間
    • 原生程式碼+redis實現分散式鎖使用比較複雜,且有些鎖續期問題更難處理