spring boot實(shí)戰(zhàn)之分布式鎖

在部分情況下撒汉,要保證操作在整個(gè)集群內(nèi)是同步的溺职,以操作庫存為例,多個(gè)減操作需要同步赛不,常見的有兩種方式:

  1. 采用類CAS的方式惩嘉,先查詢庫存,然后使用update xxx set num=num-1 where id=:id and num=:num;這樣可保證庫在本次修改之前未被修改俄删;
  2. 使用分布式鎖宏怔,保證同時(shí)只有一個(gè)地方在修改庫存奏路。

這里向大家展示一個(gè)基于redis的分布式鎖畴椰。主要涉及三個(gè)類:

  1. DistributedLockUtil對(duì)外提供獲取分布式鎖的方法;
  2. DistributedLock 分布式鎖接口鸽粉,定義分布式鎖支持的方法斜脂,主要有acquire和release;
  3. JedisLock實(shí)現(xiàn)DistributedLock接口触机,是基于redis的分布鎖實(shí)現(xiàn) 帚戳;

需要使用StringRedisTemplate,如對(duì)spring boot整合redis不熟悉儡首,請(qǐng)參考spring boot項(xiàng)目實(shí)戰(zhàn):redis.

DistributedLock接口

public interface DistributedLock {

    /**
     * 獲取鎖
     * @author yangwenkui
     * @time 2016年5月6日 上午11:02:54
     * @return
     * @throws InterruptedException
     */
    public boolean acquire();
    
    /**
     * 釋放鎖
     * @author yangwenkui
     * @time 2016年5月6日 上午11:02:59
     */
    public void release();
    
}

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

public class JedisLock implements DistributedLock{

    private static Logger logger = LoggerFactory.getLogger(JedisLock.class);
    
    private static StringRedisTemplate redisTemplate;

    /**
     * 分布式鎖的鍵值
     */
    String lockKey; //鎖的鍵值
    int expireMsecs  = 10 * 1000; //鎖超時(shí)片任,防止線程在入鎖以后,無限的執(zhí)行等待
    int     timeoutMsecs = 10 * 1000; //鎖等待蔬胯,防止線程饑餓
    boolean locked = false; //是否已經(jīng)獲取鎖

    /**
     * 獲取指定鍵值的鎖
     * @param lockKey 鎖的鍵值
     */
    public JedisLock(String lockKey) {
        this.lockKey = lockKey;
    }

    /**
     * 獲取指定鍵值的鎖,同時(shí)設(shè)置獲取鎖超時(shí)時(shí)間
     * @param lockKey 鎖的鍵值
     * @param timeoutMsecs 獲取鎖超時(shí)時(shí)間
     */
    public JedisLock(String lockKey, int timeoutMsecs) {
        this.lockKey = lockKey;
        this.timeoutMsecs = timeoutMsecs;
    }

    /**
     * 獲取指定鍵值的鎖,同時(shí)設(shè)置獲取鎖超時(shí)時(shí)間和鎖過期時(shí)間
     * @param lockKey 鎖的鍵值
     * @param timeoutMsecs 獲取鎖超時(shí)時(shí)間
     * @param expireMsecs 鎖失效時(shí)間
     */
    public JedisLock(String lockKey, int timeoutMsecs, int expireMsecs) {
        this.lockKey = lockKey;
        this.timeoutMsecs = timeoutMsecs;
        this.expireMsecs = expireMsecs;
    }

    public String getLockKey() {
        return lockKey;
    }

    /**
     * 
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException
     *             in case of thread interruption
     */
    public synchronized boolean acquire() {
        int timeout = timeoutMsecs;
        if(redisTemplate == null){
            redisTemplate = SpringContextUtil.getBean(StringRedisTemplate.class);
        }
        try {
            while (timeout >= 0) {
                long expires = System.currentTimeMillis() + expireMsecs + 1;
                String expiresStr = String.valueOf(expires); //鎖到期時(shí)間

                if (redisTemplate.opsForValue().setIfAbsent(lockKey, expiresStr)) {
                    // lock acquired
                    locked = true;
                    return true;
                }

                String currentValueStr = redisTemplate.opsForValue().get(lockKey); //redis里的時(shí)間
                if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
                    //判斷是否為空对供,不為空的情況下,如果被其他線程設(shè)置了值,則第二個(gè)條件判斷是過不去的
                    // lock is expired

                    String oldValueStr = redisTemplate.opsForValue().getAndSet(lockKey, expiresStr);
                    //獲取上一個(gè)鎖到期時(shí)間产场,并設(shè)置現(xiàn)在的鎖到期時(shí)間鹅髓,
                    //只有一個(gè)線程才能獲取上一個(gè)線上的設(shè)置時(shí)間,因?yàn)閖edis.getSet是同步的
                    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                        //如過這個(gè)時(shí)候京景,多個(gè)線程恰好都到了這里窿冯,但是只有一個(gè)線程的設(shè)置值和當(dāng)前值相同,他才有權(quán)利獲取鎖
                        // lock acquired
                        locked = true;
                        return true;
                    }
                }
                timeout -= 100;
                Thread.sleep(100);
            }
        } catch (Exception e) {
            logger.error("release lock due to error",e);
        } 
        return false;
    }

    /**
     * 釋放鎖
     */
    public synchronized void release() {
            if(redisTemplate == null){
            redisTemplate = SpringContextUtil.getBean(StringRedisTemplate.class);
        }
        try {
            if (locked) {
                    String currentValueStr = redisTemplate.opsForValue().get(lockKey); //redis里的時(shí)間
                    //校驗(yàn)是否超過有效期确徙,如果不在有效期內(nèi)醒串,那說明當(dāng)前鎖已經(jīng)失效,不能進(jìn)行刪除鎖操作
                if (currentValueStr != null && Long.parseLong(currentValueStr) > System.currentTimeMillis()) {
                        redisTemplate.delete(lockKey);
                    locked = false;
                }
            }
        } catch (Exception e) {
            logger.error("release lock due to error",e);
        }
    }
}

DistributedLockUtil

public class DistributedLockUtil{

    /**
     * 獲取分布式鎖
     * 默認(rèn)獲取鎖10s超時(shí)鄙皇,鎖過期時(shí)間60s
     * @author yangwenkui
     * @time 2016年5月6日 下午1:30:46
     * @return
     */
    public static DistributedLock getDistributedLock(String lockKey){
        lockKey = assembleKey(lockKey);
        JedisLock lock = new JedisLock(lockKey);
        return lock;
    }
    
    /**
     * 正式環(huán)境厦凤、測(cè)試環(huán)境共用一個(gè)redis時(shí),避免key相同造成影響
     * @author yangwenkui
     * @param lockKey
     * @return
     */
    private static String assembleKey(String lockKey) {
        return String.format("lock_%s",lockKey  );
    }

    /**
     * 獲取分布式鎖
     * 默認(rèn)獲取鎖10s超時(shí)育苟,鎖過期時(shí)間60s
     * @author yangwenkui
     * @time 2016年5月6日 下午1:38:32
     * @param lockKey
     * @param timeoutMsecs 指定獲取鎖超時(shí)時(shí)間
     * @return
     */
    public static DistributedLock getDistributedLock(String lockKey,int timeoutMsecs){
        lockKey = assembleKey(lockKey);
        JedisLock lock = new JedisLock(lockKey,timeoutMsecs);
        return lock;
    }
    
    /**
     * 獲取分布式鎖
     * 默認(rèn)獲取鎖10s超時(shí)较鼓,鎖過期時(shí)間60s
     * @author yangwenkui
     * @time 2016年5月6日 下午1:40:04
     * @param lockKey 鎖的key
     * @param timeoutMsecs 指定獲取鎖超時(shí)時(shí)間
     * @param expireMsecs 指定鎖過期時(shí)間
     * @return
     */
    public static DistributedLock getDistributedLock(String lockKey,int timeoutMsecs,int expireMsecs){
        lockKey = assembleKey(lockKey);
        JedisLock lock = new JedisLock(lockKey,expireMsecs,timeoutMsecs);
        return lock;
    }
    
}

使用示例

DistributedLock lock = DistributedLockUtil.getDistributedLock(key);
try {
    if (lock.acquire()) {
        //獲取鎖成功業(yè)務(wù)代碼
    } else { // 獲取鎖失敗
        //獲取鎖失敗業(yè)務(wù)代碼
} finally {
    if (lock != null) {
        lock.release();
    }
}

實(shí)現(xiàn)原理簡(jiǎn)析

主要是依賴redis的setnx和getset命令對(duì)時(shí)間進(jìn)行操作,從而實(shí)現(xiàn)鎖的功能违柏。以下兩個(gè)文章對(duì)分布式鎖進(jìn)行了極其明細(xì)的分析博烂,會(huì)讓你對(duì)分布式鎖的認(rèn)識(shí)更加清晰。
基于Redis的分布式鎖到底安全嗎(上)漱竖?
基于Redis的分布式鎖到底安全嗎(下)禽篱?

注意事項(xiàng)

  1. 基于redis的分布式鎖依賴于系統(tǒng)時(shí)鐘,需要保證各個(gè)競(jìng)爭(zhēng)者的時(shí)鐘的一致性馍惹,否則會(huì)出現(xiàn)一個(gè)參與者獲得鎖躺率,而另一個(gè)參與者的時(shí)鐘判斷其已過期,導(dǎo)致分布式鎖失效万矾;
  2. 需要保證redis節(jié)點(diǎn)的高可用悼吱,建議使用哨兵機(jī)制;
  3. 在使用分布式鎖之前良狈,考慮是否可以通過樂觀鎖或無鎖解決并發(fā)同步問題后添,畢竟使用鎖的代價(jià)很是比較高昂的;

本人搭建好的spring boot web后端開發(fā)框架已上傳至GitHub薪丁,歡迎吐槽遇西!
https://github.com/q7322068/rest-base,已用于多個(gè)正式項(xiàng)目,當(dāng)前可能因?yàn)榘姹締栴}不是很完善严嗜,后續(xù)持續(xù)優(yōu)化粱檀,希望你能有所收獲!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末漫玄,一起剝皮案震驚了整個(gè)濱河市茄蚯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖第队,帶你破解...
    沈念sama閱讀 212,185評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哮塞,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡凳谦,警方通過查閱死者的電腦和手機(jī)忆畅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,445評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來尸执,“玉大人家凯,你說我怎么就攤上這事∪缡В” “怎么了绊诲?”我有些...
    開封第一講書人閱讀 157,684評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長褪贵。 經(jīng)常有香客問我掂之,道長,這世上最難降的妖魔是什么脆丁? 我笑而不...
    開封第一講書人閱讀 56,564評(píng)論 1 284
  • 正文 為了忘掉前任世舰,我火速辦了婚禮,結(jié)果婚禮上槽卫,老公的妹妹穿的比我還像新娘跟压。我一直安慰自己,他們只是感情好歼培,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,681評(píng)論 6 386
  • 文/花漫 我一把揭開白布震蒋。 她就那樣靜靜地躺著,像睡著了一般躲庄。 火紅的嫁衣襯著肌膚如雪查剖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,874評(píng)論 1 290
  • 那天读跷,我揣著相機(jī)與錄音梗搅,去河邊找鬼禾唁。 笑死效览,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的荡短。 我是一名探鬼主播丐枉,決...
    沈念sama閱讀 39,025評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼掘托!你這毒婦竟也來了瘦锹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,761評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎弯院,沒想到半個(gè)月后辱士,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,217評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡听绳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,545評(píng)論 2 327
  • 正文 我和宋清朗相戀三年颂碘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片椅挣。...
    茶點(diǎn)故事閱讀 38,694評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡头岔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鼠证,到底是詐尸還是另有隱情峡竣,我是刑警寧澤,帶...
    沈念sama閱讀 34,351評(píng)論 4 332
  • 正文 年R本政府宣布量九,位于F島的核電站适掰,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏荠列。R本人自食惡果不足惜攻谁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,988評(píng)論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望弯予。 院中可真熱鬧戚宦,春花似錦、人聲如沸锈嫩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,778評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呼寸。三九已至艳汽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間对雪,已是汗流浹背河狐。 一陣腳步聲響...
    開封第一講書人閱讀 32,007評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瑟捣,地道東北人馋艺。 一個(gè)月前我還...
    沈念sama閱讀 46,427評(píng)論 2 360
  • 正文 我出身青樓胡嘿,卻偏偏與公主長得像永部,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子侵佃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,580評(píng)論 2 349

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