在 Java 中利用 redis 實(shí)現(xiàn)一個(gè)分布式鎖服務(wù)

在現(xiàn)代的編程語(yǔ)言中,接觸過多線程編程的程序員多多少少對(duì)鎖有一定的了解呻惕。簡(jiǎn)單的說荆责,多線程中的鎖就是在多線程環(huán)境下,多個(gè)線程對(duì)共享資源進(jìn)行修改的時(shí)候亚脆,保證共享資源一致性的機(jī)制做院。這里不展開說。在分布式環(huán)境下,原來(lái)的多線程的鎖就不管用了键耕,也就出現(xiàn)了分布式鎖的需求寺滚。所謂分布式鎖服務(wù)也就是在分布式環(huán)境下,保證多個(gè)分布式的服務(wù)共享的資源一致性的服務(wù)屈雄。

在分布式環(huán)境下實(shí)現(xiàn)一個(gè)分布式鎖服務(wù)并不太容易村视,需要考慮很多在單進(jìn)程下的鎖服務(wù)不需要考慮的問題。分布式鎖鎖的實(shí)現(xiàn)也有很多酒奶。這里我們討論在 Java 中通過 redis 來(lái)實(shí)現(xiàn)蚁孔。在 GitHub 中的 redisson 項(xiàng)目中已經(jīng)有開源的實(shí)現(xiàn)。但是那個(gè)太復(fù)雜了⊥锖浚現(xiàn)在我們來(lái)基于單機(jī)的 redis 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的分布式鎖服務(wù)杠氢。這個(gè)服務(wù)必須滿足下面的要求

  1. 支持立即獲取鎖方式,如果獲取到返回true另伍,獲取不到則返回false鼻百;
  2. 支持等待獲取鎖方式,如果獲取到摆尝,直接返回true温艇,獲取不到在等待一小段時(shí)間,在這一小段時(shí)間內(nèi)反復(fù)嘗試堕汞,如果嘗試成功中贝,則返回true,等待時(shí)間過后還獲取不到則返回false臼朗;
  3. 不能產(chǎn)生死鎖的情況;
  4. 不能釋放非自己加的鎖蝎土;

下面我們用實(shí)例來(lái)演示在 Java 中利用 redis 實(shí)現(xiàn)分布式鎖服務(wù)

加鎖

通過 redis 來(lái)實(shí)現(xiàn)分布式鎖的加鎖邏輯如下所示:

redis 分布式鎖上鎖實(shí)現(xiàn)邏輯

根據(jù)這個(gè)邏輯视哑,實(shí)現(xiàn)上鎖的核心代碼如下所示:

jedis.select(dbIndex);
String key = KEY_PRE + key;
String value = fetchLockValue();
if(jedis.exists(key)){
  jedis.set(key,value);
  jedis.expire(key,lockExpirseTime);
  return value;
}

表面上看這段代碼好像沒有什么問題,實(shí)際上并不能在分布式環(huán)境中正確的實(shí)現(xiàn)加鎖的操作誊涯。要能夠正確的實(shí)現(xiàn)加鎖操作挡毅,“判斷 key 是否存在”“保存 key-value”暴构、“設(shè)置 key 的過期時(shí)間”這三步操作必須是原子操作跪呈。如果不是原子操作,那么可能會(huì)出現(xiàn)下面兩種情況:

  1. “判斷 key 是否存在”得出 key 不存在的結(jié)果步驟后取逾,“保存 key-value”步驟前耗绿,另一個(gè)客戶端執(zhí)行同樣的邏輯,并且執(zhí)行到了“判斷 key 是否存在”步驟砾隅,同樣得出了 key 不存在的結(jié)果误阻。這樣回導(dǎo)致多個(gè)客戶端獲得了同一把鎖;
  2. 在客戶端執(zhí)行完“保存 key-value” 步驟后,需要設(shè)置一個(gè) key 的過期時(shí)間究反,防止客戶端因?yàn)榇a質(zhì)量未解鎖寻定,在或者進(jìn)程崩潰未解鎖導(dǎo)致的死鎖情況淡诗。在“保存 key-value”步驟之后议泵,“設(shè)置 key 的過期時(shí)間”步驟之前,可能進(jìn)程崩潰酝蜒,導(dǎo)致“設(shè)置 key 的過期時(shí)間”步驟失斬酝!向胡;

redis 在2.6.12版本之后,對(duì) set 命令進(jìn)行了擴(kuò)充沫浆,能夠規(guī)避上面的兩個(gè)問題捷枯。新版的 redis set 命令的參數(shù)如下

SET key value [EX seconds] [PX milliseconds] [NX|XX]

新版的 set 命令增加了 EX 、 PX 专执、 NX|XX 參數(shù)選項(xiàng)淮捆。他們的含義如下

EX seconds – 設(shè)置鍵 key 的過期時(shí)間,單位時(shí)秒
PX milliseconds – 設(shè)置鍵 key 的過期時(shí)間本股,單位時(shí)毫秒
NX – 只有鍵 key 不存在的時(shí)候才會(huì)設(shè)置 key 的值
XX – 只有鍵 key 存在的時(shí)候才會(huì)設(shè)置 key 的值

這樣攀痊,原來(lái)的三步操作就可以在一個(gè) set 的原子操作里面來(lái)完成,規(guī)避了上面我們提到的兩個(gè)問題拄显。新版的 redis 加鎖核心代碼修改如下所示:

jedis = redisConnection.getJedis();
jedis.select(dbIndex);
String key = KEY_PRE + key;
String value = fetchLockValue();
if ("OK".equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
    return value;
}

解鎖

解鎖的基本流程如下:

redis 分布式鎖解鎖實(shí)現(xiàn)邏輯

根據(jù)這個(gè)邏輯苟径,在 Java 中解鎖的核心代碼如下所示:

jedis.select(dbIndex);
String key = KEY_PRE + key;
if(jedis.exists(key) && value.equals(jedis.get(key))){
    jedis.del(key);
    return true;
}
return false;

和加鎖的時(shí)候一樣,key 是否存在躬审、判斷是否自己持有鎖棘街、刪除 key-value 這三步操作需要是原子操作,否則當(dāng)一個(gè)客戶端執(zhí)行完“判斷是否自己持有鎖”步驟后承边,得出自己持有鎖的結(jié)論遭殉,此時(shí)鎖的過期時(shí)間到了,自動(dòng)被 redis 釋放了博助,同時(shí)另一個(gè)客戶端又基于這個(gè) key 加鎖成功险污,如果第一個(gè)客戶端還繼續(xù)執(zhí)行刪除 key-value的操作,就將不屬于自己的鎖給釋放了富岳。這顯然是不運(yùn)行的蛔糯。在這里我們利用 redis 執(zhí)行 Lua 腳本的能力來(lái)解決原子操作的問題。修改后的解鎖核心代碼如下所示:

jedis.select(dbIndex);
String key = KEY_PRE + key;
String command = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
if (1L.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {
    return true;
}

另外窖式,判斷是否自己持有鎖的機(jī)制是用加鎖的時(shí)候的 key-value 來(lái)判斷當(dāng)前的 key 的值是否等于自己持有鎖時(shí)獲得的值蚁飒。所以加鎖的時(shí)候的 value 必須是一個(gè)全局唯一的字符串。

完整的代碼如下所示

package com.x9710.common.redis.impl;

import com.x9710.common.redis.LockService;
import com.x9710.common.redis.RedisConnection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import redis.clients.jedis.Jedis;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.UUID;

/**
 * 分布式鎖 redis 實(shí)現(xiàn)
 *
 * @author 楊高超
 * @since 2017-12-14
 */
public class LockServiceRedisImpl implements LockService {

private static Log log = LogFactory.getLog(LockServiceRedisImpl.class);

private static String SET_SUCCESS = "OK";

private static String KEY_PRE = "REDIS_LOCK_";

private DateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");

private RedisConnection redisConnection;

private Integer dbIndex;

private Integer lockExpirseTime;

private Integer tryExpirseTime;

public void setRedisConnection(RedisConnection redisConnection) {
    this.redisConnection = redisConnection;
}

public void setDbIndex(Integer dbIndex) {
    this.dbIndex = dbIndex;
}

public void setLockExpirseTime(Integer lockExpirseTime) {
    this.lockExpirseTime = lockExpirseTime;
}

public void setTryExpirseTime(Integer tryExpirseTime) {
    this.tryExpirseTime = tryExpirseTime;
}

public String lock(String key) {
    Jedis jedis = null;
    try {
        jedis = redisConnection.getJedis();
        jedis.select(dbIndex);
        key = KEY_PRE + key;
        String value = fetchLockValue();
        if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
            log.debug("Reids Lock key : " + key + ",value : " + value);
            return value;
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }
    return null;
}

public String tryLock(String key) {

    Jedis jedis = null;
    try {
        jedis = redisConnection.getJedis();
        jedis.select(dbIndex);
        key = KEY_PRE + key;
        String value = fetchLockValue();
        Long firstTryTime = new Date().getTime();
        do {
            if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
                log.debug("Reids Lock key : " + key + ",value : " + value);
                return value;
            }
            log.info("Redis lock failure,waiting try next");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } while ((new Date().getTime() - tryExpirseTime * 1000) < firstTryTime);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }
    return null;
}

public boolean unLock(String key, String value) {
    Long RELEASE_SUCCESS = 1L;
    Jedis jedis = null;
    try {
        jedis = redisConnection.getJedis();
        jedis.select(dbIndex);
        key = KEY_PRE + key;
        String command = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        if (RELEASE_SUCCESS.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {
            return true;
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }
    return false;
}

/**
 * 生成加鎖的唯一字符串
 *
 * @return 唯一字符串
 */
private String fetchLockValue() {
    return UUID.randomUUID().toString() + "_" + df.format(new Date());
}
}

測(cè)試代碼

package com.x9710.common.redis.test;

import com.x9710.common.redis.RedisConnection;
import com.x9710.common.redis.impl.LockServiceRedisImpl;

public class RedisLockTest {

public static void main(String[] args) {
    for (int i = 0; i < 9; i++) {
        new Thread(new Runnable() {
            public void run() {
                RedisConnection redisConnection = RedisConnectionUtil.create();
                LockServiceRedisImpl lockServiceRedis = new LockServiceRedisImpl();
                lockServiceRedis.setRedisConnection(redisConnection);
                lockServiceRedis.setDbIndex(15);
                lockServiceRedis.setLockExpirseTime(20);
                String key = "20171228";
                String value = lockServiceRedis.lock(key);
                try {
                    if (value != null) {
                        System.out.println(Thread.currentThread().getName() + " lock key = " + key + " success! ");
                        Thread.sleep(25 * 1000);
                    }else{
                        System.out.println(Thread.currentThread().getName() + " lock key = " + key + " failure! ");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (value == null) {
                        value = "";
                    }
                    System.out.println(Thread.currentThread().getName() + " unlock key = " + key + " " + lockServiceRedis.unLock(key, value));

                }
            }
        }).start();
    }
}
}

測(cè)試結(jié)果

Thread-1 lock key = 20171228 failure! 
Thread-2 lock key = 20171228 failure! 
Thread-4 lock key = 20171228 failure! 
Thread-8 lock key = 20171228 failure! 
Thread-7 lock key = 20171228 failure! 
Thread-3 lock key = 20171228 failure! 
Thread-5 lock key = 20171228 failure! 
Thread-0 lock key = 20171228 failure! 
Thread-6 lock key = 20171228 success! 
Thread-1 unlock key = 20171228 false
Thread-2 unlock key = 20171228 false
Thread-4 unlock key = 20171228 false
Thread-8 unlock key = 20171228 false
Thread-3 unlock key = 20171228 false
Thread-5 unlock key = 20171228 false
Thread-0 unlock key = 20171228 false
Thread-7 unlock key = 20171228 false
Thread-6 unlock key = 20171228 true

從測(cè)試結(jié)果來(lái)看可以看到脖镀,9個(gè)線程同時(shí)給一個(gè) key 加鎖飒箭,只有一個(gè)能夠成功獲取到鎖狼电,其余的客戶端都無(wú)法獲取到鎖。

后記

這個(gè)代碼里面還實(shí)現(xiàn)了一個(gè) tryLock 的接口弦蹂。這個(gè)主要是客戶端無(wú)法獲取到鎖的時(shí)候會(huì)在一小段時(shí)間內(nèi)反復(fù)嘗試是否能夠獲取到鎖肩碟。

這個(gè)程序?qū)嵲谇懊娴奈恼?a href="http://www.reibang.com/p/e030632b19d3" target="_blank">《在 Java 中使用 redis》的基礎(chǔ)上添加新的實(shí)現(xiàn)類的方式完成的。代碼同步發(fā)布在 GitHub 倉(cāng)庫(kù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凸椿,一起剝皮案震驚了整個(gè)濱河市削祈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌脑漫,老刑警劉巖髓抑,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異优幸,居然都是意外死亡吨拍,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門网杆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)羹饰,“玉大人,你說我怎么就攤上這事碳却《又龋” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵昼浦,是天一觀的道長(zhǎng)馍资。 經(jīng)常有香客問我,道長(zhǎng)关噪,這世上最難降的妖魔是什么鸟蟹? 我笑而不...
    開封第一講書人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮使兔,結(jié)果婚禮上戏锹,老公的妹妹穿的比我還像新娘。我一直安慰自己火诸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開白布荠察。 她就那樣靜靜地躺著置蜀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪悉盆。 梳的紋絲不亂的頭發(fā)上盯荤,一...
    開封第一講書人閱讀 51,562評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音焕盟,去河邊找鬼秋秤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的灼卢。 我是一名探鬼主播绍哎,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鞋真!你這毒婦竟也來(lái)了崇堰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤涩咖,失蹤者是張志新(化名)和其女友劉穎海诲,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體檩互,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡特幔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了闸昨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蚯斯。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖零院,靈堂內(nèi)的尸體忽然破棺而出溉跃,到底是詐尸還是另有隱情,我是刑警寧澤告抄,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布撰茎,位于F島的核電站,受9級(jí)特大地震影響打洼,放射性物質(zhì)發(fā)生泄漏龄糊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一募疮、第九天 我趴在偏房一處隱蔽的房頂上張望炫惩。 院中可真熱鬧,春花似錦阿浓、人聲如沸他嚷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)筋蓖。三九已至,卻和暖如春退敦,著一層夾襖步出監(jiān)牢的瞬間粘咖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工侈百, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瓮下,地道東北人翰铡。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像讽坏,于是被迫代替她去往敵國(guó)和親锭魔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

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