分散式鎖(三)__基於redis實現
本文參考借鑑了論壇大佬的一篇很詳細的博文並在此基礎上加以實現,在此謝謝此位博主!,博文連線:
https://www.cnblogs.com/linjiqin/p/8003838.html
前言
首先,為了確保分散式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:
- 互斥性。在任意時刻,只有一個客戶端能持有鎖。
- 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
- 具有容錯性。只要大部分的Redis節點正常執行,客戶端就可以加鎖和解鎖。
- 解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了
實現
引入依賴:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.2</version>
</dependency>
和分散式鎖一,二一樣的orderSevice
package com.th.order;
public interface OrderService {
void createOrder();
}
模擬共享資源的OrderCodeGenerator:
package com.th.order; import java.text.SimpleDateFormat; import java.util.Date; public class OrderCodeGenerator { private static int i = 0; public String getOrderCode() { Date now = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-"); return sdf.format(now) + ++i; } }
主角RedisLock:
下面是加鎖的方法:下面包含程式碼解析
@Override
public void lock() {
if (!tryLock()) {
// 沒有獲得鎖,阻塞自己
waitForLock();
// 再次嘗試加鎖
lock();
}
}
@Override
public boolean tryLock() {
String result = jedis.set(LOCK_KEY, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result))
return true;
return false;
}
private void waitForLock() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
可以看到,我們加鎖就一行程式碼:jedis.set(String key, String value, String nxxx, String expx, int time)
,這個set()方法一共有五個形參:
-
第一個為key,我們使用key來當鎖,因為key是唯一的。
-
第二個為value,我們傳的是requestId,很多童鞋可能不明白,有key作為鎖不就夠了嗎,為什麼還要用到value?原因就是我們在上面講到可靠性時,分散式鎖要滿足第四個條件解鈴還須繫鈴人,通過給value賦值為requestId,我們就知道這把鎖是哪個請求加的了,在解鎖的時候就可以有依據。requestId可以使用
UUID.randomUUID().toString()
方法生成。 -
第三個為nxxx,這個引數我們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,我們進行set操作;若key已經存在,則不做任何操作;
-
第四個為expx,這個引數我們傳的是PX,意思是我們要給這個key加一個過期的設定,具體時間由第五個引數決定。
-
第五個為time,與第四個引數相呼應,代表key的過期時間。
總的來說,執行上面的set()方法就只會導致兩種結果:1. 當前沒有鎖(key不存在),那麼就進行加鎖操作,並對鎖設定個有效期,同時value表示加鎖的客戶端。2. 已有鎖存在,不做任何操作。
心細的童鞋就會發現了,我們的加鎖程式碼滿足我們可靠性裡描述的三個條件。首先,set()加入了NX引數,可以保證如果已有key存在,則函式不會呼叫成功,也就是隻有一個客戶端能持有鎖,滿足互斥性。其次,由於我們對鎖設定了過期時間,即使鎖的持有者後續發生崩潰而沒有解鎖,鎖也會因為到了過期時間而自動解鎖(即key被刪除),不會發生死鎖。最後,因為我們將value賦值為requestId,代表加鎖的客戶端請求標識,那麼在客戶端在解鎖的時候就可以進行校驗是否是同一個客戶端。由於我們只考慮Redis單機部署的場景,所以容錯性我們暫不考慮。
解鎖方法:
@Override
public void unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
// return true;
System.out.println("解鎖成功!");
} else {
System.out.println("不是自己持有鎖,或者鎖已經過期,不用解鎖!");
}
// return false;
}
RedisLock完整程式碼:
package com.th.order;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import redis.clients.jedis.Jedis;
public class RedisLock implements Lock {
private Jedis jedis;
private String requestId; // 鎖的value 用於解鎖的時候 比較是否自己獲得鎖
private int expireTime; // 設定自動過期時間
private static final String LOCK_KEY = "lock_key";
private static final String SET_IF_NOT_EXIST = "NX"; // 如果key不存在,才set
// 否則不做任何操作
private static final String SET_WITH_EXPIRE_TIME = "PX";// 設定過期
private static final String LOCK_SUCCESS = "OK";// redis 加鎖成功後返回的值
private static final Long RELEASE_SUCCESS = 1L;// 解鎖成功後返回的值
RedisLock(String ip, int port, String requestId, int expireTime) {
this.jedis = new Jedis(ip, port);
this.requestId = requestId;
this.expireTime = expireTime;
}
@Override
public void lock() {
if (!tryLock()) {
// 沒有獲得鎖,阻塞自己
waitForLock();
// 再次嘗試加鎖
lock();
}
}
@Override
public boolean tryLock() {
String result = jedis.set(LOCK_KEY, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result))
return true;
return false;
}
private void waitForLock() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void unlock() {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
// return true;
System.out.println("解鎖成功!");
} else {
System.out.println("不是自己持有鎖,或者鎖已經過期,不用解鎖!");
}
// return false;
}
@Override
public void lockInterruptibly() throws InterruptedException {
// TODO Auto-generated method stub
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// TODO Auto-generated method stub
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
OrderServiceImplWithZkDis測試類:
package com.th.order;
import java.util.UUID;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.locks.Lock;
public class OrderServiceImplWithZkDis implements OrderService {
private static OrderCodeGenerator org = new OrderCodeGenerator();
// private Lock lock = new ZookeeperDisLock("/LOCK_TEST");
// private Lock lock = new ZookeeperReAbleDisLock("/LOCK_TEST");
// private Lock lock = new DbLock(new User("1","張三丰"));
private Lock lock = new RedisLock("192.168.1.118", 6379, UUID.randomUUID().toString(), 1000);
@Override
public void createOrder() {
String orderCode = null;
try {
lock.lock();
orderCode = org.getOrderCode();
// TestReLock();
System.out.println(Thread.currentThread().getName() + "生成訂單:" + orderCode);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void TestReLock() {
lock.lock();
System.out.println(Thread.currentThread().getName() + "測試重入鎖成功...");
lock.unlock();
}
public static void main(String[] args) {
int num = 20;
CyclicBarrier cyclicBarrier = new CyclicBarrier(num);
for (int i = 0; i < num; i++) {
new Thread(new Runnable() {
@Override
public void run() {
OrderService orderService = new OrderServiceImplWithZkDis();
System.out.println(Thread.currentThread().getName() + ": 我準備好了");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
orderService.createOrder();
}
}).start();
}
}
}