Redis 實(shí)現(xiàn)簡單的分布式鎖

借助 SETNX(不完全正確)

Redis 中 SETNE 只有在 key 不存在時(shí)設(shè)置 key 的值,因此非常容易就實(shí)現(xiàn)了鎖功能。只需要客戶端對指定 KEY 成功設(shè)置一個隨機(jī)值,借助這個值來防止其他的進(jìn)程取得鎖。

127.0.0.1:6379> get simpleLock
(nil)
127.0.0.1:6379> setnx simpleLock Locked 
(integer) 1    //成功得到鎖返回1
127.0.0.1:6379> get simpleLock 
"Locked"
127.0.0.1:6379> setnx simpleLock Release 
(integer) 0   // 這里重新設(shè)置鎖的值,返回0代表其他客戶端獲得鎖
127.0.0.1:6379> get simpleLock
"Locked"
127.0.0.1:6379> 
127.0.0.1:6379> del simpleLock  // 刪除鍵,釋放鎖
(integer) 1 
127.0.0.1:6379> setnx simpleLock Release //再重新獲取鎖
(integer) 1
127.0.0.1:6379> 

通過 SETNX 基本能實(shí)現(xiàn)一個不完全正確的鎖,Java代碼如下:

package me.touch.redis;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@RunWith(JUnit4.class)
public class SimpleLock {

    private Jedis jedis;
    private JedisPool pool;

    @Before
    public void setUp() {
        pool = new JedisPool(new JedisPoolConfig(), "localhost");
        jedis = pool.getResource();
    }

    @After
    public void after() {
        jedis.close();
        pool.destroy();
    }
    
    /**
     * 獲得簡單鎖
     * @return
     */
    public boolean acquireSimpleLock(String lockName){
        return jedis.setnx(lockName, "Locked") == 1 ;
    }
    
    /**
     * 釋放鎖
     * @return
     */
    public boolean releaseSimpleLock(String lockName){
        return jedis.del(lockName, "Locked") == 1 ;
    }
    
    @Test
    public void test(){
        if(acquireSimpleLock("simpleLock")){
            System.out.println("獲取鎖成功 ·····");
            
            // Do something ........
            
            if(releaseSimpleLock("simpleLock")){
                System.out.println("釋放鎖成功 ·····");
            }
            
        }
    }
}   

運(yùn)行結(jié)果:

獲取鎖成功 ·····
釋放鎖成功 ·····

但是這個鎖是不完全正確的,缺少超時(shí)機(jī)制,缺少重試機(jī)制,釋放鎖的時(shí)候沒有驗(yàn)證當(dāng)前鎖是否由當(dāng)前進(jìn)程擁有等。
一個不完全正確的鎖會導(dǎo)致一些不正確的行為,如:

  • 當(dāng)缺少超時(shí)機(jī)制時(shí),當(dāng)持有鎖的進(jìn)程死掉后,鎖得不釋放,造成死鎖。
  • 當(dāng)持有鎖的進(jìn)程操作時(shí)間過長導(dǎo)致鎖自動釋放,但是很進(jìn)程本身不知道,使得邏輯完成后錯誤的釋放其他進(jìn)程的鎖(需要驗(yàn)證鎖是否是當(dāng)前進(jìn)程持有)。
  • 當(dāng)持有鎖的進(jìn)程崩潰后,其他進(jìn)程無法檢測到,只能浪費(fèi)時(shí)間等待鎖達(dá)到超時(shí)時(shí)候被釋放。
  • 當(dāng)一個進(jìn)程持有鎖過期后,其他多個進(jìn)程同時(shí)嘗試去獲取鎖,并且都獲取了鎖,而且都認(rèn)為自己是唯一一個獲取到鎖的進(jìn)程(需要驗(yàn)證鎖是否是當(dāng)前進(jìn)程持有)

使用 Luna 腳本 (基本正確)

Redis 中的命令是原子執(zhí)行,的所以我們可以在 Lua 腳本中組合多個命令來完成我們的的邏輯。

lua 腳本獲取鎖

--  EXISTS 判斷是否存在 KEY ,如果存在,說明其他進(jìn)程已經(jīng)獲得鎖,不存在這,設(shè)置KEY
if redis.call('EXISTS', KEYS[1]) == 0 then
    return redis.call('SETEX', KEYS[1], unpack(ARGV))
end

lua 腳本釋放鎖

-- GET 獲取 KEY 值,判斷是否與指定的值相等,相等則刪除KEY
if redis.call('GET', KEYS[1]) == ARGV[1] then 
  return redis.call('DEL', KEYS[1]) or true 
end

Java 源碼

     /**
     * 獲得鎖
     * @param keyName 鎖的名稱
     * @param keyVlaue 鎖的值,建議使用UUID
     * @param expire 鎖的過期時(shí)間
     * @param timeout 獲取所得超時(shí)時(shí)間,毫秒
     * @return
     */
    public boolean  acquireLockWhithTimeOut(String keyName, String keyVlaue, 
                  String expire, long timeout){
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call('EXISTS', KEYS[1]) == 0 then \n")
          .append(" return redis.call('SETEX', KEYS[1], unpack(ARGV)) \n")
          .append("end");
        
        long now = System.currentTimeMillis();
        do{
            if("OK".equals(jedis.eval(sb.toString(), 1, keyName, expire,  keyVlaue))){
                return true;
            }
        }while( System.currentTimeMillis() < (now + timeout));
        
        return false;
    }
    
    
    /**
     * 釋放鎖
     * @param keyName 鎖名稱
     * @param keyVlaue 鎖的值
     * @return
     */
    public boolean   releaseLock(String keyName, String keyVlaue){
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call('GET', KEYS[1]) == ARGV[1] then \n")
          .append(" return redis.call('DEL', KEYS[1]) or true \n")
          .append("end");
        return ((Long) jedis.eval(sb.toString(), 1, keyName,  keyVlaue))  == 1 ;
    }

     @Test
    public void test() throws InterruptedException{
        //使用 uuid 作為鎖的值
        String  uuid = UUID.randomUUID().toString();
        if(acquireLockWhithTimeOut("simpleLock",  uuid, "60", 60*1000)){
            System.out.println("獲取鎖成功 ·····");
            // Do something ........
            TimeUnit.SECONDS.sleep(1); // 線程睡上30秒
            if(releaseLock("simpleLock", uuid)){
                System.out.println("釋放鎖成功 ·····");
            }
        }
    }

運(yùn)行結(jié)果:

b308b026-8b01-4cf0-b145-b9061bf617f6
獲取鎖成功 ·····
釋放鎖成功 ·····

在這個例子中通過傳入 timeout 設(shè)置獲取鎖的超時(shí)時(shí)間實(shí)現(xiàn)了鎖獲取的重試機(jī)制;同時(shí),通過 expire 指定了 key 的過期時(shí)間,避免照成了死鎖。在獲取鎖時(shí)指定的值為UUID,保證了鎖的唯一性。此外,在釋放鎖時(shí)比較 UUID 成功避免錯誤釋放其他進(jìn)程鎖的問題,因此也不會出現(xiàn)多個進(jìn)程多獲取到鎖的情況。當(dāng)前實(shí)現(xiàn)已經(jīng)是基本正確的鎖實(shí)現(xiàn)了,能用于絕大部分應(yīng)用場景,但是依然沒有解決因?yàn)槌钟墟i的進(jìn)程崩潰造成其他進(jìn)程浪費(fèi)時(shí)間等待鎖過期的問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容