github地址: Redis和ZooKeeper對於分佈式鎖的實現
Redis分佈式鎖
客戶端在讀寫redis之前必須先從redis獲取鎖, 只有獲取到鎖的客戶端才能讀寫redis, 而其他沒有獲取到鎖的客戶端, 會以每秒一次的頻率不斷地去嘗試獲取鎖.
(1) 獲取鎖
SET my_lock 隨機值 PX 5000 NX
PX是設置過期時間, 單位毫秒. NX是僅當key不存在時才設置值.
(2) 刪除鎖
只有提供的value值相同才能刪除鎖, 因爲我們不能讓客戶端刪除別人的鎖. 因爲涉及到條件判斷, 爲了保證事務特性, 必須使用Lua腳本.
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
(3) Java實例
public class RedisDistributeLock {
private static Logger logger = LoggerFactory.getLogger(RedisDistributeLock.class);
private Jedis jedis;
public RedisDistributeLock(String host) {
JedisPool jedisPool = new JedisPool(host, 6379);
jedis = jedisPool.getResource();
}
/**
* 獲取鎖
* @param key
* @param value
* @param expireTime 過期時間, 單位毫秒
*/
public void getLock(String key, String value, long expireTime) {
try {
SetParams params = new SetParams();
params.px(expireTime);
params.nx();
while (true) {
// 舊版本的Jedis使用命令: String result = jedis.set(key, value, "NX", "PX", 100);
String result = jedis.set(key, value, params);
if ("OK".equals(result)) {
return;
}
Thread.sleep(100L);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 釋放鎖
* @param key
* @param value
*/
public void releaseLock(String key, String value) {
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
RedisDistributeLock redisDistributeLock = new RedisDistributeLock("xxx.xx.xx.xxx");
//獲取鎖, 沒有獲取到鎖就繼續嘗試獲取鎖
String key = "my_lock";
String value = UUID.randomUUID().toString();
redisDistributeLock.getLock(key, value, 200L);
try {
logger.info(Thread.currentThread().getName() + " 進行扣減庫存操作...");
} catch (Exception e) {
logger.error("處理業務邏輯報錯", e);
}finally {
//釋放鎖
redisDistributeLock.releaseLock(key, value);
}
}
});
}
}
}
ZooKeeper分佈式鎖
(1) 使用臨時節點實現
- 所有客戶端都去/exclusive_lock節點下創建臨時子節點/exclusive_lock/myLock.
- 只有一個客戶端能創建成功, 表示該客戶端拿到了鎖.
- 其他沒有創建成功的客戶端在/exclusive_lock/myLock節點上註冊一個監聽器
- 當獲取到鎖的客戶端宕機或正常完成業務邏輯後, 臨時節點/exclusive_lock/myLock會被刪除.
- 其他客戶端都會收到通知, 重新去創建臨時節點/exclusive_lock/myLock.
這種實現有兩個問題: 羊羣效應和鎖公平性問題, 即每次當臨時節點被刪除後, 其他客戶端都會去獲取鎖, 且上一次獲取鎖的順序無效.
(2) 使用臨時順序節點實現
我們可以ZooKeeper的臨時順序節點來解決上面的兩個問題.
- 所有客戶端都去/exclusive_lock節點下創建臨時順序子節點/exclusive_lock/myLock.
- 然後再對這些臨時順序節點按字典序進行排序.
- 排在第一個的臨時順序節點對應的客戶端獲取到鎖.
- 其他客戶端在排自己前面的臨時順序節點上註冊一個監聽器.
- 當獲取到鎖的客戶端的釋放鎖之後, ZK會通過監聽器通知下一個臨時順序節點對應的客戶端獲取到鎖.
下面我們來看下具體實現:
public class ZkDistributeLock {
private static Logger logger = LoggerFactory.getLogger(ZkDistributeLock.class);
/**
* 分佈式鎖的根節點路徑
*/
private String rootLockPath = "/exclusive_lock";
/**
* 分佈式鎖節點路徑
*/
private String lockPath;
/**
* 分佈式鎖名
*/
private String lockName;
private ZooKeeper zk;
/**
* 連接zk, 並創建分佈式鎖的根節點
* @param host zk服務地址
* @param lockName 分佈式鎖名
*/
public ZkDistributeLock(String host, String lockName) {
try {
CountDownLatch connectedSignal = new CountDownLatch(1);
zk = new ZooKeeper(host, 5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getState() == Event.KeeperState.SyncConnected) {
connectedSignal.countDown();
}
}
});
//因爲監聽器是異步操作, 要保證監聽器操作先完成, 即要確保先連接上ZooKeeper再返回實例.
connectedSignal.await();
//創建鎖的根節點(持久節點)
if (zk.exists(rootLockPath, false) == null) {
zk.create(rootLockPath, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//指定分佈式鎖節點路徑
this.lockName = lockName;
} catch (Exception e) {
logger.error("connect zookeeper server error.", e);
}
}
/**
* 獲取鎖
* 在業務中獲取到鎖後才能繼續往下執行, 否則堵塞, 直到獲取到鎖
*/
public void getLock() {
try {
//創建分佈式鎖的臨時順序節點
lockPath = zk.create(rootLockPath + "/" + lockName, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
//取出所有分佈式鎖的臨時順序節點, 然後排序
List<String> children = zk.getChildren(rootLockPath, false);
TreeSet<String> sortedChildren = new TreeSet<>();
for (String child : children) {
sortedChildren.add(rootLockPath + "/" + child);
}
//如果當前客戶端創建的順序節點是第一個, 則獲取到鎖
String firstNode = sortedChildren.first();
if (firstNode.equals(lockPath)) {
return;
}
//如果當前客戶端沒有獲取到鎖, 則在前一個臨時順序節點上加一個監聽器
String lowerNode = sortedChildren.lower(lockPath);
CountDownLatch latch = new CountDownLatch(1);
if (StringUtils.isBlank(lowerNode)) {
return;
}
Stat stat = zk.exists(lowerNode, new Watcher() {
@Override
public void process(WatchedEvent event) {
//當前一個臨時順序節點被刪除後, 當前客戶端就獲取到鎖(這樣就保證了鎖的公平性)
if (event.getType() == Event.EventType.NodeDeleted) {
latch.countDown();
}
}
});
if (stat != null) {
latch.await();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 釋放鎖
*/
public void releaseLock() {
try {
zk.delete(lockPath, -1);
} catch (Exception e) {
logger.error("release lock error.", e);
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
ZkDistributeLock zkDistributeLock = new ZkDistributeLock("xxx.xx.xx.xxx:2181", "myLock");
//獲取鎖, 沒有獲取到鎖就一直等待
zkDistributeLock.getLock();
try {
logger.info(Thread.currentThread().getName() + " 進行扣減庫存操作...");
} catch (Exception e) {
logger.error("處理業務邏輯報錯", e);
}finally {
//釋放鎖
zkDistributeLock.releaseLock();
}
}
});
}
}
}