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

借助 SETNX(不完全正確)

Redis 中 SETNE 只有在 key 不存在時(shí)設(shè)置 key 的值劝术,因此非常容易就實(shí)現(xiàn)了鎖功能。只需要客戶端對(duì)指定 KEY 成功設(shè)置一個(gè)隨機(jī)值,借助這個(gè)值來防止其他的進(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)一個(gè)不完全正確的鎖,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é)果:

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

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

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

使用 Luna 腳本 (基本正確)

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

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
獲取鎖成功 ·····
釋放鎖成功 ·····

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子丐怯,更是在濱河造成了極大的恐慌喷好,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件读跷,死亡現(xiàn)場(chǎng)離奇詭異绒窑,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)舔亭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門些膨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人钦铺,你說我怎么就攤上這事订雾。” “怎么了矛洞?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵洼哎,是天一觀的道長。 經(jīng)常有香客問我沼本,道長噩峦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任抽兆,我火速辦了婚禮识补,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辫红。我一直安慰自己凭涂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布贴妻。 她就那樣靜靜地躺著切油,像睡著了一般。 火紅的嫁衣襯著肌膚如雪名惩。 梳的紋絲不亂的頭發(fā)上澎胡,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音娩鹉,去河邊找鬼攻谁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛底循,可吹牛的內(nèi)容都是我干的巢株。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼熙涤,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼阁苞!你這毒婦竟也來了困檩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤那槽,失蹤者是張志新(化名)和其女友劉穎悼沿,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骚灸,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡糟趾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了甚牲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片义郑。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖丈钙,靈堂內(nèi)的尸體忽然破棺而出非驮,到底是詐尸還是另有隱情,我是刑警寧澤雏赦,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布劫笙,位于F島的核電站,受9級(jí)特大地震影響星岗,放射性物質(zhì)發(fā)生泄漏填大。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一俏橘、第九天 我趴在偏房一處隱蔽的房頂上張望允华。 院中可真熱鬧,春花似錦敷矫、人聲如沸例获。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蠕搜,卻和暖如春怎茫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妓灌。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工轨蛤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人虫埂。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓祥山,卻偏偏與公主長得像,于是被迫代替她去往敵國和親掉伏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子缝呕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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