1. 程式人生 > >【轉】基於Redis Lua指令碼實現的分散式鎖(Java實現)

【轉】基於Redis Lua指令碼實現的分散式鎖(Java實現)

最近專案中需要用到一個分散式的鎖,考慮到基於會話節點實現的zookeeper鎖效能不夠,於是想使用redis來實現一個分散式的鎖。看了網上的幾個實現方案後,發現都不夠嚴謹。比如這篇:用Redis實現分散式鎖裡面設計的鎖有個最大的問題是鎖的超時值TTL會一直被改寫,“儘管C3沒拿到鎖,但它改寫了C4設定的鎖的超時值,不過這一點非常微小的誤差帶來的影響可以忽略不計”,其實在高併發的時候會導致程序“餓死”(也有文章稱為死鎖)。還有這篇文章“兩種分散式鎖實現方案2”裡面的v2=getset(key,時間戮+超時+1),其加1秒操作在大併發下也會觸發同樣的問題。網上這篇文章解決了這個“無休止的TTL”問題,我簡單翻譯了下。

鎖是程式設計中非常常見的概念。在維基百科上對鎖有個相當精確的定義:

在電腦科學中,鎖是一種在多執行緒環境中用於強行限制資源訪問的同步機制。鎖被設計用於執行一個互斥的併發控制策略。

In computer science, a lock is a synchronization mechanism for
enforcing limits on access to a resource in an environment where there
are many threads of execution. A lock is designed to enforce a mutual
exclusion concurrency control policy.

簡單的說,鎖是一個單一的參考點,多個執行緒基於它來檢查是否允許訪問資源。例如,一個想寫資料的執行緒,它必須先檢查是否存在一個寫鎖。如果寫鎖存在,需要等待直到鎖釋放後它才能獲取到屬於它的鎖並執行寫操作。這樣,通過鎖就可以避免多個執行緒的同時寫造成的資料衝突。

現代的作業系統提供了內建的函式來幫助程式設計師實現併發控制,例如 flock 函式。但是如果多執行緒的程式執行在多臺機器上呢?如何在分散式系統下控制對資源的訪問呢?

使用一箇中心化的鎖服務

首先,我們需要一個所有執行緒都可以訪問到的地方來儲存鎖。這個鎖只能存在於一個地方,從而保證只有一個權威的地方可以定義鎖的建立和釋放。

Redis是實現鎖的一個理想的候選方案。作為一個輕量級的記憶體資料庫,快速,事務性和一致性是選擇redis所為鎖服務的主要原因。

設計鎖

鎖本身是很簡單的,就是redis資料庫中一個簡單的key。建立和釋放鎖,並保證絕對的安全,是這個鎖的設計比較棘手的地方。有兩個潛在的陷阱:

  1. 應用程式通過網路和redis互動,這意味著從應用程式發出命令到redis結果返回之間會有延遲。這段時間內,redis可能正在執行其他的命令,而redis內資料的狀態可能不是你的程式所期待的。如果保證程式中獲取鎖的執行緒和其他執行緒不發生衝突?

  2. 如果程式在獲取鎖後突然crash,而無法釋放它?這個鎖會一直存在而導致程式進入“餓死”(原文成為“死鎖”,感覺不太準確)。

建立鎖

可能想到的最簡單的方法是“用GET方法檢查鎖,如果鎖不存在,就用SET方式設定一個值”。

這個方法雖然簡單,但是不能保證獨佔鎖。回顧前面所說的第1個陷阱:因為在GET和SET操作之間有延遲,我們沒法知道從“傳送命令”到“redis伺服器返回結果”之間的這段時間內是否有其他執行緒也去建立鎖。當然,這些都在幾毫秒之內,發生的可能性相當低。但是如果在一個繁忙的環境中執行著大量的併發執行緒和命令,重疊的可能性並不是微不足道的。

為了解決這個問題,應該用SETNX命令。SETNX消除了GET命令需要等待返回值的問題,SETNX只有在key不存在時才返回成功。這意味著只有一個執行緒可以成功執行SETNX命令,而其他執行緒會失敗,然後不斷重試,直到它們能建立鎖。

釋放鎖

一旦執行緒成功執行了SETNX命令,它就建立了鎖並且可以基於資源進行工作。工作完成後,執行緒需要通過刪除redis的key來釋放這個鎖,從而允許其他執行緒能儘快的獲取鎖。

儘管如此,也有需要小心的地方!回顧前面說的第2個陷阱:如果執行緒crash了,它永遠都不會刪除redis的key,所以這個鎖會一直存在,從而導致“餓死”現象。那麼如何避免這個問題呢?

鎖的存活時間

我們可以給鎖加一個存活時間(TTL),這樣一旦TTL超時,這個鎖的key會被redis自動刪除。任何由於執行緒錯誤而遺留下來的鎖在一個合適的時間之後都會被釋放,從而避免了“餓死”。這純粹是一個安全特性,更有效的方式仍然是確保儘量線上程裡面釋放鎖。

可以通過PEXPIRE命令為Redis的key設定TTL,而且執行緒裡可以通過MULTI/EXEC事務的方式在SETNX命令後立即執行,例如:

MULTI
SETNX lock-key
PEXPIRE 10000 lock-key
EXEC

儘管如此,這會產生另外一個問題。PEXPIRE命令沒有判斷SETNX命令的返回結果,無論如何都會設定key的TTL。如果這個地方無法獲取到鎖或有異常,那麼多個執行緒每次想獲取鎖時,都會頻繁更新key的TTL,這樣會一直延長key的TTL,導致key永遠都不會過期。為了解決這個問題,我們需要Redis在一個命令裡面處理這個邏輯。我們可以通過Redis指令碼的方式來實現。

注意-如果不採用指令碼的方式來實現,可以使用Redis 2.6.12之後版本SET命令的PX和NX引數來實現。為了考慮相容2.6.0之前的版本,我們還是採用指令碼的方式來實現。

Redis指令碼

由於Redis支援指令碼,我們可以寫一個Lua指令碼在Redis服務端執行多個Redis命令。應用程式通過一條EVALSHA命令就可以呼叫被Redis服務端快取的指令碼。這裡強大的地方在於你的程式只需要執行一條命令(指令碼)就可以以事務的方式執行多個redis命令,還能避免併發衝突,因為一個redis指令碼同一時刻只能執行一次。

這是Redis裡面一個設定帶TTL的鎖的Lua指令碼:

--
-- Set a lock
--
-- KEYS[1]   - key
-- KEYS[2]   - ttl in ms
-- KEYS[3]   - lock content
local key     = KEYS[1]
local ttl     = KEYS[2]
local content = KEYS[3]

local lockSet = redis.call('setnx', key, content)

if lockSet == 1 then
  redis.call('pexpire', key, ttl)
end

return lockSet

從這個指令碼可以很清楚的看到,我們通過在鎖上只執行PEXPIRE命令就解決了前面提到的“無休止的TTL”問題。

Warlock: 一個成熟的基於Redis的分散式鎖

上面我們介紹了基於Redis的鎖的理論,這裡有一個用Node.js寫的開源模組Warlock,通過npm可以獲取。它使用了redis指令碼來建立/釋放鎖,用於為快取,資料庫,任務佇列和其他需要併發的地方提供分散式的鎖服務,詳見Github

原文中還缺少一個釋放鎖的指令碼,如果一直依賴TTL來釋放鎖,效率會很低。Redis的SET操作文件就提供了一個釋放鎖的指令碼:

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

應用程式中只要加鎖的時候指定一個隨機數或特定的value作為key的值,解鎖的時候用這個value去解鎖就可以了。當然,每次加鎖時的value必須要保證是唯一的。

下面是基於這篇文章,使用Java程式碼來實現分散式鎖,redis客戶端使用RedisTemplate

import java.util.Collections;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;

/**
 * redis分散式鎖.<br>
 * 思路:
 * <pre>
 * 用SETNX命令,SETNX只有在key不存在時才返回成功。這意味著只有一個執行緒可以成功執行SETNX命令,而其他執行緒會失敗,然後不斷重試,直到它們能建立鎖。
 * 然後使用指令碼來建立鎖,因為一個redis指令碼同一時刻只能執行一次。
 * 建立鎖程式碼:
 * <code>
-- KEYS[1] key,
-- ARGV[1] value,
-- ARGV[2] expireTimeMilliseconds

if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then 
    redis.call('pexpire', KEYS[1], ARGV[2]) 
    return 1 
else 
    return 0 
end
 * </code>
 * 最後使用指令碼來解鎖。
 * 解鎖程式碼:
 * 
 * <code>
-- KEYS[1] key,
-- ARGV[1] value
if redis.call("get", KEYS[1]) == ARGV[1]
then
    return redis.call("del", KEYS[1])
else
    return 0
end
 * </code>
 * </pre>
 * 
 * @author tanghc
 */
public class RedisLockUtil {

    private static final Long SUCCESS = 1L;

    // 加鎖指令碼
    private static final String SCRIPT_LOCK = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end";
    // 解鎖指令碼
    private static final String SCRIPT_UNLOCK = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    // 加鎖指令碼sha1值
    private static final String SCRIPT_LOCK_SHA1 = Sha1Util.encrypt(SCRIPT_LOCK);
    // 解鎖指令碼sha1值
    private static final String SCRIPT_UNLOCK_SHA1 = Sha1Util.encrypt(SCRIPT_UNLOCK);

    /**
     * 嘗試獲取分散式鎖
     * 
     * @param redisTemplate
     *            Redis客戶端
     * @param lockKey
     *            鎖
     * @param requestId
     *            請求標識
     * @param expireTimeMilliseconds
     *            超期時間,多少毫秒後這把鎖自動釋放
     * @return 返回true表示拿到鎖
     */
    @SuppressWarnings("unchecked")
    public static boolean tryGetDistributedLock(@SuppressWarnings("rawtypes") final RedisTemplate redisTemplate,
            final String lockKey, final String requestId, final int expireTimeMilliseconds) {

        Object result = redisTemplate.execute(new RedisScript<Long>() {
            @Override
            public String getSha1() {
                return SCRIPT_LOCK_SHA1;
            }

            @Override
            public Class<Long> getResultType() {
                return Long.class;
            }

            @Override
            public String getScriptAsString() {
                return SCRIPT_LOCK;
            }

        }, Collections.singletonList(lockKey),// KEYS[1]
                requestId, // ARGV[1]
                String.valueOf(expireTimeMilliseconds) // ARGV[2]
                );

        return SUCCESS.equals(result);
    }

    /**
     * 釋放分散式鎖
     * 
     * @param redisTemplate
     *            Redis客戶端
     * @param lockKey
     *            鎖
     * @param requestId
     *            請求標識
     * @return 返回true表示釋放鎖成功
     */
    @SuppressWarnings("unchecked")
    public static boolean releaseDistributedLock(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate,
            String lockKey, String requestId) {

        Object result = redisTemplate.execute(new RedisScript<Long>() {
            @Override
            public String getSha1() {
                return SCRIPT_UNLOCK_SHA1;
            }

            @Override
            public Class<Long> getResultType() {
                return Long.class;
            }

            @Override
            public String getScriptAsString() {
                return SCRIPT_UNLOCK;
            }

        }, Collections.singletonList(lockKey), requestId);

        return SUCCESS.equals(result);
    }

}
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Sha1Util {

    private static final String ZERO = "0";
    private static final String ALGORITHM = "SHA1";

    /**
     * sha1加密
     * @param str
     * @return 返回十六進位制的字串形式,全部小寫
     */
    public static String encrypt(String str) {
        if (str == null) {
            return null;
        }
        try {
            MessageDigest messageDigest = MessageDigest.getInstance(ALGORITHM);
            messageDigest.update(str.getBytes());
            return byte2hex(messageDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 二進位制轉十六進位制字串
     * 
     * @param bytes
     * @return
     */
    public static String byte2hex(byte[] bytes) {
        StringBuilder sign = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if (hex.length() == 1) {
                sign.append(ZERO);
            }
            sign.append(hex);
        }
        return sign.toString();
    }

}

相關推薦

基於Redis Lua指令碼實現分散式Java實現

最近專案中需要用到一個分散式的鎖,考慮到基於會話節點實現的zookeeper鎖效能不夠,於是想使用redis來實現一個分散式的鎖。看了網上的幾個實現方案後,發現都不夠嚴謹。比如這篇:用Redis實現分散式鎖裡面設計的鎖有個最大的問題是鎖的超時值TTL會一直被改寫

PANDAS 數據合並與重塑concat篇

分享 levels 不同的 整理 con 簡單 post ignore num 轉自:http://blog.csdn.net/stevenkwong/article/details/52528616 1 concat concat函數是在pandas底下的方法,可以將數據

Python+opencv利用sobel進行邊緣檢測細節講解

#! usr/bin/env python # coding:utf-8 # 2018年7月2日06:48:35 # 2018年7月2日23:11:59 import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2

讀書22014基於MATLAB的雷達訊號處理基礎第二版——接收機(3)

圖1.9指出了對高質量接收機設計的幾個要求。 Figure 1.9 implies several requirements ona high-quality receiver design. 例如,本地振盪器與發射機頻率必須是相同的。 For example, the loca

讀書22014基於MATLAB的雷達訊號處理基礎第二版——接收機(2)

圖1.10對該問題進行了描述。 Figure 1.10 illustrates the problem. 圖1.10 (a)圖1.9中接收機的I通道只測量相位θ(t) 的餘弦值;(b)Q通道只測量相位θ(t) 的正弦值。(a) The Ichannel of the recei

讀書22014基於MATLAB的雷達訊號處理基礎第二版——接收機(1)

1.3.3. 接收機 1.3.3. Receivers 第1.3.1節指出,雷達訊號通常是窄帶、帶通的相位調製或頻率調製訊號。 It was shown in Sec. 1.3.1that radar signals are usually narrowband, bandpas

讀書22014基於MATLAB的雷達訊號處理基礎第二版——天線(5)

第n個陣元的訊號複數加權權值為an。 The signal in branch n is weighted with thecomplex weight an . 若參考陣元接收到的電場強度為E0exp(jΩt),那麼整個陣列接收的總電壓E為 For an incoming el

讀書22014基於MATLAB的雷達訊號處理基礎第二版——信幹比與積累(1)

Es與A之間的比例關係與訊號的形狀有關。 Theproportionality between Es and A depends on the signal shape. 對於幅度為A、持續N個取樣的矩形脈衝或復指數訊號,則Es = N · A2。 For a rectan

讀書22014基於MATLAB的雷達訊號處理基礎第二版——解析度(1)

圖1.14以頻率為例描述了解析度的概念。 Figure 1.14 illustrates the concept ofresolution, in this case in frequency. Figure 1.14. 頻率上兩個正弦波的解析度,每個正弦波的瑞利頻率寬度為10

JRebel外掛安裝配置與破解啟用多方案詳細教程

JRebel 介紹   IDEA上原生是不支援熱部署的,一般更新了 Java 檔案後要手動重啟 Tomcat 伺服器,才能生效,浪費不少生命啊。目前對於idea熱部署最好的解決方案就是安裝JRebel外掛,這樣不論是更新 class 類還是更新 Spring 配置檔案都能做

讀書22014基於MATLAB的雷達訊號處理基礎第二版——訊號調理與干擾抑制(1)

空時自適應濾波(STAP)結合角度和多普勒域的自適應波束形成,同時實現雜波和干擾的抑制。 Space-time adaptive filtering (STAP)combines adaptive beamforming in both angle and Doppler for sim

讀書22014基於MATLAB的雷達訊號處理基礎第二版——本質現象(1)

例如,在所有其它條件都相同的情況下,如果發射更大的功率,則接收到的回波訊號更強。 For example, if more power is transmitted amore powerful received echo is expected, all other things be

騰訊研發類筆試面試試題C++方向

C的記憶體基本上分為4部分:靜態儲存區、堆區、棧區以及常量區。他們的功能不同,對他們使用方式也就不同。 1.棧 ——由編譯器自動分配釋放; 2.堆 ——一般由程式設計師分配釋放,若程式設計師不釋放,程式結束時可能由OS回收; 3.全域性區(靜態區)——全域性變數

ZooKeeper完全解析(七) 使用ZooKeeper實現分散式Java實現

  在上一節中,我們講了使用ZooKeeper來實現分散式鎖的原理,連結為  ZooKeeper完全解析(六) 使用ZooKeeper實現分散式鎖之實現原理 ,這一節我們來講一下如何使用Java來實現分散式鎖:   在實現原理中,我們把使用ZooKeeper實現分散式鎖分成

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

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

使用ssh-keygen和ssh-copy-id三步實現SSH無密碼登錄

works message targe auth mes unix use ner not 【原】http://blog.chinaunix.net/uid-26284395-id-2949145.html ssh-keygen 產生公鑰與私鑰對. ssh-copy-id

基於Map的簡易記憶化緩存

還在 自己 == map cti extends inter end 參考資料 看到文章後,自己也想寫一些關於這個方面的,但是覺得寫的估計沒有那位博主好,而且又會用到裏面的許多東西,所以幹脆轉載。但是會在文章末尾寫上自己的學習的的東西。 原文出處如下: http://www

基於localStorage的資源離線和更新技術

同時 前端 event 原來 read 前端資源 獲取 tex tor ServiceWorker的資源離線與更新 ServiceWorker是替代Application Cache的機制,目前為止其兼容性很差。 localStorage資源離線緩存與更新 基本思路:將

文件下載之斷點續傳客戶端與服務端的實現

http協議 當前時間 end box [] ada demo 服務端 sem 【轉】文件下載之斷點續傳(客戶端與服務端的實現) 【轉】文件下載之斷點續傳(客戶端與服務端的實現) 前面講了文件的上傳,今天來聊聊文件的下載。 老規矩,還是從最簡單粗暴的開始。那麽多簡單算簡單

金蝶EAS BOS工作流開發附帶JAVA指令碼

目錄(?)[+] 流程配置基本知識及示例 重要概念 流程變數 任務輸入輸出 注意事項 基本流程的配置示例