redisLock redis分佈式鎖

redis-lock

  • redis setnx cmmand
  • java object condition queue 條件隊列
  • retrycount 帶有重試次數限制
  • object wait time 帶有超時時間的wait
  • delete lock 刪除遠程鎖
  • acquire lock 申請lock
  • release lock 釋放lock
  • demo 演示
  • 鎖的粒度問題,鎖分解、鎖分段
  • github https://github.com/Plen-wang/redis-lock

redis setnx 命令

redis setnx 命令特性java

當指定key不存在時才設置。也就是說,若是返回1說明你的命令被執行成功了,redis服務器中的key是你以前設置的值。若是返回0,說明你設置的key在redis服務器裏已經存在。git

status = jedis.setnx(lockKey, redisIdentityKey);/**設置 lock key.*/
            if (status > 0) {
                expire = jedis.expire(lockKey, lockKeyExpireSecond);/**set  redis key expire time.*/
            }

若是設置成功了,才進行過時時間設置,防止你的retry lock重複設置這個過時時間,致使永遠不過時。github

java object condition queue 條件隊列

這裏有一個小竅門,能夠儘量的最大化cpu利用率又能夠解決公平性問題。redis

當你頻繁retry的時候,要麼while(true)死循環,而後加個Thread.sleep,或者CAS。前者存在必定線程上下文切換開銷(Thread.sleep是不會釋放出當前內置鎖),而CAS在不清楚遠程鎖被佔用多久的狀況會浪費不少CPU計算週期,有可能一個任務計算個十幾分鍾,CPU不可能空轉這麼久。服務器

這裏我嘗試使用condition queue條件隊列特性來實現(固然確定還有其餘更優的方法)。併發

if (isWait && retryCounts < RetryCount) {
                    retryCounts++;
                    synchronized (this) {//藉助object condition queue 來提升CPU利用率
                        logger.info(String.
                                format("t:%s,當前節點:%s,嘗試等待獲取鎖:%s", Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
                        this.wait(WaitLockTimeSecond); //未能獲取到lock,進行指定時間的wait再重試.
                    }
                } else if (retryCounts == RetryCount) {
                    logger.info(String.
                            format("t:%s,當前節點:%s,指定時間內獲取鎖失敗:%s", Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
                    return false;
                } else {
                    return false;//不須要等待,直接退出。
                }

使用條件隊列的好處就是,它雖然釋放出了CPU可是也不會持有當前synchronized,這樣就可讓其餘併發進來的線程也能夠獲取到當前內置鎖,而後造成隊列。當wait時間到了被調度喚醒以後纔會從新來申請synchronized鎖。
簡單講就是不會再鎖上等待而是在隊列裏等待。java object每個對象都持有一個條件隊列,與當前內置鎖配合使用。性能

retrycount 帶有重試次數限制

等待遠程redis lock確定是須要必定重試機制,可是這種重試是須要必定的限制。優化

/**
     * 重試獲取鎖的次數,能夠根據當前任務的執行時間來設置。
     * 須要時間=RetryCount*(WaitLockTimeSecond/1000)
     */
    private static final int RetryCount = 10;

這種等待是須要用戶指定的, if (isWait && retryCounts < RetryCount) ,當isWait爲true纔會進行重試。ui

object wait time 帶有超時時間的wait

object.wait(timeout),條件隊列中的方法wait是須要一個waittime。this

/**
     * 等待獲取鎖的時間,能夠根據當前任務的執行時間來設置。
     * 設置的過短,浪費CPU,設置的太長鎖就不太公平。
     */
    private static final long WaitLockTimeSecond = 2000;

默認2000毫秒。

this.wait(WaitLockTimeSecond); //未能獲取到lock,進行指定時間的wait再重試.

注意:this.wait雖然會blocking住,可是這裏的內置鎖是會當即釋放出來的。因此,有時候咱們能夠藉助這種特性來優化特殊場景。

delete lock 刪除遠程鎖

釋放redis lock比較簡單,直接del key就行了

long status = jedis.del(lockKey);
        if (status > 0) {
            logger.info(String.
                    format("t:%s,當前節點:%s,釋放鎖:%s 成功。", Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
            return true;
        }

一旦delete 以後,首先wait喚醒的線程將會得到鎖。

acquire lock 申請lock

/**
     * 帶超時時間的redis lock.
     *
     * @param lockKeyExpireSecond 鎖key在redis中的過去時間
     * @param lockKey             lock key
     * @param isWait              當獲取不到鎖時是否須要等待
     * @throws Exception lockKey is empty throw exception.
     */
    public Boolean acquireLockWithTimeout(int lockKeyExpireSecond, String lockKey, Boolean isWait) throws Exception {
        if (StringUtils.isEmpty(lockKey)) throw new Exception("lockKey is empty.");

        int retryCounts = 0;
        while (true) {
            Long status, expire = 0L;
            status = jedis.setnx(lockKey, redisIdentityKey);/**設置 lock key.*/
            if (status > 0) {
                expire = jedis.expire(lockKey, lockKeyExpireSecond);/**set  redis key expire time.*/
            }
            if (status > 0 && expire > 0) {
                logger.info(String.
                        format("t:%s,當前節點:%s,獲取到鎖:%s", Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
                return true;/**獲取到lock*/
            }

            try {
                if (isWait && retryCounts < RetryCount) {
                    retryCounts++;
                    synchronized (this) {//藉助object condition queue 來提升CPU利用率
                        logger.info(String.
                                format("t:%s,當前節點:%s,嘗試等待獲取鎖:%s", Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
                        this.wait(WaitLockTimeSecond); //未能獲取到lock,進行指定時間的wait再重試.
                    }
                } else if (retryCounts == RetryCount) {
                    logger.info(String.
                            format("t:%s,當前節點:%s,指定時間內獲取鎖失敗:%s", Thread.currentThread().getId(), getRedisIdentityKey(), lockKey));
                    return false;
                } else {
                    return false;//不須要等待,直接退出。
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
## release lock 釋放lock
/**
     * 釋放redis lock。
     *
     * @param lockKey lock key
     * @throws Exception lockKey is empty throw exception.
     */
    public Boolean releaseLockWithTimeout(String lockKey) throws Exception {
        if (StringUtils.isEmpty(lockKey)) throw new Exception("lockKey is empty.");

        long status = jedis.del(lockKey);
        if (status > 0) {
            logger.info(String.format("當前節點:%s,釋放鎖:%s 成功。", getRedisIdentityKey(), lockKey));
            return true;
        }
        logger.info(String.format("當前節點:%s,釋放鎖:%s 失敗。", getRedisIdentityKey(), lockKey));
        return false;
    }

demo 演示

2017-06-18 13:57:43.867 INFO 1444 --- [nio-8080-exec-1] c.plen.opensource.implement.RedisLocker : t:23,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,獲取到鎖:product:10100101:shopping
2017-06-18 13:57:47.062 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:57:49.063 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:57:51.064 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:57:53.066 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:57:55.068 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:57:57.069 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:57:59.070 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:01.071 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:03.072 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:05.073 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:07.074 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,指定時間內獲取鎖失敗:product:10100101:shopping
2017-06-18 13:58:23.768 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:25.769 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:27.770 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:29.772 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:31.773 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:33.774 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:35.774 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,獲取到鎖:product:10100101:shopping

thread 23 優先獲取到對商品ID 10100101 進行修改,因此先鎖住當前商品。

t:23,當前節點:843d3ec0-9c22-4d8a-bcaa-745dba35b8a4,獲取到鎖:product:10100101:shopping

緊接着,thread 25也來對當前商品 10100101進行修改,因此在嘗試獲取鎖。

2017-06-18 13:50:11.021 INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:946b7250-29f3-459b-8320-62d31e6f1fc4,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:50:13.023 INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:946b7250-29f3-459b-8320-62d31e6f1fc4,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:50:15.026 INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:946b7250-29f3-459b-8320-62d31e6f1fc4,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:50:17.028 INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:946b7250-29f3-459b-8320-62d31e6f1fc4,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:50:19.030 INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:946b7250-29f3-459b-8320-62d31e6f1fc4,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:50:21.031 INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:946b7250-29f3-459b-8320-62d31e6f1fc4,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:50:23.035 INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:946b7250-29f3-459b-8320-62d31e6f1fc4,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:50:25.037 INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:946b7250-29f3-459b-8320-62d31e6f1fc4,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:50:27.041 INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:946b7250-29f3-459b-8320-62d31e6f1fc4,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:50:29.042 INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:946b7250-29f3-459b-8320-62d31e6f1fc4,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:50:35.289 INFO 4616 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:946b7250-29f3-459b-8320-62d31e6f1fc4,指定時間內獲取鎖失敗:product:10100101:shopping

在進行了retry10次(2000毫秒,2秒)以後,獲取失敗,直接返回,等待下次任務調度開始。

2017-06-18 13:58:07.074 INFO 1444 --- [nio-8080-exec-3] c.plen.opensource.implement.RedisLocker : t:25,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,指定時間內獲取鎖失敗:product:10100101:shopping
2017-06-18 13:58:23.768 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:25.769 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:27.770 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:29.772 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:31.773 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:33.774 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,嘗試等待獲取鎖:product:10100101:shopping
2017-06-18 13:58:35.774 INFO 1444 --- [nio-8080-exec-6] c.plen.opensource.implement.RedisLocker : t:28,當前節點:5f81f482-295a-4394-b8cb-d7282e51dd6e,獲取到鎖:product:10100101:shopping

thread 28 發起對商品 10100101 進行修改,retry6次以後獲取到lock。

鎖的粒度問題,鎖分解、鎖分段

這裏的例子比較簡單。若是在併發比較大的狀況下是須要結合鎖分解、鎖分段來進行優化的。
修改商品,沒有必要鎖住整個商品庫,只須要鎖住你須要修改的指定ID的商品。也能夠借鑑鎖分段思路,將數據按照必定維度進行劃分,而後加上不一樣維度的鎖,能夠提高CPU性能。能夠根據商品catagory來設計段鎖或者batch來設計段鎖。

github

源碼已提交gihub,代碼若有不對請多指教。 github地址:https://github.com/Plen-wang/redis-lock

相關文章
相關標籤/搜索