Redis分佈式鎖實現

Redis鎖主要用到了Redis中的如下幾個API,這裏引用官方的API文檔介紹說明一下:redis

SETNX

Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed. SETNX is short for "SET if Not eXists".分佈式

Example:ide

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis> this

GETSET

GETSET can be used together with INCR for counting with atomic reset. For example: a process may call INCR against the key mycounter every time some event occurs, but from time to time we need to get the value of the counter and reset it to zero atomically. This can be done using GETSET mycounter "0":atom

 

Example:線程

 

redis> SET mykey "Hello"
"OK"
redis> GETSET mykey "World"
"Hello"
redis> GET mykey
"World"
redis> orm

expire

Integer reply, specifically:
1 if the timeout was set.
0 if key does not exist.接口

Example:ip

redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10
redis> SET mykey "Hello World"
"OK"
redis> TTL mykey
(integer) -1
redis> ci

官方文檔介紹的也比較淺顯易懂,這裏不過多贅述。Redis可使用Redission,也可使用JedisCulster,而且以Api的方式提供給業務方

這裏實現方式以下,互斥key業務方本身定製

接口層:

/**
 * Redis分佈式鎖
 */
public interface DistributeLock {
    /**
     * 默認鎖有效時間(單位毫秒)
     */
    public static final long DEFAULT_LOCK_EXPIRE_TIME_MS = 60000;
    /**
     * 默認睡眠時間(單位毫秒)
     */
    public static final long DEFAULT_SLEEP_TIME_MS = 100;

    /**
     * 嘗試鎖
     *
     * @param lock 鎖的鍵
     * @param requestTimeoutMS 請求超時 ms
     * @return 若是鎖成功,則返回true;不然返回false
     */
    boolean tryLock(String lock, long requestTimeoutMS);

    /**
     * 嘗試鎖
     *
     * @param lock 鎖的鍵
     * @param lockExpireTimeMS 鎖有效期 ms
     * @param requestTimeoutMS 請求超時 ms
     */
    boolean tryLock(String lock, long lockExpireTimeMS, long requestTimeoutMS);

    /**
     * 解鎖
     *
     * @param lock 鎖的鍵
     */
    void unlock(String lock);
}

 

具體實現層:

/**
 * Description:分佈式鎖
 * Created by Jiyajie on 2017/12/15.
 */
@Service
public class RedisDistributeLock implements DistributeLock {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisDistributeLock.class);
    /**
     * 每秒的毫秒數
     */
    private static final long MILLIS_PER_SECOND = 1000L;
    /**
     * 0值
     */
    private static final int INT_ZERO = 0;
    /**
     * 1值
     */
    private static final int INT_ONE = 1;
    /**
     * jedisCluster
     */
    @Autowired
    private JedisCluster jedisCluster;


    @Override
    public boolean tryLock(String lock, long requestTimeoutMS) {
        return this.tryLock(lock, DEFAULT_LOCK_EXPIRE_TIME_MS, requestTimeoutMS);
    }

    @Override
    public boolean tryLock(String lock, long lockExpireTimeMS, long requestTimeoutMS) {
        Preconditions.checkArgument(StringUtils.isNotBlank(lock), "lock invalid");
        Preconditions.checkArgument(lockExpireTimeMS > INT_ZERO, "lockExpireTimeMS invalid");
        Preconditions.checkArgument(requestTimeoutMS > INT_ZERO, "requestTimeoutMS invalid");

        while (requestTimeoutMS > INT_ZERO) {
            String expire = String.valueOf(System.currentTimeMillis() + lockExpireTimeMS + INT_ONE);

            Long result = jedisCluster.setnx(lock, expire);
            if (result > INT_ZERO) {
                //目前沒有線程佔用此鎖
                jedisCluster.expire(lock, Long.valueOf(lockExpireTimeMS / MILLIS_PER_SECOND).intValue());
                return true;
            }
            String currentValue = jedisCluster.get(lock);
            if (currentValue == null) {
                //鎖已經被其餘線程刪除立刻重試獲取鎖
                continue;
            } else if (Long.parseLong(currentValue) < System.currentTimeMillis()) {
                //此處判斷出鎖已經超過了其有效的存活時間
                String oldValue = jedisCluster.getSet(lock, expire);
                if (oldValue == null || oldValue.equals(currentValue)) {
                    //1.若是拿到的舊值是空則說明在此線程作getSet以前已經有線程將鎖刪除,因爲此線程getSet操做以後已經對鎖設置了值,實際上至關於它已經佔有了鎖
                    //2.若是拿到的舊值不爲空且等於前面查到的值,則說明在此線程進行getSet操做以前沒有其餘線程對鎖設置了值,則此線程是第一個佔有鎖的
                    jedisCluster.expire(lock, Long.valueOf(lockExpireTimeMS / MILLIS_PER_SECOND).intValue());
                    return true;
                }
            }
            long sleepTime;
            if (requestTimeoutMS > DEFAULT_SLEEP_TIME_MS) {
                sleepTime = DEFAULT_SLEEP_TIME_MS;
                requestTimeoutMS -= DEFAULT_SLEEP_TIME_MS;
            } else {
                sleepTime = requestTimeoutMS;
                requestTimeoutMS = INT_ZERO;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(sleepTime);
            } catch (InterruptedException e) {
                LOGGER.warn("分佈式鎖線程休眠異常{}", lock, e);
            }
        }
        return false;
    }

    @Override
    public void unlock(String lock) {
        String value = jedisCluster.get(lock);
        if (null != value && Long.parseLong(value) > System.currentTimeMillis()) {
            //若是鎖還存在而且還在有效時間則進行刪除
            jedisCluster.del(lock);
        }
    }
}

這裏對實現步驟,以及實現原理進行分析:

1:參數校驗,對傳入的互斥key,鎖超時時間,請求超時時間進行檢驗

2:在請求超時時間以內的請求,這裏以while死循環的方式不斷進行獲取鎖重試

3:設置鎖過時時間,並嘗試用setnx命令,redis以前不存在key的狀況下,設置key,同時把過時時間expire做爲value設置進去。若是獲取成功,這裏給鎖加上真正的過時時間,獲取鎖成功~

4:在第三步沒有成功的狀況下,咱們直接再次獲取鎖。若是爲空,則說明鎖已通過期,或者已經被其餘線程解鎖,那麼咱們馬上結束本次循環,嘗試從新獲取~

5:若是第四步獲取鎖成功,咱們須要進行一下判斷:1拿到鎖的過時時間(key對應的value),並判斷鎖是否在過時時間以內,若是在的話,用具備原子性操做的命令getset,取出以前的過時時間oldValue值,這裏會有兩種狀況:

//1.若是拿到的舊值是空則說明在此線程作getSet以前已經有線程將鎖刪除,因爲此線程getSet操做以後已經對鎖設置了值,實際上至關於它已經佔有了鎖

//2.若是拿到的舊值不爲空且等於前面查到的值,則說明在此線程進行getSet操做以前沒有其餘線程對鎖設置了值,則此線程是第一個佔有鎖的
兩種狀況都說明已經獲取鎖成功,結束循環

以上步驟都是創建在,請求超時時間以內的,這裏每次循環獲取,間隔100毫秒~,當獲取時間超過請求超時時間的話:也是鎖獲取失敗的一種狀況

至此分佈式鎖的獲取結束~

解鎖操做很簡單,這裏也再也不贅述~

相關文章
相關標籤/搜索