一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

不用找了,基于 Redis 的分布式鎖實(shí)戰(zhàn)來了!

 但丁叔叔 2019-09-26

Java技術(shù)棧

www.javastack.cn

優(yōu)秀的Java技術(shù)公眾號(hào)

作者:菜蚜

my.oschina.net/wnjustdoit/blog/1606215

前言:在分布式環(huán)境中,我們經(jīng)常使用鎖來進(jìn)行并發(fā)控制,鎖可分為樂觀鎖和悲觀鎖,
基于數(shù)據(jù)庫版本戳的實(shí)現(xiàn)是樂觀鎖,基于redis或zookeeper的實(shí)現(xiàn)可認(rèn)為是悲觀鎖了。樂觀鎖和悲觀鎖最根本的區(qū)別在于線程之間是否相互阻塞。

那么,本文主要來討論基于redis的分布式鎖算法問題。

從2.6.12版本開始,redis為SET命令增加了一系列選項(xiàng)(set [key] NX/XX EX/PX [expiration]):
  • EX seconds – 設(shè)置鍵key的過期時(shí)間,單位時(shí)秒
  • PX milliseconds – 設(shè)置鍵key的過期時(shí)間,單位時(shí)毫秒
  • NX – 只有鍵key不存在的時(shí)候才會(huì)設(shè)置key的值
  • XX – 只有鍵key存在的時(shí)候才會(huì)設(shè)置key的值
原文地址:https:///commands/set
中文地址:http:///commands/set.html
注意: 由于SET命令加上選項(xiàng)已經(jīng)可以完全取代SETNX, SETEX, PSETEX的功能,所以在將來的版本中,redis可能會(huì)不推薦使用并且最終拋棄這幾個(gè)命令。
這里簡單提一下,在舊版本的redis中(指2.6.12版本之前),使用redis實(shí)現(xiàn)分布式鎖一般需要setNX、expire、getSet、del等命令。而且會(huì)發(fā)現(xiàn)這種實(shí)現(xiàn)有很多邏輯判斷的原子操作以及本地時(shí)間等并沒有控制好。
而在舊版本的redis中,redis的超時(shí)時(shí)間很難控制,用戶迫切需要把setNX和expiration結(jié)合為一體的命令,把他們作為一個(gè)原子操作,這樣新版本的多選項(xiàng)set命令誕生了。然而這并沒有完全解決復(fù)雜的超時(shí)控制帶來的問題。
接下來,我們的一切討論都基于新版redis。
在這里,我先提出幾個(gè)在實(shí)現(xiàn)redis分布式鎖中需要考慮的關(guān)鍵問題

1、死鎖問題;

1.1、為了防止死鎖,redis至少需要設(shè)置一個(gè)超時(shí)時(shí)間;
1.2、由1.1引申出來,當(dāng)鎖自動(dòng)釋放了,但是程序并沒有執(zhí)行完畢,這時(shí)候其他線程又獲取到鎖執(zhí)行同樣的程序,可能會(huì)造成并發(fā)問題,這個(gè)問題我們需要考慮一下是否歸屬于分布式鎖帶來問題的范疇。

2、鎖釋放問題,這里會(huì)有兩個(gè)問題;

2.1、每個(gè)獲取redis鎖的線程應(yīng)該釋放自己獲取到的鎖,而不是其他線程的,所以我們需要在每個(gè)線程獲取鎖的時(shí)候給鎖做上不同的標(biāo)記以示區(qū)分;
2.2、由2.1帶來的問題是線程在釋放鎖的時(shí)候需要判斷當(dāng)前鎖是否屬于自己,如果屬于自己才釋放,這里涉及到邏輯判斷語句,至少是兩個(gè)操作在進(jìn)行,那么我們需要考慮這兩個(gè)操作要在一個(gè)原子內(nèi)執(zhí)行,否者在兩個(gè)行為之間可能會(huì)有其他線程插入執(zhí)行,導(dǎo)致程序紊亂。

3、更可靠的鎖;

單實(shí)例的redis(這里指只有一個(gè)master節(jié)點(diǎn))往往是不可靠的,雖然實(shí)現(xiàn)起來相對簡單一些,但是會(huì)面臨著宕機(jī)等不可用的場景,即使在主從復(fù)制的時(shí)候也顯得并不可靠(因?yàn)閞edis的主從復(fù)制往往是異步的)。
關(guān)于Martin Kleppmann的Redlock的分析
原文地址:https:///topics/distlock
中文地址:http:///topics/distlock.html
文章分析得出,這種算法只需具備3個(gè)特性就可以實(shí)現(xiàn)一個(gè)最低保障的分布式鎖。
  • 安全屬性(Safety property): 獨(dú)享(相互排斥)。在任意一個(gè)時(shí)刻,只有一個(gè)客戶端持有鎖。
  • 活性A(Liveness property A): 無死鎖。即便持有鎖的客戶端崩潰(crashed)或者網(wǎng)絡(luò)被分裂(gets partitioned),鎖仍然可以被獲取。
  • 活性B(Liveness property B): 容錯(cuò)。只要大部分Redis節(jié)點(diǎn)都活著,客戶端就可以獲取和釋放鎖.
我們來分析一下:
第一點(diǎn)安全屬性意味著悲觀鎖(互斥鎖)是我們做redis分布式鎖的前提,否者將可能造成并發(fā);
第二點(diǎn)表明為了避免死鎖,我們需要設(shè)置鎖超時(shí)時(shí)間,保證在一定的時(shí)間過后,鎖可以重新被利用;
第三點(diǎn)是說對于客戶端來說,獲取鎖和手動(dòng)釋放鎖可以有更高的可靠性。
更進(jìn)一步分析,結(jié)合上文提到的關(guān)鍵問題,這里可以引申出另外的兩個(gè)問題:
  • 怎么才能合理判斷程序真正處理的有效時(shí)間范圍?(這里有個(gè)時(shí)間偏移的問題)
  • redis Master節(jié)點(diǎn)宕機(jī)后恢復(fù)(可能還沒有持久化到磁盤)、主從節(jié)點(diǎn)切換,(N/2)+1這里的N應(yīng)該怎么動(dòng)態(tài)計(jì)算更合理?
接下來再看,redis之父antirez對Redlock的評價(jià)
原文地址:http:///news/101
文中主要提到了網(wǎng)絡(luò)延遲和本地時(shí)鐘的修改(不管是時(shí)間服務(wù)器或人為修改)對這種算法可能造成的影響。

最后,來點(diǎn)實(shí)踐吧

I、傳統(tǒng)的單實(shí)例redis分布式鎖實(shí)現(xiàn)(關(guān)鍵步驟)
獲取鎖(含自動(dòng)釋放鎖):
SET resource_name my_random_value NX PX 30000
 手動(dòng)刪除鎖(Lua腳本):

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

II、分布式環(huán)境的redis(多master節(jié)點(diǎn))的分布式鎖實(shí)現(xiàn)

為了保證在盡可能短的時(shí)間內(nèi)獲取到(N/2)+1個(gè)節(jié)點(diǎn)的鎖,可以并行去獲取各個(gè)節(jié)點(diǎn)的鎖(當(dāng)然,并行可能需要消耗更多的資源,因?yàn)榇兄恍枰猚ount到足夠數(shù)量的鎖就可以停止獲取了);

另外,怎么動(dòng)態(tài)實(shí)時(shí)統(tǒng)一獲取redis master nodes需要更進(jìn)一步去思考了。

QA,補(bǔ)充一下說明(以下為我與朋友溝通的情況,以說明文中大家可能不夠明白的地方):

1、在關(guān)鍵問題2.1中,刪除就刪除了,會(huì)造成什么問題?

線程A超時(shí),準(zhǔn)備刪除鎖;但此時(shí)的鎖屬于線程B;線程B還沒執(zhí)行完,線程A把鎖刪除了,這時(shí)線程C獲取到鎖,同時(shí)執(zhí)行程序;所以不能亂刪。

2、在關(guān)鍵問題2.2中,只要在key生成時(shí),跟線程相關(guān)就不用考慮這個(gè)問題了嗎?

不同的線程執(zhí)行程序,線程之間肯雖然有差異呀,然后在redis鎖的value設(shè)置有線程信息,比如線程id或線程名稱,是分布式環(huán)境的話加個(gè)機(jī)器id前綴咯(類似于twitter的snowflake算法!),但是在del命令只會(huì)涉及到key,不會(huì)再次檢查value,所以還是需要lua腳本控制if(condition){xxx}的原子性。

3、那要不要考慮鎖的重入性?

不需要重入;try…finally 沒得重入的場景;對于單個(gè)線程來說,執(zhí)行是串行的,獲取鎖之后必定會(huì)釋放,因?yàn)閒inally的代碼必定會(huì)執(zhí)行?。ㄖ灰M(jìn)入了try塊,finally必定會(huì)執(zhí)行)。

4、為什么兩個(gè)線程都會(huì)去刪除鎖?(貌似重復(fù)的問題。不管怎樣,還是耐心解答吧)

每個(gè)線程只能管理自己的鎖,不能管理別人線程的鎖啊。這里可以聯(lián)想一下ThreadLocal。

5、如果加鎖的線程掛了怎么辦?只能等待自動(dòng)超時(shí)?

看你怎么寫程序的了,一種是問題3的回答;另外,那就自動(dòng)超時(shí)咯。這種情況也適用于網(wǎng)絡(luò)over了。

6、時(shí)間太長,程序異常就會(huì)蛋疼,時(shí)間太短,就會(huì)出現(xiàn)程序還沒有處理完就超時(shí)了,這豈不是很尷尬?

是呀,所以需要更好的衡量這個(gè)超時(shí)時(shí)間的設(shè)置。

實(shí)踐部分主要代碼:

RedisLock工具類:

package com.caiya.cms.web.component;

import com.caiya.cache.CacheException;
import com.caiya.cache.redis.JedisCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * redis實(shí)現(xiàn)分布式鎖
 * 可實(shí)現(xiàn)特性:
 * 1、使多線程無序排隊(duì)獲取和釋放鎖;
 * 2、丟棄未成功獲得鎖的線程處理;
 * 3、只釋放線程本身加持的鎖;
 * 4、避免死鎖
 *
 * @author wangnan
 * @since 1.0
 */
public final class RedisLock {

    private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);

    /**
     * 嘗試加鎖(僅一次)
     *
     * @param lockKey       鎖key
     * @param lockValue     鎖value
     * @param expireSeconds 鎖超時(shí)時(shí)間(秒)
     * @return 是否加鎖成功
     * @throws CacheException
     */
    public static boolean tryLock(String lockKey, String lockValue, long expireSeconds) throws CacheException {
        JedisCache jedisCache = JedisCacheFactory.getInstance().getJedisCache();
        try {
            String response = jedisCache.set(lockKey, lockValue, 'nx', 'ex', expireSeconds);
            return Objects.equals(response, 'OK');
        } finally {
            jedisCache.close();
        }
    }

    /**
     * 加鎖(指定最大嘗試次數(shù)范圍內(nèi))
     *
     * @param lockKey       鎖key
     * @param lockValue     鎖value
     * @param expireSeconds 鎖超時(shí)時(shí)間(秒)
     * @param tryTimes      最大嘗試次數(shù)
     * @param sleepMillis   每兩次嘗試之間休眠時(shí)間(毫秒)
     * @return 是否加鎖成功
     * @throws CacheException
     */
    public static boolean lock(String lockKey, String lockValue, long expireSeconds, int tryTimes, long sleepMillis) throws CacheException {
        boolean result;
        int count = 0;
        do {
            count++;
            result = tryLock(lockKey, lockValue, expireSeconds);
            try {
                TimeUnit.MILLISECONDS.sleep(sleepMillis);
            } catch (InterruptedException e) {
                logger.error(e.getMessage(), e);
            }
        } while (!result && count <= tryTimes);
        return result;
    }

    /**
     * 釋放鎖
     *
     * @param lockKey   鎖key
     * @param lockValue 鎖value
     */
    public static void unlock(String lockKey, String lockValue) {
        JedisCache jedisCache = JedisCacheFactory.getInstance().getJedisCache();
        try {
            String luaScript = 'if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end';
            Object result = jedisCache.eval(luaScript, 1, lockKey, lockValue);
//            Objects.equals(result, 1L);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        } finally {
            jedisCache.close();
        }
//        return false;
    }


    private RedisLock() {
    }

}

使用工具類的代碼片段1:

        ...
        String lockKey = Constant.DEFAULT_CACHE_NAME + ':addItemApply:' + applyPriceDTO.getItemId() + '_' + applyPriceDTO.getSupplierId();// 跟業(yè)務(wù)相關(guān)的唯一拼接鍵
        String lockValue = Constant.DEFAULT_CACHE_NAME + ':' + System.getProperty('JvmId') + ':' + Thread.currentThread().getName() + ':' + System.currentTimeMillis();// 生成集群環(huán)境中的唯一值
        boolean locked = RedisLock.tryLock(lockKey, lockValue, 100);// 只嘗試一次,在本次處理過程中直接拒絕其他線程的請求
        if (!locked) {
            throw new IllegalAccessException('您的操作太頻繁了,休息一下再來吧~');
        }
        try {
            // 開始處理核心業(yè)務(wù)邏輯
            Item item = itemService.queryItemByItemId(applyPriceDTO.getItemId());
            ...
            ...
        } finally {
            RedisLock.unlock(lockKey, lockValue);// 在finally塊中釋放鎖
        }

使用工具類的代碼片段2:

        ...
        String lockKey = Constant.DEFAULT_CACHE_NAME + ':addItemApply:' + applyPriceDTO.getItemId() + '_' + applyPriceDTO.getSupplierId();
        String lockValue = Constant.DEFAULT_CACHE_NAME + ':機(jī)器編號(hào):' + Thread.currentThread().getName() + ':' + System.currentTimeMillis();
        boolean locked = RedisLock.lock(lockKey, lockValue, 100, 20, 100);// 非公平鎖,無序競爭(這里需要合理根據(jù)業(yè)務(wù)處理情況設(shè)置最大嘗試次數(shù)和每次休眠時(shí)間)
        if (!locked) {
            throw new IllegalAccessException('系統(tǒng)太忙,本次操作失敗');// 一般來說,不會(huì)走到這一步;如果真的有這種情況,并且在合理設(shè)置鎖嘗試次數(shù)和等待響應(yīng)時(shí)間之后仍然處理不過來,可能需要考慮優(yōu)化程序響應(yīng)時(shí)間或者用消息隊(duì)列排隊(duì)執(zhí)行了
        }

        try {
            // 開始處理核心業(yè)務(wù)邏輯
            Item item = itemService.queryItemByItemId(applyPriceDTO.getItemId());
            ...
            ...
        } finally {
            RedisLock.unlock(lockKey, lockValue);
        }
        ...

附加:

基于redis的分布式鎖實(shí)現(xiàn)客戶端Redisson:

https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers

基于zookeeper的分布式鎖實(shí)現(xiàn):

http://curator./curator-recipes/shared-reentrant-lock.html

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    亚洲中文字幕视频在线观看| 中文字幕禁断介一区二区| 日韩高清毛片免费观看| 国产免费一区二区三区av大片| 亚洲高清亚洲欧美一区二区| 国产精品一区二区三区日韩av| 丰满少妇被猛烈撞击在线视频 | 欧美一区二区三区喷汁尤物| 国产内射一级一片内射高清| 色婷婷在线视频免费播放| 国产av熟女一区二区三区蜜桃| 亚洲精品蜜桃在线观看| 热久久这里只有精品视频| 激情五月天深爱丁香婷婷| 国产av一区二区三区麻豆| 98精品永久免费视频| 久久热这里只有精品视频| 我想看亚洲一级黄色录像| 国产又色又爽又黄又免费| 精品国模一区二区三区欧美| 91久久精品中文内射| 日韩人妻少妇一区二区| 欧美一级特黄大片做受大屁股| 99久久精品久久免费| 午夜福利92在线观看| 国产午夜精品美女露脸视频| 久久青青草原中文字幕| 国产乱人伦精品一区二区三区四区 | 久热在线视频这里只有精品| 欧美成人免费视频午夜色| 老熟妇2久久国内精品| 久草视频这里只是精品| 日本乱论一区二区三区| 91精品国自产拍老熟女露脸| 丰满少妇被粗大猛烈进出视频| 午夜福利国产精品不卡| 亚洲精品偷拍一区二区三区 | 亚洲国产av一二三区| 日本欧美在线一区二区三区| 加勒比日本欧美在线观看| 麻豆精品在线一区二区三区|