jedisLock—redis分散式鎖實現

来源:http://www.cnblogs.com/0201zcr/archive/2016/10/10/5942748.html
-Advertisement-
Play Games

一、使用分散式鎖要滿足的幾個條件: 二、應用的場景例子 管理後臺的部署架構(多台tomcat伺服器+redis【多台tomcat伺服器訪問一臺redis】+mysql【多台tomcat伺服器訪問一臺伺服器上的mysql】)就滿足使用分散式鎖的條件。多台伺服器要訪問redis全局緩存的資源,如果不使用 ...


一、使用分散式鎖要滿足的幾個條件:

  1. 系統是一個分散式系統(關鍵是分散式,單機的可以使用ReentrantLock或者synchronized代碼塊來實現)
  2. 共用資源(各個系統訪問同一個資源,資源的載體可能是傳統關係型資料庫或者NoSQL)
  3. 同步訪問(即有很多個進程同事訪問同一個共用資源。沒有同步訪問,誰管你資源競爭不競爭)

二、應用的場景例子

  管理後臺的部署架構(多台tomcat伺服器+redis【多台tomcat伺服器訪問一臺redis】+mysql【多台tomcat伺服器訪問一臺伺服器上的mysql】)就滿足使用分散式鎖的條件。多台伺服器要訪問redis全局緩存的資源,如果不使用分散式鎖就會出現問題。 看如下偽代碼:

long N=0L;
//N從redis獲取值
if(N<5){
N++//N寫回redis
}

上面的代碼主要實現的功能:

  從redis獲取值N,對數值N進行邊界檢查,自加1,然後N寫回redis中。 這種應用場景很常見,像秒殺,全局遞增ID、IP訪問限制等。以IP訪問限制來說,惡意攻擊者可能發起無限次訪問,併發量比較大,分散式環境下對N的邊界檢查就不可靠,因為從redis讀的N可能已經是臟數據。傳統的加鎖的做法(如java的synchronized和Lock)也沒用,因為這是分散式環境,這個同步問題的救火隊員也束手無策。在這危急存亡之秋,分散式鎖終於有用武之地了。

  分散式鎖可以基於很多種方式實現,比如zookeeper、redis...。不管哪種方式,他的基本原理是不變的:用一個狀態值表示鎖,對鎖的占用和釋放通過狀態值來標識。

   這裡主要講如何用redis實現分散式鎖。

三、使用redis的setNX命令實現分散式鎖  

1、實現的原理

  Redis為單進程單線程模式,採用隊列模式將併發訪問變成串列訪問,且多客戶端對Redis的連接並不存在競爭關係。redis的SETNX命令可以方便的實現分散式鎖。

2、基本命令解析

1)setNX(SET if Not eXists)

語法:

SETNX key value

將 key 的值設為 value ,當且僅當 key 不存在。

若給定的 key 已經存在,則 SETNX 不做任何動作。

SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫

返回值:

  設置成功,返回 1 。   設置失敗,返回 0 。

 例子:

redis> EXISTS job                # job 不存在
(integer) 0

redis> SETNX job "programmer"    # job 設置成功
(integer) 1

redis> SETNX job "code-farmer"   # 嘗試覆蓋 job ,失敗
(integer) 0

redis> GET job                   # 沒有被覆蓋
"programmer"

 所以我們使用執行下麵的命令

SETNX lock.foo <current Unix time + lock timeout + 1> 
  • 如返回1,則該客戶端獲得鎖,把lock.foo的鍵值設置為時間值表示該鍵已被鎖定,該客戶端最後可以通過DEL lock.foo來釋放該鎖。

  • 如返回0,表明該鎖已被其他客戶端取得,這時我們可以先返回或進行重試等對方完成或等待鎖超時。

2)getSET

語法:

GETSET key value

  將給定 key 的值設為 value ,並返回 key 的舊值(old value)

  當 key 存在但不是字元串類型時,返回一個錯誤。

返回值:

  返回給定 key 的舊值。   當 key 沒有舊值時,也即是, key 不存在時,返回 nil 。 3)get 語法:
GET key

 返回值:

  當 key 不存在時,返回 nil ,否則,返回 key 的值。   如果 key 不是字元串類型,那麼返回一個錯誤

四、解決死鎖

  上面的鎖定邏輯有一個問題:如果一個持有鎖的客戶端失敗或崩潰了不能釋放鎖,該怎麼解決

我們可以通過鎖的鍵對應的時間戳來判斷這種情況是否發生了,如果當前的時間已經大於lock.foo的值,說明該鎖已失效,可以被重新使用。 

  發生這種情況時,可不能簡單的通過DEL來刪除鎖,然後再SETNX一次(講道理,刪除鎖的操作應該是鎖擁有這執行的,這裡只需要等它超時即可),當多個客戶端檢測到鎖超時後都會嘗試去釋放它,這裡就可能出現一個競態條件,讓我們模擬一下這個場景: 

C0操作超時了,但它還持有著鎖,C1和C2讀取lock.foo檢查時間戳,先後發現超時了。 
C1 發送DEL lock.foo 
C1 發送SETNX lock.foo 並且成功了。 
C2 發送DEL lock.foo 
C2 發送SETNX lock.foo 並且成功了。 
這樣一來,C1,C2都拿到了鎖!問題大了! 

  幸好這種問題是可以避免的,讓我們來看看C3這個客戶端是怎樣做的: 

C3發送SETNX lock.foo 想要獲得鎖,由於C0還持有鎖,所以Redis返回給C3一個0 
C3發送GET lock.foo 以檢查鎖是否超時了,如果沒超時,則等待或重試。 
反之,如果已超時,C3通過下麵的操作來嘗試獲得鎖: 
GETSET lock.foo <current Unix time + lock timeout + 1> 
通過GETSET,C3拿到的時間戳如果仍然是超時的,那就說明,C3如願以償拿到鎖了。 
如果在C3之前,有個叫C4的客戶端比C3快一步執行了上面的操作,那麼C3拿到的時間戳是個未超時的值,這時,C3沒有如期獲得鎖,需要再次等待或重試。留意一下,儘管C3沒拿到鎖,但它改寫了C4設置的鎖的超時值,不過這一點非常微小的誤差帶來的影響可以忽略不計。 

  註意:為了讓分散式鎖的演算法更穩鍵些,持有鎖的客戶端在解鎖之前應該再檢查一次自己的鎖是否已經超時,再去做DEL操作,因為可能客戶端因為某個耗時的操作而掛起,操作完的時候鎖因為超時已經被別人獲得,這時就不必解鎖了。  

五、代碼實現

  expireMsecs 鎖持有超時,防止線程在入鎖以後,無限的執行下去,讓鎖無法釋放 
  timeoutMsecs 鎖等待超時,防止線程饑餓,永遠沒有入鎖執行代碼的機會 

註意:項目裡面需要先搭建好redis的相關配置

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis distributed lock implementation.
 *
 * @author zhengcanrui
 */
public class RedisLock {

    private static Logger logger = LoggerFactory.getLogger(RedisLock.class);

    private RedisTemplate redisTemplate;

    private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

    /**
     * Lock key path.
     */
    private String lockKey;

    /**
     * 鎖超時時間,防止線程在入鎖以後,無限的執行等待
     */
    private int expireMsecs = 60 * 1000;

    /**
     * 鎖等待時間,防止線程饑餓
     */
    private int timeoutMsecs = 10 * 1000;

    private volatile boolean locked = false;

    /**
     * Detailed constructor with default acquire timeout 10000 msecs and lock expiration of 60000 msecs.
     *
     * @param lockKey lock key (ex. account:1, ...)
     */
    public RedisLock(RedisTemplate redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey + "_lock";
    }

    /**
     * Detailed constructor with default lock expiration of 60000 msecs.
     *
     */
    public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs) {
        this(redisTemplate, lockKey);
        this.timeoutMsecs = timeoutMsecs;
    }

    /**
     * Detailed constructor.
     *
     */
    public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {
        this(redisTemplate, lockKey, timeoutMsecs);
        this.expireMsecs = expireMsecs;
    }

    /**
     * @return lock key
     */
    public String getLockKey() {
        return lockKey;
    }

    private String get(final String key) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] data = connection.get(serializer.serialize(key));
                    connection.close();
                    if (data == null) {
                        return null;
                    }
                    return serializer.deserialize(data);
                }
            });
        } catch (Exception e) {
            logger.error("get redis error, key : {}", key);
        }
        return obj != null ? obj.toString() : null;
    }

    private boolean setNX(final String key, final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return success;
                }
            });
        } catch (Exception e) {
            logger.error("setNX redis error, key : {}", key);
        }
        return obj != null ? (Boolean) obj : false;
    }

    private String getSet(final String key, final String value) {
        Object obj = null;
        try {
            obj = redisTemplate.execute(new RedisCallback<Object>() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {
                    StringRedisSerializer serializer = new StringRedisSerializer();
                    byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));
                    connection.close();
                    return serializer.deserialize(ret);
                }
            });
        } catch (Exception e) {
            logger.error("setNX redis error, key : {}", key);
        }
        return obj != null ? (String) obj : null;
    }

    /**
     * 獲得 lock.
     * 實現思路: 主要是使用了redis 的setnx命令,緩存了鎖.
     * reids緩存的key是鎖的key,所有的共用, value是鎖的到期時間(註意:這裡把過期時間放在value了,沒有時間上設置其超時時間)
     * 執行過程:
     * 1.通過setnx嘗試設置某個key的值,成功(當前沒有這個鎖)則返回,成功獲得鎖
     * 2.鎖已經存在則獲取鎖的到期時間,和當前時間比較,超時的話,則設置新的值
     *
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException in case of thread interruption
     */
    public synchronized boolean lock() throws InterruptedException {
        int timeout = timeoutMsecs;
        while (timeout >= 0) {
            long expires = System.currentTimeMillis() + expireMsecs + 1;
            String expiresStr = String.valueOf(expires); //鎖到期時間
            if (this.setNX(lockKey, expiresStr)) {
                // lock acquired
                locked = true;
                return true;
            }

            String currentValueStr = this.get(lockKey); //redis里的時間
            if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                //判斷是否為空,不為空的情況下,如果被其他線程設置了值,則第二個條件判斷是過不去的
                // lock is expired

                String oldValueStr = this.getSet(lockKey, expiresStr);
                //獲取上一個鎖到期時間,並設置現在的鎖到期時間,
                //只有一個線程才能獲取上一個線上的設置時間,因為jedis.getSet是同步的
                if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                    //防止誤刪(覆蓋,因為key是相同的)了他人的鎖——這裡達不到效果,這裡值會被覆蓋,但是因為什麼相差了很少的時間,所以可以接受

                    //[分散式的情況下]:如過這個時候,多個線程恰好都到了這裡,但是只有一個線程的設置值和當前值相同,他才有權利獲取鎖
                    // lock acquired
                    locked = true;
                    return true;
                }
            }
            timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;

            /*
                延遲100 毫秒,  這裡使用隨機時間可能會好一點,可以防止饑餓進程的出現,即,當同時到達多個進程,
                只會有一個進程獲得鎖,其他的都用同樣的頻率進行嘗試,後面有來了一些進行,也以同樣的頻率申請鎖,這將可能導致前面來的鎖得不到滿足.
                使用隨機的等待時間可以一定程度上保證公平性
             */
            Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);

        }
        return false;
    }


    /**
     * Acqurired lock release.
     */
    public synchronized void unlock() {
        if (locked) {
            redisTemplate.delete(lockKey);
            locked = false;
        }
    }

}

 調用:

 RedisLock lock = new RedisLock(redisTemplate, key, 10000, 20000);
 try {
            if(lock.lock()) {
                   //需要加鎖的代碼
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //為了讓分散式鎖的演算法更穩鍵些,持有鎖的客戶端在解鎖之前應該再檢查一次自己的鎖是否已經超時,再去做DEL操作,因為可能客戶端因為某個耗時的操作而掛起,
//操作完的時候鎖因為超時已經被別人獲得,這時就不必解鎖了。 ————這裡沒有做
lock.unlock(); }

六、一些問題

1、為什麼不直接使用expire設置超時時間,而將時間的毫秒數其作為value放在redis中?

如下麵的方式,把超時的交給redis處理:

lock(key, expireSec){
isSuccess = setnx key
if (isSuccess)
expire key expireSec
}

  這種方式貌似沒什麼問題,但是假如在setnx後,redis崩潰了,expire就沒有執行,結果就是死鎖了。鎖永遠不會超時。

 2、為什麼前面的鎖已經超時了,還要用getSet去設置新的時間戳的時間獲取舊的值,然後和外面的判斷超時時間的時間戳比較呢?

  因為是分散式的環境下,可以在前一個鎖失效的時候,有兩個進程進入到鎖超時的判斷。如:

C0超時了,還持有鎖,C1/C2同時請求進入了方法裡面

C1/C2獲取到了C0的超時時間

C1使用getSet方法

C2也執行了getSet方法

假如我們不加 oldValueStr.equals(currentValueStr) 的判斷,將會C1/C2都將獲得鎖,加了之後,能保證C1和C2只能一個能獲得鎖,一個只能繼續等待。

註意:這裡可能導致超時時間不是其原本的超時時間,C1的超時時間可能被C2覆蓋了,但是他們相差的毫秒及其小,這裡忽略了。

 

致謝:感謝您的閱讀!轉載請加原文鏈接,謝謝。轉載請加上原文鏈接,謝謝!http://www.cnblogs.com/0201zcr/p/5942748.html


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

-Advertisement-
Play Games
更多相關文章
  • SQL 基礎知識梳理(四) - 數據更新 【博主】反骨仔 【原文】http://www.cnblogs.com/liqingwen/p/5929786.html 序 這是《SQL 基礎知識梳理(三) - 聚合和排序》的下篇。 目錄 插入數據 刪除數據 更新數據 事務 一、插入數據 1.INSERT ...
  • 本設計基於以下需求提出 1. 快速接入數據源表(貼源/落地) 2. 無須給單獨表開發轉換/作業 3. 動態生成數據源連接, 表欄位等信息(預先保存在數據倉庫中) 本設計所需條件 1. 數據源為關係型資料庫 2. 不同數據源需要寫一小段Java Scripts以保證數據源連接可用 總體作業結構 jb_ ...
  • 487down vote Differences KEY or INDEX refers to a normal non-unique index. Non-distinct values for the index are allowed, so the index may contain row ...
  • 1.在my.ini的 [mysqld] 下添加 skip-grant-tables 2.重啟mysql服務即可 ...
  • 上學時學的東西,都忘了,用到了,就翻出來學習了一下。使用存儲過程編寫,可直接運行該存儲過程註釋都寫好了,變數賦值也比較清楚,需要的可以直接複製下來然後替換就好。 ...
  • 需求:按照分組,將多條記錄內容合併成一條,效果如下: 資料庫示例: 根據不同的SQL版本,可以有以下方法: 一、SQL 2000 不支持FOR XML,不支持CONCAT。只能寫自定義函數。 二、SQL 2012 支持 concat,2000版本自定義函數的基礎上可少量優化 三、SQL2005支持f ...
  • 新浪微博:intsmaze劉洋洋哥。 storm框架中的kafkaspout類實現的是BaseRichSpout,它裡面已經重寫了fail和ack方法,所以我們的bolt必須實現ack機制,就可以保證消息的重新發送;如果不實現ack機制,那麼kafkaspout就無法得到消息的處理響應,就會在超時以 ...
  • 【等待事件】等待事件系列(5.1)--Enqueue(隊列等待) 1 BLOG文檔結構圖 2 前言部分 2.1 導讀和註意事項 各位技術愛好者,看完本文後,你可以掌握如下的技能,也可以學到一些其它你所不知道的知識,~O(∩_∩)O~: ① Enqueue隊列等待 ② Enq數據字典 ③ enq: A... ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...