java 通過redis實現(xiàn)分布式鎖

1. 開局

在多線程環(huán)境中,經(jīng)常會碰到需要加鎖的情況,由于現(xiàn)在的系統(tǒng)基本都是集群分布式部署,JVM的lock已經(jīng)不能滿足分布式要求,分布式鎖就這樣產(chǎn)生了。。。

百度一下,網(wǎng)上有很多分布式鎖的方案或者例子,琳瑯滿目,看了之后不知所措,總體來說有以下幾種:

  1. 基于數(shù)據(jù)庫
  2. 基于zookeeper
  3. 基于redis
  4. 基于memcached

各有優(yōu)缺點和實現(xiàn)難度,這里就不一一分析。本文主要是基于redis的setnx實現(xiàn)分布式鎖,比較簡單有一定的局限性,歡迎大家提出意見建議!

2. 加鎖過程
  1. 執(zhí)行redis的setnx,只有key不存在才能set成功(實際使用的是set(key, value, "NX", "EX", seconds),redis較新版本支持)
  2. 如果set成功(同時也設(shè)置了key的過期時間),則表示加鎖成功
  3. 如果set失敗,則每次sleep(x)毫秒后不斷嘗試,直到成功或者超時
3. 釋放過程
  1. 判斷加鎖是否成功
  2. 如果成功,則執(zhí)行redis的del刪除
4. 問題思考
  1. 加鎖時,鎖的redis key過期時間多長合適?
    需要根據(jù)業(yè)務(wù)執(zhí)行的時間長度來評估,默認30秒滿足絕大部分需求,支持動態(tài)修改
  2. 加鎖時,重試超時時間多長合適?本文設(shè)置的是過期時間的1.2倍,目的是在最壞的情況下等待鎖過期后,盡量保證獲取到鎖,否則拋出超時異常。這個設(shè)置不完全合理
  3. 加鎖時,重試的sleep時間多長合適?本文采用的是隨機[50-300)毫秒,避免出現(xiàn)大量線程同時競爭,目的是錯峰吧
  4. 釋放時,如何避免釋放了其他線程的鎖(A獲取鎖后由于掛起導(dǎo)致鎖到期自動釋放;此時B獲取到鎖,而A又恢復(fù)運行釋放了B的鎖)?在初始化鎖時生個一個唯一字符串,作為redis鎖的value;value一致時表明是自己的鎖,可以釋放
5. 上代碼!
  1. 用法

RedisLock lock = new RedisLock(redisHelper, lockKey);
try {
    // 執(zhí)行加鎖,防止并發(fā)問題
    lock.tryLock();
    // do somethings
    doSomethings()
}
finally {
    // 釋放鎖
    lock.release();
}
  1. RedisLock實現(xiàn)(注:依賴RedisHepler類,僅僅是對jedis的一層封裝,可自行實現(xiàn))
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * RedisLock
 * 
 * @version 2017-9-21上午11:56:27
 * @author xiaoyun.zeng
 */
public class RedisLock {
    
    private Logger logger = LoggerFactory.getLogger(getClass());
    
    /**
     * key前綴
     */
    private static final String PREFIX = "lock:";
    /**
     * 操作redis的工具類
     */
    private RedisHelper redisHelper;
    /**
     * redis key
     */
    private String redisKey = null;
    /**
     * redis value
     */
    private String redisValue = null;
    /**
     * 鎖的過期時間(秒),默認30秒,防止線程獲取鎖后掛掉無法釋放鎖
     */
    private int lockExpire = 30;
    /**
     * 嘗試加鎖超時時間(毫秒),默認為expire的1.2倍
     */
    private int tryTimeout = lockExpire * 1200;
    /**
     * 嘗試加鎖次數(shù)計數(shù)器
     */
    private long tryCounter = 0;
    /**
     * 加鎖成功標記
     */
    private boolean success = false;
    private long startMillis = 0;
    private long expendMillis = 0;
    
    /**
     * RedisLock
     * 
     * @param redisHelper
     * @param lockKey
     */
    public RedisLock(RedisHelper redisHelper, String lockKey) {
        this.redisHelper = redisHelper;
        this.redisKey = PREFIX + lockKey;
        // 生成redis value,用于釋放鎖時比對是否屬于自己的鎖
        // 生成規(guī)則 lockKey+時間戳+隨機數(shù),避免重復(fù)
        // 樂觀地認為:
        // 1、同一毫秒內(nèi),隨機數(shù)相同的概率極小
        // 2、釋放非自己線程鎖的幾率極小(release方法有說明這種情況)
        this.redisValue = lockKey + "-" + System.currentTimeMillis() + "-" + this.random(10000);
    }
    
    /**
     * RedisLock
     * 
     * @param redisHelper
     * @param lockKey
     * @param expire
     */
    public RedisLock(RedisHelper redisHelper, String lockKey, int lockExpire) {
        this(redisHelper, lockKey);
        // 過期時間
        this.lockExpire = lockExpire;
        // 超時時間(毫秒),默認為expire的1.2倍
        this.tryTimeout = lockExpire * 1200;
    }
    
    /**
     * 嘗試加鎖
     * <p>
     * 嘗試加鎖的過程將會一直阻塞下去,直到加鎖成功或超時
     * 
     * @version 2017-9-21下午12:00:07
     * @author xiaoyun.zeng
     * @return
     */
    public void tryLock() throws RuntimeException {
        startMillis = System.currentTimeMillis();
        // 首次直接請求加鎖
        if (!lock()) {
            do {
                // 超時判斷,避免永遠獲取不到鎖的情況下,一直嘗試
                // 超時拋出runtime異常
                if (System.currentTimeMillis() - startMillis >= tryTimeout) {
                    throw new RuntimeException("嘗試加鎖超時" + tryTimeout + "ms");
                }
                // 隨機休眠[50-300)毫秒
                // 避免出現(xiàn)大量線程同時競爭
                try {
                    Thread.sleep(this.random(250) + 50);
                }
                catch (InterruptedException e) {
                    // 出現(xiàn)異常直接拋出
                    throw new RuntimeException(e);
                }
            }
            while (!lock());
        }
    }
    
    /**
     * 釋放鎖
     * 
     * @version 2017-9-21下午12:00:21
     * @author xiaoyun.zeng
     * @param lockKey
     */
    public void release() {
        // 加鎖成功才執(zhí)行釋放
        if (success) {
            // 釋放前,檢查redis value是否一致
            // 避免A獲取鎖后由于掛起導(dǎo)致鎖到期自動釋放
            // 此時B獲取到鎖,而A又恢復(fù)運行釋放了B的鎖
            String value = redisHelper.get(redisKey);
            if (redisValue.equals(value)) {
                redisHelper.del(redisKey);
                logger.debug("已釋放鎖:{}", redisValue);
            }
        }
    }
    
    /**
     * 加鎖
     * 
     * @version 2017-9-21下午6:25:58
     * @author xiaoyun.zeng
     * @param key
     * @param value
     * @param lockExpire
     * @return
     */
    private boolean lock() {
        // 加鎖計數(shù)器+1
        tryCounter++;
        // 調(diào)用redis setnx完成加鎖,返回true表示加鎖成功,否則失敗
        success = redisHelper.setNx(redisKey, redisValue, lockExpire);
        // 計算總耗時
        expendMillis = System.currentTimeMillis() - startMillis;
        // 記錄日志
        if (success) {
            logger.debug("加鎖成功:嘗試{}次,耗時{}ms,{}", tryCounter, expendMillis, redisValue);
        }
        return success;
    }
    
    /**
     * 產(chǎn)生隨機數(shù)
     * 
     * @version 2017-9-22上午10:05:52
     * @author xiaoyun.zeng
     * @param max
     * @return
     */
    private int random(int max) {
        return (int) (Math.random() * max);
    }
    
}

6. 測試代碼

單元測試:

@RunWith(SpringRunner.class)  
@SpringBootTest
public class RedisLockTest {
    
    @Autowired
    private RedisHelper redisHelper;
    
    @Test
    public void test() {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    RedisLock lock = new RedisLock(redisHelper, "zxy");
                    try {
                        lock.tryLock();
                        try {
                            Thread.sleep(2 * 1000);
                        }
                        catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    finally {
                        lock.release();
                    }
                }
            }).start();
        }
        while(true) {
        }
    }
}

日志輸出:

2017/10/12 17:47:28.335 [Thread-8]  DEBUG [RedisLock.161] 加鎖成功:嘗試1次,耗時4ms,zxy-1507801648330-6665
2017/10/12 17:47:30.340 [Thread-8]  DEBUG [RedisLock.137] 已釋放鎖:zxy-1507801648330-6665
2017/10/12 17:47:30.351 [Thread-14] DEBUG [RedisLock.161] 加鎖成功:嘗試12次,耗時2018ms,zxy-1507801648333-6866
2017/10/12 17:47:32.356 [Thread-14] DEBUG [RedisLock.137] 已釋放鎖:zxy-1507801648333-6866
2017/10/12 17:47:32.396 [Thread-11] DEBUG [RedisLock.161] 加鎖成功:嘗試22次,耗時4065ms,zxy-1507801648331-5217
2017/10/12 17:47:34.400 [Thread-11] DEBUG [RedisLock.137] 已釋放鎖:zxy-1507801648331-5217
2017/10/12 17:47:34.430 [Thread-12] DEBUG [RedisLock.161] 加鎖成功:嘗試39次,耗時6098ms,zxy-1507801648332-7708
2017/10/12 17:47:36.433 [Thread-12] DEBUG [RedisLock.137] 已釋放鎖:zxy-1507801648332-7708
2017/10/12 17:47:36.453 [Thread-17] DEBUG [RedisLock.161] 加鎖成功:嘗試50次,耗時8119ms,zxy-1507801648334-2362
2017/10/12 17:47:38.457 [Thread-17] DEBUG [RedisLock.137] 已釋放鎖:zxy-1507801648334-2362
2017/10/12 17:47:38.494 [Thread-9]  DEBUG [RedisLock.161] 加鎖成功:嘗試57次,耗時10164ms,zxy-1507801648330-7086
2017/10/12 17:47:40.497 [Thread-9]  DEBUG [RedisLock.137] 已釋放鎖:zxy-1507801648330-7086
2017/10/12 17:47:40.587 [Thread-13] DEBUG [RedisLock.161] 加鎖成功:嘗試70次,耗時12254ms,zxy-1507801648333-8881
2017/10/12 17:47:42.590 [Thread-13] DEBUG [RedisLock.137] 已釋放鎖:zxy-1507801648333-8881
2017/10/12 17:47:42.611 [Thread-15] DEBUG [RedisLock.161] 加鎖成功:嘗試82次,耗時14276ms,zxy-1507801648335-2509
2017/10/12 17:47:44.614 [Thread-15] DEBUG [RedisLock.137] 已釋放鎖:zxy-1507801648335-2509
2017/10/12 17:47:44.699 [Thread-16] DEBUG [RedisLock.161] 加鎖成功:嘗試89次,耗時16365ms,zxy-1507801648334-5791
2017/10/12 17:47:46.702 [Thread-16] DEBUG [RedisLock.137] 已釋放鎖:zxy-1507801648334-5791
2017/10/12 17:47:46.802 [Thread-10] DEBUG [RedisLock.161] 加鎖成功:嘗試106次,耗時18471ms,zxy-1507801648331-7347
2017/10/12 17:47:48.805 [Thread-10] DEBUG [RedisLock.137] 已釋放鎖:zxy-1507801648331-7347
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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