首先說一下需要使用分佈式鎖的場景
- 多實例部署,分佈式系統中經常會遇到,基於jvm的鎖無法滿足多實例中鎖的需求, synchronized,Lock只能控制在當前JVM中的資源競爭。基於數據庫實現的話肯能會造成IO壓力,甚至死鎖,其本身的效率也比較低。zk的實現方式也是可以的。
redis作爲分佈式鎖要注意的事項,還有用到的一些功能
注意事項:
- 互斥性,同一時刻,智能有一個客戶端持有鎖。
- 防止死鎖發生,如果持有鎖的客戶端崩潰沒有主動釋放鎖,也要保證鎖可以正常釋放及其他客戶端可以正常加鎖。
- 防止誤刪,加鎖和釋放鎖必須是同一個客戶端。
- 高可用,如果只有Redis服務端負責加鎖,這個Redis掛了怎麼辦?
用到的功能:
- setnx(key, value): set if not exits,若該key-value不存在,則成功加入緩存並且返回1,否則返回0。
- expire(key, seconds):設置key-value的有效期爲seconds秒。
- watch(key):用於監視一個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那麼事務將被打斷
- unwatch:取消 WATCH 命令對所有 key 的監視
- multi:標記一個事務塊的開始。多條命令會按照先後順序被放進一個隊列當中,最後由 EXEC 命令原子性(atomic)地執行。
- del(key):在 key 存在時刪除 key。
代碼:
註釋TODO的地方需要自行修改處理,日誌也自行替換吧。思路解釋寫在註釋中了,方便理解。
redis客戶端:
/**
* reids 客戶端
*/
public class RedisManager {
private static JedisPool jedisPool;
static{
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPool = new JedisPool(jedisPoolConfig,"127.0.0.1",6379);
}
public static Jedis getJedis() throws Exception {
if(null != jedisPool){
return jedisPool.getResource();
}
throw new Exception("Jedispool was not init");
}
}
獲取鎖:
/**
* 獲取分佈式鎖
* @param key
* @param timeout 超時時限
* @return
*/
public String getLock(String key,int timeout){
//TODO 過期時間從緩存中取 配置化
try {
//獲得客戶端實例
Jedis jedis = RedisManager.getJedis();
//生成隨機鎖號,解鎖時用於判斷,是否爲當前線程持有鎖
String value = UUID.randomUUID().toString();
//不能無限等待,超出時間放棄自旋
long end = System.currentTimeMillis() + timeout;
//判斷是否超出時限 並且阻塞
while(System.currentTimeMillis()<end){
//設置 setnx 成功後 返回1,否則爲0
if(jedis.setnx(key,value)==1){
jedis.expire(key,timeout);
return value;
}
//如果鎖獲取成功 reids掛掉,判斷鎖永久有效,再次設置失效時間
if(jedis.ttl(key)==-1){
jedis.expire(key,timeout);
}
//不能無限佔用資源 設置休眠,然後再次獲取鎖
Thread.sleep(1000);
}
} catch (Exception e) {
//TODO 日誌
e.printStackTrace();
}
return null;
}
釋放鎖:
/**
* 釋放鎖(判斷是否爲當前線程持有)
* @param key
* @param value
* @return
*/
public boolean releaseLock(String key,String value){
try {
Jedis jedis = RedisManager.getJedis();
while (true) {
//監控一個或多個KEY 一旦KEY 被刪除 ,後面事務的代碼不會被執行
jedis.watch(key);
//判斷是否爲當前線程持有鎖
if(value.equals(jedis.get(key))) {
//開啓事務
Transaction transaction = jedis.multi();
transaction.del(key);
List<Object> list = transaction.exec();
//沒有指令集執行成功 解鎖失敗
//TODO 關注事務執行過程(監視解鎖前 人爲變動鎖可能會出現的問題)
if (list == null) {
continue;
}
System.out.println("鎖釋放成功,key【"+key+"】,value【"+value+"】");
return true;
}
jedis.unwatch();
break;
}
} catch (Exception e) {
//TODO 添加日誌
System.out.println("進程名稱【"+Thread.currentThread().getName()+"】"+"進程ID【"+Thread.currentThread().getId()+"】解鎖失敗"+e.getCause());
e.printStackTrace();
}
return false;
}
測試:
public static void main(String... args) throws Exception {
RedisLock redisLock = new RedisLock();
// Jedis jedis = RedisManager.getJedis();
// String curLock = jedis.get("lock:testLock");
// System.out.println(curLock);
// //強制解鎖 清空
// jedis.del("lock:testLock");
String key = "lock:testLock";
String lockId = redisLock.getLock(key,10000);
if(null != lockId){
System.out.println("進程名稱【"+Thread.currentThread().getName()+"】"+"進程ID【"+Thread.currentThread().getId()+"】獲得鎖成功,"+"鎖KEY【"+key+"】"+"鎖序號:【"+lockId+"】");
}else{
System.out.println("進程名稱【"+Thread.currentThread().getName()+"】"+"進程ID【"+Thread.currentThread().getId()+"】獲得鎖失敗");
}
redisLock.releaseLock("lock:testLock",lockId);
System.out.println("完成");
}
入門級demo 主要用於理解思路,demo未經過生產考驗!!!!