(五)Redis分布式鎖的問題和實現(xiàn)

關(guān)于分布式的鎖刹泄,大概有幾個熱點問題

關(guān)鍵點一:原子命令加鎖震糖。

對于 Redis 的加鎖操作先set key,再設(shè)置 key 的過期時間屏鳍,這樣的話不是原子性操作。不是原子操作會帶來什么問題局服,就不用我說了吧钓瞭?

而在 2.6.12 版本后,可以通過向 Redis 發(fā)送下面的命令淫奔,實現(xiàn)原子性的加鎖操作:

SET key random_value NX PX 30000

我們常用的redis客戶端山涡,Jedis、Lettuce都實現(xiàn)了這一命令方法唆迁,比如jedis的setnx()鸭丛,Lettuce的setIfAbsent()。

關(guān)鍵點二:設(shè)置值的時候唐责,放的是random_value鳞溉。而不是你隨便扔個“OK”進(jìn)去。

先解釋一下上面的命令中的幾個參數(shù)的含義:

random_value:是由客戶端生成的一個隨機(jī)字符串妒蔚,它要保證在足夠長的一段時間內(nèi)在所有客戶端的所有獲取鎖的請求中都是唯一的穿挨。 比如我們使用UUID。

NX:表示只有當(dāng)要設(shè)置的 key 值不存在的時候才能 set 成功肴盏。這保證了只有第一個請求的客戶端才能獲得鎖科盛,而其它客戶端在鎖被釋放之前都無法獲得鎖。

PX 30000:表示這個鎖有一個 30 秒的自動過期時間菜皂。當(dāng)然贞绵,這里 30 秒只是一個例子,客戶端可以選擇合適的過期時間恍飘。

那么為什么要使用 PX 30000 去設(shè)置一個超時時間榨崩?是怕進(jìn)程 A 不講道理啊,鎖沒等釋放呢章母,萬一崩了母蛛,直接原地把鎖帶走了,導(dǎo)致系統(tǒng)中誰也拿不到鎖乳怎。

就算這樣彩郊,還是不能保證萬無一失。如果進(jìn)程 A 又不講道理蚪缀,操作鎖內(nèi)資源超過筆者設(shè)置的超時時間秫逝,那么就會導(dǎo)致其他進(jìn)程拿到鎖,等進(jìn)程 A 回來了询枚,回手就是把其他進(jìn)程的鎖刪了违帆。這就引出了下一個關(guān)鍵點。

再解釋一下為什么 value 需要設(shè)置為一個隨機(jī)字符串金蜀。這也是第三個關(guān)鍵點痹仙。

關(guān)鍵點三:value 的值設(shè)置為隨機(jī)數(shù)主要是為了更安全的釋放鎖,釋放鎖的時候需要檢查 key 是否存在末融,且 key 對應(yīng)的值是否和我指定的值一樣撩笆,是一樣的才能釋放鎖。所以可以看到這里有獲取抒线、判斷班巩、刪除三個操作。

釋放鎖偽代碼

String uuid = xxxx;
// 偽代碼嘶炭,具體實現(xiàn)看項目中用的連接工具
// 有的提供的方法名為set 有的叫setIfAbsent
set Test uuid NX PX 3000
try{
// biz handle....
} finally {
    // unlock
    if(uuid.equals(redisTool.get('key')){
        redisTool.del('key');
    }
}

這回看起來是不是穩(wěn)了抱慌?相反,這回的問題更明顯了眨猎,在 Finally 代碼塊中抑进,Get 和 Del 并非原子操作,還是有進(jìn)程安全問題睡陪。

進(jìn)程安全問題會在什么場景下出現(xiàn)寺渗?我就先不回答了匿情,感興趣的可以在留言中交流下

為了保障原子性,我們需要用 lua 腳本信殊。
那么刪除鎖的正確姿勢之一炬称,就是可以使用 Lua 腳本,通過 Redis 的 eval/evalsha 命令來涡拘。

下面簡單看下一個釋放鎖有問題的代玲躯,相信也是很多人使用最多的方法

/**
     * 獲取鎖
     * @param key
     * @param expireSecond
     * @return
     */
    public boolean lock(String key, int expireSecond) {

        Jedis Jedis = null;
        try {
            Jedis = jedisPool.getResource();
            Long result = Jedis.setnx(key, "1");
            if(result == 1){
                Jedis.expire(key, expireSecond);
                return true;
            }
            return false;
        } catch (Exception ex) {
            logger.error("lock-setnx error:", ex);
            returnBrokenResource(Jedis);
            throw new TraweServiceException("獲取鎖出現(xiàn)異常:" + ex.getMessage(), ex);
        } finally {
            returnResource(Jedis);
        }
    }

    /**
     * 獲取鎖,如果沒有獲取到會再去嘗試幾次
     *
     * @param key 鎖鍵值
     * @param expireSecond 鎖過期時間
     * @param tryCount 嘗試次數(shù)
     * @return 是否獲得鎖
     */
    @SuppressWarnings("static-access")
    public boolean tryLock(String key, int expireSecond, int tryCount) {
        if (lock(key, expireSecond)) {
            return true;
        }

        for (int i=0; i<tryCount; i++) {
            int sleepMills = RandomUtils.nextInt(20, 200);
            try {
                Thread.currentThread().sleep(sleepMills);
            } catch (InterruptedException e) {
                logger.error(e, e);
            }

            if (lock(key, expireSecond)) {
                return true;
            }
        }

        return false;
    }

    /**
     * 釋放鎖
     * @param key
     */
    public void unlock(String key) {
        del(key);
    }

實現(xiàn)Redis鎖

網(wǎng)上大佬用jedis寫的鎖簡單修改下使用lettuce實現(xiàn)

@Component
public class RedisDistributedLock {
    
    @Autowired
    @Resource(name="redisTemplateMaster")
    private RedisTemplate<Object,Object> redisTemplate;
    
    private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);
    
    private ThreadLocal<String> lockFlag = new ThreadLocal<String>();
    
    public static final String UNLOCK_LUA;

    static {
        StringBuilder sb = new StringBuilder();
        sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
        sb.append("then ");
        sb.append("    return redis.call(\"del\",KEYS[1]) ");
        sb.append("else ");
        sb.append("    return 0 ");
        sb.append("end ");
        UNLOCK_LUA = sb.toString();
    }


    public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
        boolean result = setRedis(key, expire);
        // 如果獲取鎖失敗鳄乏,按照傳入的重試次數(shù)進(jìn)行重試
        while((!result) && retryTimes-- > 0){
            try {
                logger.debug("lock failed, retrying..." + retryTimes);
                Thread.sleep(sleepMillis);
            } catch (InterruptedException e) {
                return false;
            }
            result = setRedis(key , expire);
        }
        return result;
    }
    
    private boolean setRedis(final String key, final long expire ) {
        try{
            
            RedisCallback<Boolean> callback = (connection) -> {
                String uuid = UUID.randomUUID().toString();
                lockFlag.set(uuid);
                return connection.set(key.getBytes(Charset.forName("UTF-8")), uuid.getBytes(Charset.forName("UTF-8")), Expiration.milliseconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT);
            };
            return (Boolean)redisTemplate.execute(callback);
        } catch (Exception e) {
            logger.error("redis lock error.", e);
        }
        return false;
    }
    
    
    public boolean releaseLock(String key) {
        // 釋放鎖的時候跷车,有可能因為持鎖之后方法執(zhí)行時間大于鎖的有效期,此時有可能已經(jīng)被另外一個線程持有鎖橱野,所以不能直接刪除
        try {
             RedisCallback<Boolean> callback = (connection) -> {
                String value = lockFlag.get();
                return connection.eval(UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN ,1, key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8")));
            };
             return (Boolean)redisTemplate.execute(callback);
        } catch (Exception e) {
            logger.error("release lock occured an exception", e);
        } finally {
            // 清除掉ThreadLocal中的數(shù)據(jù)朽缴,避免內(nèi)存溢出
            lockFlag.remove();
        }
        return false;
    }
    
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市水援,隨后出現(xiàn)的幾起案子不铆,更是在濱河造成了極大的恐慌,老刑警劉巖裹唆,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件誓斥,死亡現(xiàn)場離奇詭異,居然都是意外死亡许帐,警方通過查閱死者的電腦和手機(jī)劳坑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來成畦,“玉大人距芬,你說我怎么就攤上這事⊙剩” “怎么了框仔?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拄养。 經(jīng)常有香客問我离斩,道長,這世上最難降的妖魔是什么瘪匿? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任跛梗,我火速辦了婚禮,結(jié)果婚禮上棋弥,老公的妹妹穿的比我還像新娘核偿。我一直安慰自己,他們只是感情好顽染,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布漾岳。 她就那樣靜靜地躺著轰绵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尼荆。 梳的紋絲不亂的頭發(fā)上左腔,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機(jī)與錄音耀找,去河邊找鬼翔悠。 笑死业崖,一個胖子當(dāng)著我的面吹牛野芒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播双炕,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼狞悲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了妇斤?” 一聲冷哼從身側(cè)響起摇锋,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎站超,沒想到半個月后荸恕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡死相,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年融求,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片算撮。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡生宛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出肮柜,到底是詐尸還是另有隱情陷舅,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布审洞,位于F島的核電站莱睁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏芒澜。R本人自食惡果不足惜缩赛,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望撰糠。 院中可真熱鬧酥馍,春花似錦、人聲如沸阅酪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至砚尽,卻和暖如春施无,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背必孤。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工猾骡, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人敷搪。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓兴想,卻偏偏與公主長得像,于是被迫代替她去往敵國和親赡勘。 傳聞我的和親對象是個殘疾皇子嫂便,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349