1. 開局
在多線程環(huán)境中,經(jīng)常會碰到需要加鎖的情況,由于現(xiàn)在的系統(tǒng)基本都是集群分布式部署,JVM的lock已經(jīng)不能滿足分布式要求,分布式鎖就這樣產(chǎn)生了。。。
百度一下,網(wǎng)上有很多分布式鎖的方案或者例子,琳瑯滿目,看了之后不知所措,總體來說有以下幾種:
- 基于數(shù)據(jù)庫
- 基于zookeeper
- 基于redis
- 基于memcached
各有優(yōu)缺點和實現(xiàn)難度,這里就不一一分析。本文主要是基于redis的setnx實現(xiàn)分布式鎖,比較簡單有一定的局限性,歡迎大家提出意見建議!
2. 加鎖過程
- 執(zhí)行redis的setnx,只有key不存在才能set成功(實際使用的是set(key, value, "NX", "EX", seconds),redis較新版本支持)
- 如果set成功(同時也設(shè)置了key的過期時間),則表示加鎖成功
- 如果set失敗,則每次sleep(x)毫秒后不斷嘗試,直到成功或者超時
3. 釋放過程
- 判斷加鎖是否成功
- 如果成功,則執(zhí)行redis的del刪除
4. 問題思考
- 加鎖時,鎖的redis key過期時間多長合適?
需要根據(jù)業(yè)務(wù)執(zhí)行的時間長度來評估,默認30秒滿足絕大部分需求,支持動態(tài)修改 - 加鎖時,重試超時時間多長合適?本文設(shè)置的是過期時間的1.2倍,目的是在最壞的情況下等待鎖過期后,盡量保證獲取到鎖,否則拋出超時異常。這個設(shè)置不完全合理
- 加鎖時,重試的sleep時間多長合適?本文采用的是隨機[50-300)毫秒,避免出現(xiàn)大量線程同時競爭,目的是錯峰吧
- 釋放時,如何避免釋放了其他線程的鎖(A獲取鎖后由于掛起導(dǎo)致鎖到期自動釋放;此時B獲取到鎖,而A又恢復(fù)運行釋放了B的鎖)?在初始化鎖時生個一個唯一字符串,作為redis鎖的value;value一致時表明是自己的鎖,可以釋放
5. 上代碼!
- 用法
RedisLock lock = new RedisLock(redisHelper, lockKey);
try {
// 執(zhí)行加鎖,防止并發(fā)問題
lock.tryLock();
// do somethings
doSomethings()
}
finally {
// 釋放鎖
lock.release();
}
- 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