1. 程式人生 > >利用redis實現的分散式鎖

利用redis實現的分散式鎖

假設一個場景:
在分散式系統中,通常會遇到多個伺服器處理同一個業務的場景,我們需要利用某種機制避免併發問題。Java語言中,我們可以通過鎖的方式避免單個服務的多執行緒併發問題,而分散式系統中的併發問題用Java的鎖機制是很難解決的。
分散式鎖也有類似地“首先獲取鎖, 然後執行操作,最後釋放鎖”的動作,為了解決分散式系統的併發問題,我們可以使用redis實現一個跨機器的分散式鎖。

下面先看redis的SETNX命令:

命令:
    SETNX key value
解釋:
    將 key 的值設為 value ,當且僅當 key 不存在。
    若給定的 key 已經存在,則 SETNX 不做任何動作。
返回值:
    設定成功,返回 1 。
    設定失敗,返回 0 。

redis提供的SETNX命令天生具有基本的加鎖功能。把要鎖定資源的某個標識作為key,並設定全域性唯一的值作為redis的key的值,如果SETNX key value返回1那麼我們認為獲取鎖成功,否則認為獲取鎖失敗。
但是使用SETNX只能構建一個簡單的分散式鎖,無法解決如下問題:
1.獲取鎖的方法是否有超時時間?
2.某個服務成功獲取了某個分散式鎖,此時該服務崩潰,那麼該分散式鎖將因鎖持有者崩潰而無法釋放。
為了解決如上問題,我們將對redis的SETNX命令進行封裝,構建一個具有高階特性的分散式鎖。
獲取鎖方法的基本思路:為了對資料進行排他性訪問,程式首先需要獲取鎖,利用SETNX命令為key設value,如果key不存在則設值成功,此時認為獲取鎖成功並把value返回。我們可以認為key和value共同構成了一把鎖,在釋放鎖的實現中將利用key和value來保證釋放正確的鎖;如果獲取鎖失敗,程式將不斷重試直到設值成功或超過給定時間限制。為了防止鎖持有者奔潰而導致鎖無法釋放,在呼叫SETNX設定成功之後,我們將呼叫redis的另一個命令EXPIRE為鎖也就是key設定過期時間,使得redis可以自動刪除過期的鎖而不必擔心鎖持有者崩潰造成鎖無法釋放。為了避免在獲取鎖成功後、設定過期時間之前獲取鎖的方法出現異常從而導致的設定鎖的過期時間失敗,在獲取鎖失敗後,獲取鎖的方法還將檢查已設值的key是否設定了過期時間,如果沒有設定過期時間,程式將給該鎖設定過期時間以保證萬無一失。
以下是程式碼實現:
    /**
     * 獲取鎖。
     * 該獲取鎖方法有如下特性:
     * 1.如果獲取鎖成功,會設定鎖的生存時間;
     * 2.雖然大多數情況下redis的鎖都有生存時間,
     * 但是為了防止在上鎖後、設定鎖的生存週期
     * 之前獲取鎖的方法出現了異常而終止。我們加入如下判斷:
     * 如果獲取鎖失敗,會檢查已存在鎖是否設定有生存時間,
     * 如果沒有設定生存時間,那麼會給鎖設定生存時間。
     * 。
     *
     * @param conn        redis連線
     * @param lockName    鎖名稱
     * @param waitTimeOut 等待獲取鎖的超時時間(毫秒)
     * @param lockTimeOut 鎖的生存時間(秒)
     * @return 如果獲取鎖成功則返回鎖鍵對應值,否則返回null
     */
    private String acquireLockWithTimeOut(Jedis conn, String lockName, long waitTimeOut, int lockTimeOut) {
        String lockKey = "lock:" + lockName;
        String lockId = UUID.randomUUID().toString();
        long end = System.currentTimeMillis() + waitTimeOut;
        int i = 0;
        while (System.currentTimeMillis() < end) {
            if (conn.setnx(lockKey, lockId) == 1) {
                conn.expire(lockKey, lockTimeOut);
                System.out.println("acquire lock '" + lockName + "',lockId=" + lockId + ",retry " + i);
                return lockId;
            }
            if (conn.ttl(lockKey) < 0) {
                conn.expire(lockKey, lockTimeOut);
            }
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            i++;
        }
        return null;
    }

既然有獲取鎖的方法,那麼也會有釋放鎖的方法:在獲取鎖之後,獲取鎖的方法會返回鎖的value標識,在釋放鎖的時候,將根據鎖和鎖的value標識來釋放鎖,以免錯誤地釋放了其他持有者的鎖。此外,釋放鎖的方法也帶有超時功能,如果釋放失敗,程式將重試直到成功或超時。

以下是程式碼實現:

    /**
     * 解鎖。
     * 解鎖時將判斷鎖鍵對應值是否是給定的值,防止誤解鎖。
     *
     * @param conn         redis連線
     * @param lockName     鎖名稱
     * @param lockId       鎖鍵對應值
     * @param waiteTimeOut 解鎖動作的超時時間(毫秒)
     * @return true如果解鎖成功,否則返回false
     */
    private boolean releaseLock(Jedis conn, String lockName, String lockId, long waiteTimeOut) {
        String lockKey = "lock:" + lockName;
        long end = System.currentTimeMillis() + waiteTimeOut;
        int i = 0;
        while (System.currentTimeMillis() < end) {
            conn.watch(lockKey);
            if (lockId.equals(conn.get(lockKey))) {
                Transaction trans = conn.multi();
                trans.del(lockKey);
                List<Object> exec = trans.exec();
                if (exec != null) {
                    System.out.println("release lock '" + lockName + "',lockId=" + lockId + ",retry " + i);
                    return true;
                }
                i++;
                continue;
            }
            conn.unwatch();
            break;
        }
        return false;
    }

以下是測試方法:
    /**
     * 分散式鎖的測試方法
     *
     * @param threads 模擬獲取鎖的請求執行緒數
     */
    public void test(int threads) {
        final AtomicInteger acquireFailCount = new AtomicInteger();
        final AtomicInteger acquireCount = new AtomicInteger();

        final CountDownLatch latch = new CountDownLatch(0);
        final CountDownLatch endLatch = new CountDownLatch(threads);
        final List<Long> countList = new ArrayList<Long>();
        ExecutorService executorService = Executors.newFixedThreadPool(threads);
        for (int i = 0; i < threads; i++) {
            executorService.execute(new Runnable() {
                public void run() {
                    final Jedis conn = new Jedis("localhost");
                    conn.select(0);
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int i1 = 0; i1 < 5; i1++) {
                        long start = System.currentTimeMillis();
                        acquireCount.incrementAndGet();
                        String aLock = acquireLockWithTimeOut(conn, "aLock", 100, 1);
                        if (aLock != null) {
                            countList.add(System.currentTimeMillis() - start);
                            releaseLock(conn, "aLock", aLock, 100);
                        } else {
                            acquireFailCount.incrementAndGet();
                        }
                    }
                    endLatch.countDown();
                }
            });
        }
        latch.countDown();
        try {
            endLatch.await();
        } catch (InterruptedException ignore) {
        }
        executorService.shutdown();
        long count = 0;
        for (Long aLong : countList) {
            count += aLong;
        }
        System.out.println("併發量:" + threads + ",嘗試獲取鎖" + acquireCount + "次,其中成功" + (acquireCount.get() - acquireFailCount.get()) + "次,獲取鎖平均耗時" + (count / (double) countList.size()) + "毫秒。");
    }

在我的計算機(12G記憶體,I5處理器)對分散式鎖進行測試,模擬5個獲取鎖的執行緒,每個執行緒請求5次,結果如下:
併發量:5,嘗試獲取鎖25次,其中成功25次,獲取鎖平均耗時17.32毫秒。

可以看出,這個效能還是可以滿足日常需求的。

相關推薦

利用Redis實現分散式 使用mysql樂觀解決併發問題

寫在最前面 我在之前總結冪等性的時候,寫過一種分散式鎖的實現,可惜當時沒有真正應用過,著實的心虛啊。正好這段時間對這部分實踐了一下,也算是對之前填坑了。 分散式鎖按照網上的結論,大致分為三種:1、資料庫樂觀鎖; 2、基於Redis的分散式鎖;3.、基於ZooKeeper的分散式鎖; 關於樂觀鎖的實現其實

利用Redis實現分散式

為什麼需要分散式鎖? 在傳統單體應用單機部署的情況下,可以使用Java併發相關的鎖,如ReentrantLcok或synchronized進行互斥控制。但是,隨著業務發展的需要,原單體單機部署的系統,漸漸的被部署在多機器多JVM上同時提供服務,這使得原單機部署情況下的併發控制鎖策略失效了,為了解決這個問

ZooKeeper分散式簡單實踐 利用Redis實現分散式

寫在最前面 前幾周寫了篇 利用Redis實現分散式鎖 ,今天簡單總結下ZooKeeper實現分散式鎖的過程。其實生產上我只用過Redis或者資料庫的方式,之前還真沒了解過ZooKeeper怎麼實現分散式鎖。這周簡單寫了個小Demo,更堅定了我繼續使用Redis的信心了。 ZooKeep

Redis利用 Redis 實現分散式

## 技術背景 首先我們需要先來了解下什麼是分散式鎖,以及為什麼需要分散式鎖。 對於這個問題,我們可以簡單將鎖分為兩種——記憶體級鎖以及分散式鎖,記憶體級鎖即我們在 Java 中的 synchronized 關鍵字(或許加上程序級鎖修飾更恰當些),而分散式鎖則是應用在分散式系統中的一種鎖機制。分散式鎖的應

高併發場景系列(一) 利用redis實現分散式事務,解決高併發環境下減庫存

問題描述:某電商平臺,首發一款新品手機,每人限購2臺,預計會有10W的併發,在該情況下,如果扣減庫存,保證不會超賣 方案一 利用資料庫鎖機制,對記錄進行鎖定,再進行操作  SELECT * from goods where ID =1 for updat

利用redis實現分散式事務,解決高併發環境下減庫存

http://download.redis.io/releases/ 安裝: sudo make test 測試編譯 sudo make install 啟動: redis-servre cd “安裝目錄” redis-server ./redis-3.2.9/redis

通過Redis 實現分散式_利用Jedis 客戶端

前言 分散式鎖一般有三種實現方式: 資料庫樂觀鎖;2. 基於Redis的分散式鎖;3. 基於ZooKeeper的分散式鎖。 本篇部落格將介紹第二種方式,基於Redis實現分散式鎖。 雖然網上已經有各種介紹Redis分散式鎖實現的部落格,然而他們的實現卻有著各種各樣的問題,為了避免誤人子弟,本篇部落格將詳細

redis】使用redisTemplate優雅地操作redis及使用redis實現分散式

前言: 上篇已經介紹了redis及如何安裝和叢集redis,這篇介紹如何通過工具優雅地操作redis. Long Long ago,程式猿們還在通過jedis來操作著redis,那時候的猿類,一個個累的沒日沒夜,重複的造著輪子,忙得沒時間陪家人,終於有一天猿類的春天來了,spring家族的r

利用Redisson實現分散式及其底層原理解析

Redis介紹 redis是一個key-value儲存系統。和Memcached類似,它支援儲存的value型別相對更多,包括string(字串)、list(連結串列)、set(集合)、zset(sorted set --有序集合)和hash(雜湊型別)。這些資料型別都支援push/po

基於Redis實現分散式

背景 在很多網際網路產品應用中,有些場景需要加鎖處理,比如:秒殺,全域性遞增ID,樓層生成等等。大部分的解決方案是基於DB實現的,Redis為單程序單執行緒模式,採用佇列模式將併發訪問變成序列訪問,且多客戶端對Redis的連線並不存在競爭關係。其次Redis提供一些命令SETNX,GETSET,可以方便

如何用 Redis 實現分散式和超時情況處理

目前各種分散式的架構和微服務架構無處不在,在這種類似架構中處理併發和分散式併發問題,本場 Chat 就主要以 Redis 為例,使用分散式鎖的方式如何處理併發問題和避免超時情況的出現,主要從以下幾個方面講述: Redis 的 Setnx 命令是如何實現分散式鎖的; Setnx 的實現鎖的

分散式-使用Redis實現分散式

使用Redis實現分散式鎖 關於分散式鎖的實現,我的前一篇文章講解了如何使用Zookeeper實現分散式鎖。關於分散式鎖的背景此處不再做贅述,我們直接討論下如何使用Redis實現分散式鎖。 關於Redis,筆主不打算做長篇大論的介紹,只介紹下Redis優秀的特性

REDIS 學習(10)流程圖解使用redis實現分散式

redis作為集中式快取,可以通過它來實現分散式鎖。 首先用到的redis操作有: setnx key value:      當key不存在的時候生效並返回1,當已經有此key的時候返回0 getset key value:    

RedLock演算法-使用redis實現分散式服務

譯自Redis官方文件 在多執行緒共享臨界資源的場景下,分散式鎖是一種非常重要的元件。 許多庫使用不同的方式使用redis實現一個分散式鎖管理。 其中有一部分簡單的實現方式可靠性不足,可以通過一些簡單的修改提高其可靠性。 這篇文章介紹了一種指導性的redis分散式鎖演算法RedLock,RedL

Redis實現分散式(spring定時任務叢集應用Redis分散式

         之前2片文章介紹了 描述:              不管用不用動態執行,單機服務都是沒有問題的,但是如果服務是叢集模式下,那麼一個任務在每臺機器都會執行一次,這肯定不是我們需要的,我們要實現的是整個叢集每次只有一個任務執行成功,但是spring

Redis實現分散式Redis實現分散式

前言 分散式鎖一般有三種實現方式:1. 資料庫樂觀鎖;2. 基於Redis的分散式鎖;3. 基於ZooKeeper的分散式鎖。本篇部落格將介紹第二種方式,基於Redis實現分散式鎖。雖然網上已經有各種介紹Redis分散式鎖實現的部落格,然而他們的實現卻有著各種各樣的問題,為

使用redis實現分散式

大致思路: 加鎖: 利用set方法的nx,px引數: nx引數表示當key不存在時才成功,key存在時失敗,故保證了排他性. px引數表示設定過期時間,保證了執行緒出現異常無法釋放鎖(刪除key) 釋放鎖: 不能簡單地刪除鍵,因為可能出現這樣的情況:執行緒成功se

Redis實現分散式機制

Redis實現分散式鎖思路   常用的是redis函式是setnx(),這個應該是實現分散式鎖最主要的函式。首先是將某一業務標識名作為鍵存到redis裡,併為其設個過期時間,如果是還有加鎖請求過來,先是通過setnx()看看是否能將鎖的標識插入到redis裡,可

基於 Redis 實現分散式

什麼是Redis? Redis通常被稱為資料結構伺服器。這意味著Redis通過一組命令提供對可變資料結構的訪問,這些命令使用帶有TCP套接字和簡單協議的伺服器 - 客戶端模型傳送。因此,不同的程序可以以共享方式查詢和修改相同的資料結構。 Redis中實現的資料結構有一些特殊屬性:

redis實現分散式(基於lua指令碼操作)

lua指令碼能保證redis的原子性操作,redis使用springboot的redistemplate /** * create by abel * create date 2018/11/16 11:28 * describe:請輸入專案描述 */ public class