基於redis的分佈式鎖方案

相關轉載:http://blog.csdn.net/ugg/article/details/41894947


大致的思路先賦上流程圖:


上圖是加鎖的流程圖,解鎖相對簡單,就不賦流程圖了


瞭解了方法之後就在本地碼代碼;調試,心情也是蠻激動的,哈哈。

我的大致策略是:

1.加鎖的方法lock會傳一個key,這個key唯一標識一個待處理的獨立個體,比如一個用戶;一筆借款等,接下來對這個獨立個體的一些敏感操作中,加鎖對應的key永遠是唯一的,這個很重要,否則redis鎖也無從談起了。

2.lock方法如果競爭鎖成功則會返回set到redis中這個key的時間戳即加鎖的時間,我的理解是這樣返回比返回布爾值有個好處:

1)我們在獲得鎖之後,爲了使這個鎖能夠更加健壯,應該在調用關鍵業務的時候重新從redis中獲取最新的時間戳,與寫入的時間戳作比較,確保這個鎖沒有被其他線程del掉。

2)我們在處理完邏輯代碼在解鎖(releaseLock方法)的時候可以回傳這個時間戳,與redis中緩存的時間戳比較,只有符合條件(看代碼註釋)方可解鎖,這樣做可以在很大程度上解決一個線程的獨佔鎖被別的線程誤刪的情況。

3.爲了讓這個鎖更加強壯,獲取鎖的客戶端,應該在調用關鍵業務的時候再次從redis中獲取最新的時間戳,與寫入的時間戳比較,以免鎖因爲其他情況被執行DEL意外解開而不知。

賦上代碼:

@Service
public class LockServiceImpl  implements LockService {
    
    private static final Long FETCH_LOCK_TIMEOUT        = 1000 * 3L;

    public final static Integer REDIS_DATABASE_LOCK     = 3;
    
    
    protected Logger logger = LogManager.getLogger(this.getClass());
    
    
    @Override
    public Long lock(String key) {
        int retry = 3;//重試3次
        Jedis jedis = null;
        Long nowTime;
        try {
            jedis = JedisUtils.getJedis(REDIS_DATABASE_LOCK);
            while (retry > 0) {
                nowTime = System.currentTimeMillis();
                Long result = jedis.setnx(key, nowTime+"");
                if (result == 1)//獲取到鎖
                    return nowTime;
                //未獲取到鎖,判斷舊鎖是否超時 30s
                String lastTimeStr = jedis.get(key);
                if (lastTimeStr == null){
                    retry--;
                    logger.error("舊鎖已釋放,去競爭 retry "+retry+":" + Thread.currentThread().getName());
                    continue;//舊鎖已釋放,去競爭
                }
                Long lastTime = Long.valueOf(lastTimeStr);
                Long interval = nowTime - lastTime;
                if (interval.longValue() < FETCH_LOCK_TIMEOUT.longValue()){
                    Thread.sleep(FETCH_LOCK_TIMEOUT/15);//舊鎖未超時,睡2s再競爭
                    retry--;
                    logger.error("舊鎖未超時,睡0.2s再競爭 retry "+retry+":" + Thread.currentThread().getName());
                    continue;
                }
                //舊鎖已超時,競爭新鎖,同時返回舊鎖時間
                String oldTimeStr = jedis.getSet(key,nowTime+"");
                if (oldTimeStr != null && Long.valueOf(oldTimeStr).longValue() != lastTime.longValue()){
                    Thread.sleep(FETCH_LOCK_TIMEOUT/15)//存在另外的線程先一步執行了以上操作,睡2s再競爭
                    retry--;
                    logger.error("存在另外的線程先一步執行了以上操作,睡0.2s再競爭 retry "+retry+":" + Thread.currentThread().getName());
                    continue;
                }
                // 兩個時間一致,說明不存在另外的併發線程競爭到鎖,競爭鎖成功
                return nowTime;
            }
            return null;
        }  catch (Exception e) {
            e.printStackTrace();
            throw new PledgeException("賬戶鎖加鎖異常!");
        } finally {
            if(jedis != null){
                logger.debug("Close jedis connection and quit key:"+key);
                JedisUtils.returnResource(REDIS_DATABASE_LOCK, jedis);
            }
        }

    }

    @Override
    public ResultCode releaseLock(String key,Long lockedTime) {
        if (lockedTime == null)
            return ResultCode.RELEASE_NOLOCK;
        Jedis jedis = null;
        try {
            jedis = JedisUtils.getJedis(REDIS_DATABASE_LOCK);
            Long lastTime = Long.valueOf(jedis.get(key));
            if (lockedTime.longValue() >= lastTime.longValue()){
                jedis.del(key);
                //如果redis中的加鎖時間比本次處理單元中的加鎖時間大,說明當前鎖是當前線程之後的線程加進去的,不進行del操作
                return ResultCode.RELEASE_SUCCESS;
            }
            return ResultCode.RELEASE_DONE;
        } catch (Exception e) {
            e.printStackTrace();
            return ResultCode.RELEASE_ERROR;
        } finally {
            if(jedis != null){
                logger.debug("Close jedis connection and quit key:"+key);
                JedisUtils.returnResource(REDIS_DATABASE_LOCK, jedis);
            }
        }
    }

}

package com.htt.app.pledge.service.lock;

/**
 * 解鎖結果枚舉
 * Created by sunnyLu on 2017/05/10.
 */
public enum ResultCode {

    RELEASE_SUCCESS("鎖釋放成功",1),
    RELEASE_NOLOCK("沒有競爭到鎖",2),
    RELEASE_DONE("鎖已被其他線程釋放",3),
    RELEASE_ERROR("鎖釋放異常",4);

    /**
     * 創建一個新的實例 ResultCode.
     *
     * @param categoryId
     * @param des
     */
    private ResultCode(String des, int categoryId) {
        this.des = des;
        this.categoryId = categoryId;
    }

    private String des;
    private int categoryId;


    public String getDes() {
        return des;
    }

    public int getCategoryId() {
        return categoryId;
    }

    public static ResultCode findByCategoryId(int categoryId){
        for(ResultCode type : values()){
            if(type.getCategoryId() == categoryId){
                return type;
            }
        }
        return null;
    }

}

本地模擬了多個線程併發訪問的情況:

package com.htt.app.pledge.service.lock;

import org.apache.log4j.Logger;
import org.springframework.beans.BeansException;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * Created by sunnyLu on 2017/5/10.
 */
public class LockThread extends Thread{

    private LockService lockService;

    private Logger logger = Logger.getLogger(LockThread.class);

    public LockThread() {
    }

    public LockThread(LockService lockService) {
        this.lockService = lockService;
    }

    @Override
    public void run() {
        String key = "test_lock";
        Long lockTime = null;
        try {
            lockTime = lockService.lock(key);
            if (lockTime != null){
                logger.error("競爭鎖成功:" + Thread.currentThread().getName());
                logger.error("處理邏輯:" + Thread.currentThread().getName());
                logger.error("模擬業務邏輯處理超時或者程序異常情況睡3秒:" + Thread.currentThread().getName());
                Thread.sleep(3000);
            } else {
                logger.error("競爭鎖失敗:" + Thread.currentThread().getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            ResultCode result = lockService.releaseLock(key,lockTime);
            if (!result.equals(ResultCode.RELEASE_NOLOCK)){
                logger.error(result.getDes()+":"+Thread.currentThread().getName());
            }
        }
    }
    public static void main(String args[]) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("/beans/**/*.xml");
        try {
            LockService lockService = applicationContext.getBean(LockService.class);
            for (int i = 0;i<5;i++){
                LockThread thread = new LockThread(lockService);
                thread.start();
            }
        } catch (BeansException e) {
            e.printStackTrace();
        }
    }
}

控制檯打印結果:



Thread-8競爭到了鎖並且花了3s時間處理,在這個過程中其他的線程分別嘗試了3次都沒有能夠獲取到鎖,只能等到Thread-8釋放鎖。


接下來模擬另外一種場景,線程1競爭到鎖但是由於客觀因素造成死鎖,其他線程嘗試去競爭鎖...

我們將默認的鎖過期時間設成2s,而競爭到鎖的線程依然sleep 3s,每次嘗試重新競爭鎖都先sleep 1s,以此來模擬死鎖的情況

控制檯打印結果:



同樣是Thread-8競爭到鎖,其他線程前兩次嘗試獲取鎖都失敗,第三次嘗試時,Thread-8的鎖已過期,Thread-11競爭鎖成功,最後Thread-8嘗試解鎖發現自己的鎖已被釋放(如果是死鎖的情況Thread-8並不會去嘗試解鎖),而Thread-11能夠成功解鎖。

最後在調試中發現,釋放鎖時還存在一個問題:

1.線程1競爭到鎖,但是程序異常崩潰造成死鎖。

2.線程2,線程3先後去競爭鎖,發現線程1的鎖已過期,並且先後通過getset方法嘗試獲取鎖

3.假設線程2先一步獲取到鎖,那麼線程3getset,覆蓋了線程2getset到redis中的時間戳

上面這種情況,雖然從實際情況來看線程2依舊獨佔鎖,但是當線程2處理完邏輯而去釋放鎖時,會發現redis中的時間戳大於自己set進去的時間戳,而誤以爲是併發原因造成了鎖已被另外的線程佔用,不去del,而事實上這個鎖是線程3競爭失敗的無效鎖。

思來想去沒有想到真正完善的解決方案,所以這個鎖並不能嚴格意義上解決所有的併發問題,在高併發的情況下可能會出現存在無效鎖而加鎖失敗的情況,或多或少會影響用戶體驗,不過相比較高併發造成的數據不同步問題來說這個影響還是可以承受的。我們的鎖有過期失效機制,只要能夠合理的設計失效時間,做好一定的容錯,還是能夠滿足應用的需求。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章