鎖 - 分散式鎖工具

来源:https://www.cnblogs.com/cnx01/archive/2022/12/03/16948315.html
-Advertisement-
Play Games

鎖概述 在電腦科學中,鎖是在執行多線程時用於強行限制資源訪問的同步機制,即用於在併發控制中保證對互斥要求的滿足。 鎖相關概念 鎖開銷:完成一個鎖可能額外耗費的資源,比如一個周期所需要的時間,記憶體空間。 鎖競爭:一個線程或進程,要獲取另一個線程或進程所持有的鎖,邊會發生鎖競爭。鎖粒度越小,競爭的可能 ...


鎖概述

在電腦科學中,鎖是在執行多線程時用於強行限制資源訪問的同步機制,即用於在併發控制中保證對互斥要求的滿足。

鎖相關概念

  • 鎖開銷:完成一個鎖可能額外耗費的資源,比如一個周期所需要的時間,記憶體空間。
  • 鎖競爭:一個線程或進程,要獲取另一個線程或進程所持有的鎖,邊會發生鎖競爭。鎖粒度越小,競爭的可能越小。
  • 死鎖:多個線程爭奪資源互相等待資源釋放導致阻塞;由於無限期阻塞,程式不能正常終止。

分類

  • 樂觀鎖、悲觀鎖:是否鎖定同步資源。
    • 樂觀鎖:認為其他線程對數據訪問時 不會 修改數據,實際未加鎖,更新數據時判斷是否被其他線程更新了(讀時不加鎖,寫時加鎖)。
      • 適合多讀的場景,因為讀操作沒有加鎖。
      • 實現原理:CAS (compare-and-swap) ,無鎖演算法,原子操作比較更新。
      • 使用:
        • Java 中的 CAS 鎖(AtomicXxx)通過 JNI 調用 CPU 中的 cmpxchg 彙編指令實現
        • 資料庫表增加 version 欄位,更新時判斷 version 未改變。
      • 缺陷:
        • ABA 問題:數據發生類似變化(A -> B -> A),會認為數據沒有改變。
          JDK 1.5 引入 AtomicStampedReference 增加標誌位(1A -> 2B -> 3A)
        • 自旋問題:CAS 無法獲取到鎖會在超時時間內迴圈獲取,造成 CPU 資源浪費
    • 悲觀鎖:認為其他線程對數據訪問時 一定會 修改數據,訪問數據時加鎖同步處理(一開始加鎖無論讀寫)。
      • 適合多寫的場景,獨占數據的讀寫許可權,確保數據的讀取和更新都是準確的。
  • 讀寫鎖
    • 讀鎖:共用鎖,可支持多線程併發讀。
    • 寫鎖:獨享鎖,讀寫、寫寫互斥。
    • 示例:ReentrantReadWriteLock
  • 可重入鎖、不可重入鎖
    • 可重入鎖(遞歸鎖):一個線程在已加鎖範圍內代碼中再次進行加鎖能夠獲取到鎖
      • synchronized 、 ReentrantLock
    • 不可重入鎖:一個線程對在已加鎖範圍內代碼中再次進行加鎖操作,由於第二次加鎖時需要等待上次鎖釋放才可以加鎖造成鎖的互相等待
  • 公平鎖、非公平鎖
    • 公平鎖:多個線程按照申請鎖的順序來獲取鎖,依賴 AQS 隊列,線程直接進入隊列中排隊,第一個線程才能獲取到鎖
    • 非公平鎖:多個線程加鎖時嘗試直接獲取鎖,獲取不到進入隊列,可能出現後申請鎖的線程先獲取到鎖
      • 優點:可以減少喚起線程的開銷,整體吞吐效率高
      • 缺點:處於等待隊列中的線程可能餓死
      • synchronized
    • 示例:ReentrantLock 預設為非公平鎖,構造方法可指定為公平鎖 new ReentrantLock(true);
  • 偏向鎖、輕量鎖、重量鎖:synchronized 的三種鎖狀態。
    • 偏向鎖:鎖標誌位 101,在對象頭(Mark Word)和棧幀中鎖記錄(Lock Record)里存儲線程ID,通過 對比 Mark Word 避免執行 CAS
      • JDK 6 引入,JDK 15 標記廢棄,可通過 JVM 參數(-XX:+UseBiasedLocking)手動啟用
    • 輕量鎖:鎖標誌位 000,偏向鎖時出現競爭升級為輕量鎖,未獲取到鎖的線程自旋獲取,通過 CAS + 自旋 避免線程阻塞喚醒
    • 重量鎖:鎖標誌位 010,輕量鎖自旋超過一定此處升級為重量鎖,未獲取到鎖的線程休眠
  • 分段鎖、自旋鎖:鎖設計,非特定的鎖。
    • 分段鎖:將要鎖定的數據拆分成段後對所需數據段加鎖,減少鎖定範圍
      • ConcurrentHashMap 在 JDK 8 之前使用 Segment (繼承 ReentrantLock)對桶數組分割分段加鎖
    • 自旋鎖:試探獲取資源,未獲取到採取自旋迴圈 where(true) 再次試探獲取,不阻塞線程
      • 輕量鎖通過 CAS + 自旋 實現
      • 優點:減少上下文切換
      • 缺點:占用 CPU

相關閱讀:


自定義鎖工具

1 :Redis 分散式鎖(簡單實現)

使用 ThreadLocal 保存鎖對應的唯一標識
加鎖:使用 STRING 保存鎖定標識, 'SET key value PX NX' 確保一個 key 只能加鎖一次
解鎖:Lua 腳本判斷是自己加的鎖進行釋放

  • 工具類

    RedisSimpleLockUtil.java
    // 使用 ThreadLocal 保存鎖對應的唯一標識
    private static final ThreadLocal<String> LOCK_FLAG = ThreadLocal.withInitial(() ->
            UUID.randomUUID().toString().replace("-", "").toLowerCase()
    );
    
    // 嘗試加鎖
    private boolean tryLock(String key, long ttl) {
        try {
            String val = LOCK_FLAG.get();
            Boolean lockRes = redisTemplate.opsForValue()
                    .setIfAbsent(key, val, ttl, TimeUnit.MILLISECONDS);
            log.debug("tryLock, key={}, val={}, lockRes={}", key, val, lockRes);
            return Boolean.TRUE.equals(lockRes);
        } catch (Exception e) {
            log.error("tryLock occurred an exception", e);
        }
    
        return false;
    }
    
    // 解鎖
    public boolean unlock(String key) {
        boolean succeed = false;
        try {
            List<String> keys = Collections.singletonList(key);
            Object[] args = {LOCK_FLAG.get()};
            Long unlockRes = redisTemplate.execute(UNLOCK_SCRIPT, keys, args);
            log.debug("unlock, key={}, args={}, unlockRes={}", key, args, unlockRes);
            succeed = Optional.ofNullable(unlockRes).filter(res -> res > 0).isPresent();
        } catch (Exception e) {
            log.error("unlock occurred an exception", e);
        } finally {
            if (succeed) {
                LOCK_FLAG.remove();
            }
        }
    
        return succeed;
    }
    
  • Lua 腳本

    解鎖: redis_unlock_simple.lua
    local lock_key = KEYS[1];
    local lock_flag = ARGV[1];
    
    --- 判斷鎖定的唯一標識與參數一致刪除鎖
    --- 返回值:1=解鎖成功(刪除成功),0=鎖已失效或刪除失敗,-1=非自己的鎖不支持解鎖
    local val = redis.call('GET', lock_key);
    if (not val) then
        return 0;
    elseif (val == lock_flag) then
        return redis.call('DEL', lock_key);
    else
        return -1;
    end
    
  • 缺陷

    • 只能單次加鎖(唯一標識通過 ThreadLocal 存儲,解鎖時會清理 ThreadLocal,多次加解鎖會導致與預期不符)
    • 不可重入
  • 參考:https://github.com/realpdai/tech-pdai-spring-demos/blob/main/264-springboot-demo-redis-jedis-distribute-lock/src/main/java/tech/pdai/springboot/redis/jedis/lock/lock/RedisDistributedLock.java

2 :Redis 分散式鎖

使用 ThreadLocal 保存 鎖key 與 相應的唯一標識
加鎖:使用 HASH 保存鎖標識與加鎖次數
解鎖:Lua 腳本判斷是自己加的鎖進行釋放
功能:可重入(Redis HASH)、支持對不同 key 進行加解鎖(ThreadLocal<Map<String, String>>)

  • 工具類

    RedisLockUtil.java
    // 使用 ThreadLocal 保存 鎖key 與 唯一標識
    private static final ThreadLocal<Map<String, String>> LOCK_FLAG =
            ThreadLocal.withInitial(HashMap::new);
    // 嘗試加鎖
    private long tryLock(String key, long ttl) {
        String uniqueFlag = LOCK_FLAG.get().get(key);
        if (uniqueFlag == null) {
            uniqueFlag = UUID.randomUUID().toString().replace("-", "");
            LOCK_FLAG.get().put(key, uniqueFlag);
        }
    
        try {
            List<String> keys = Collections.singletonList(key);
            Object[] args = {uniqueFlag, ttl};
            Long lockRes = redisTemplate.execute(LOCK_SCRIPT, keys, args);
            log.debug("tryLock, lock_flag={}, key={}, args={}, lockRes={}",
                    LOCK_FLAG.get(), key, args, lockRes);
            return lockRes != null ? lockRes : 0L;
        } catch (Exception e) {
            log.error("tryLock occurred an exception", e);
        }
    
        return 0L;
    }
    
    // 嘗試解鎖
    public long tryUnlock(String key) {
        String uniqueFlag = LOCK_FLAG.get().get(key);
        if (uniqueFlag == null) {
            return 0L;
        }
    
        long lockNum = -1L;
        try {
            List<String> keys = Collections.singletonList(key);
            Object[] args = {uniqueFlag};
            Long unlockRes = redisTemplate.execute(UNLOCK_SCRIPT, keys, args);
            log.debug("unlock, key={}, args={}, unlockRes={}", key, args, unlockRes);
            lockNum = unlockRes != null ? unlockRes : 0L;
        } catch (Exception e) {
            log.error("release lock occurred an exception", e);
        } finally {
            if (lockNum == 0L) {
                LOCK_FLAG.get().remove(key);
                if (LOCK_FLAG.get().isEmpty()) {
                    LOCK_FLAG.remove();
                }
            }
        }
    
        return lockNum;
    }
    
  • Lua 腳本

    加鎖: redis_lock.lua
      ```lua
      local lock_key = KEYS[1];
      local lock_flag = ARGV[1];
      --- 鎖定時長,單位:毫秒
      local lock_ttl = tonumber(ARGV[2]);
    
      --- HASH 支持可重入
      --- lock_flag 保存加鎖唯一標識
      --- lock_num 保存加鎖次數
      local info = redis.call("HMGET", lock_key, "lock_flag", "lock_num");
      local h_flag = info[1];
      local h_num = tonumber(info[2]);
      if (h_num == nil or h_num < 0) then
          h_num = 0;
      end
    
      --- 返回加鎖次數,未加鎖成功返回 -1
      if (not h_flag or h_flag == lock_flag) then
          local res_num = h_num + 1;
          redis.call("HMSET", lock_key, "lock_flag", lock_flag, "lock_num", res_num);
          redis.call("PEXPIRE", lock_key, lock_ttl);
          return res_num;
      else
          return -1;
      end
      ```
    
    解鎖: redis_unlock.lua
      ```lua
      local lock_key = KEYS[1];
      local lock_flag = ARGV[1];
    
      --- HASH 支持可重入
      --- lock_flag 保存加鎖唯一標識
      --- lock_num 保存加鎖次數
      local info = redis.call("HMGET", lock_key, "lock_flag", "lock_num");
      local h_flag = info[1];
      local h_num = tonumber(info[2]);
      if (h_num == nil) then
          h_num = 0;
      end
    
      --- 返回剩餘加鎖次數,未被加鎖或解鎖完返回 0,非自己加鎖返回 -1
      if (not h_flag) then
          return 0;
      elseif (h_flag == lock_flag) then
          if (h_num <= 0) then
              redis.call("DEL", lock_key);
              return 0;
          else
              local res_num = h_num - 1;
              redis.call("HMSET", lock_key, "lock_flag", lock_flag, "lock_num", res_num);
              return res_num;
          end
      else
          return -1;
      end
      ```
    

其他

demo 地址:https://github.com/EastX/java-practice-demos/tree/main/demo-lock

作者:EastX

本文發自博客園,歡迎轉載,轉載請註明原文鏈接:https://www.cnblogs.com/cnx01/p/16948315.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 散耦合的架構是一種軟體應用程式開發模式,其中多個組件相互連接,但並不嚴重依賴對方。這些組件共同創建了一個總的網路或系統,儘管每個服務都是為執行單一任務而創建的獨立實體。鬆散耦合架構的主要目的是創建一個不會因為單個組件的失敗而失敗的系統。面向服務的架構(SOA)通常由鬆散耦合的架構組成。 鬆散耦合架構 ...
  • 讀本篇文章之前,如果讓你敘述一下 Exception Error Throwable 的區別,你能回答出來麽? 你的反應是不是像下麵一樣呢? 你在寫代碼時會經常 try catch(Exception) 在 log 中會看到 OutOfMemoryError Throwable 似乎不常見,但也大概... ...
  • 如果想直接查看修改部分請跳轉 動手-點擊跳轉 本文基於 ReactiveLoadBalancerClientFilter使用RoundRobinLoadBalancer 灰度發佈 灰度發佈,又稱為金絲雀發佈,是一種新舊版本平滑發佈的方式。在上面可以對同一個API進行兩個版本 的內容,由一部分用戶先行 ...
  • S11 介面:從協議到抽象基類 # random.shuffle 就地打亂 from random import shuffle l = list(range(10)) shuffle(l) print(l) shuffle(l) print(l) [0, 6, 3, 2, 4, 8, 5, 7, ...
  • 好家伙, xdm,密碼驗證忘寫了,哈哈 bug展示: 1.登陸沒有密碼驗證 主要體現為,亂輸也能登進去 (小問題) 要是這麼上線估計直接寄了 分析一波密碼校驗怎麼做: 前端輸完用戶名密碼之後,將數據發送到後端處理 後端要做以下幾件事 ①先確認這個用戶名已註冊 ②我們拿著這個用戶名去資料庫中找對應的數 ...
  • 說明: 本文基於Spring-Framework 5.1.x版本講解 概述 說起生命周期, 很多開源框架、中間件的組件都有這個詞,其實就是指組件從創建到銷毀的過程。 那這裡講Spring Bean的生命周期,並不是講Bean是如何創建的, 而是想講下Bean從實例化到銷毀,Spring框架在Bean ...
  • 1、Durid 1.1 簡介 Java程式很大一部分要操作資料庫,為了提高性能操作資料庫的時候,又不得不使用資料庫連接池。 Druid 是阿裡巴巴開源平臺上一個資料庫連接池實現,結合了 C3P0、DBCP 等 DB 池的優點,同時加入了日誌監控。 Druid 可以很好的監控 DB 池連接和 SQL ...
  • 1、參考文獻說明 參考博客:https://www.cnblogs.com/dy12138/articles/16799941.html Vmware Workstation pro 17 安裝會比較簡單,基本上點下一步就行了。 新功能介紹和破解碼請見:https://www.ghxi.com/vm ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...