redis實現分佈式鎖
方法1:普通實現方案
實現方式: 使用指令: set key 隨機值 ex 5 nx.意思是當key不存在的時候設置key. 如果key存在返回OK,否則返回nil.
實現過程:
1.執行命令set key true ex 5 nx
2.如果成功,執行其他業務邏輯
3.使用lua腳本刪除key:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
爲什麼要設置過期時間?
防止加鎖的客戶端異常導致刪除key的命令沒執行,那麼就會產生死鎖。
爲什麼value要使用隨機值?
因爲設置了超時時間,如果獲得鎖的線程執行時間過長,導致key自動過期,然後被別的線程獲得鎖。 所以隨機值是爲了反正誤刪別的鎖。
未獲得鎖的處理: 客戶端在處理請求時加鎖沒加成功怎麼辦。一般有 3 種策略來處理加鎖失敗:
直接拋出異常,通知用戶稍後重試;
sleep 一會再重試;
將請求轉移至延時隊列,過一會再試;
總結:
這個方案的弊端:
1. 因爲如果業務邏輯執行時間過長導致自動過期,別的線程又獲得了鎖。可能會出現同一把鎖被兩個線程持有的情況。
2. 如果沒有獲得鎖,客戶端需要不斷重試,影響客戶端性能;
3. 在哨兵模式下,發生故障轉移時,由於主從之間是異步同步,可能獲得鎖的命令還沒有被同步到slave,剛好發生了故障轉移,也可能會出現同一把鎖被兩個線程持有的情況。
方法2:Redlock 算法
Redlock 算法是redis官方支持的分佈式鎖算法。 目的是就是爲了解決集羣故障轉移時可能發生的同一把鎖被兩個線程持有的問題。
總之這個算法比較複雜,不建議使用。。。。。
zookeeper實現分佈式鎖
方法1:多個客戶端監聽同一個節點
實現思路:
多個客戶端同時創建一個相同的臨時節點,zk可以保證一定只有一個客戶端創建成功,創建成功的就獲得了鎖。 創建不成功的客戶端監聽這個節點,如果監聽到節點刪除事件,那麼再次嘗試創建節點。
案例:利用分佈式鎖的思路實現master選舉。
/**
* 測試leader選舉
*
* @author leiqian
*
*/
public class MasterSelect {
private static Logger LOG = LoggerFactory.getLogger(MasterSelect.class);
/** zookeeper地址 */
static final String CONNECT_ADDR = "192.168.99.100:2181";
/** session超時時間 */
static final int SESSION_OUTTIME = 20000;// ms
static final String PATH = "/master_select";
static CountDownLatch SEMAPHORE;
public static void main(String[] args) throws InterruptedException {
// 1 重試策略:初試時間爲1s 重試10次
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 10);
// 2 通過工廠創建連接
CuratorFramework cf = CuratorFrameworkFactory.builder().connectString(CONNECT_ADDR)
.sessionTimeoutMs(SESSION_OUTTIME).retryPolicy(retryPolicy).build();
// 3 開啓連接
cf.start();
// 註冊監聽器
registerListener(cf);
// 開始選舉
select(cf);
Thread.sleep(Long.MAX_VALUE);
}
/**
* 註冊子節點監聽器
*
* @param cf
*/
public static void registerListener(CuratorFramework cf) {
// 監聽子節點
final PathChildrenCache childrenCache = new PathChildrenCache(cf, PATH, false);
try {
childrenCache.start(StartMode.POST_INITIALIZED_EVENT);
} catch (Exception e1) {
e1.printStackTrace();
}
childrenCache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
switch (event.getType()) {
case CHILD_ADDED:
// System.out.println("CHILD_ADDED," + event.getData().getPath());
break;
case CHILD_UPDATED:
// System.out.println("CHILD_UPDATED," + event.getData().getPath());
break;
case CHILD_REMOVED:
// System.out.println("CHILD_REMOVED," + event.getData().getPath());
// 再次開始參與選舉
SEMAPHORE.countDown();
break;
default:
break;
}
}
});
}
/**
* 進行選舉
*
* @param cf
* @throws InterruptedException
*/
public static void select(CuratorFramework cf) throws InterruptedException {
SEMAPHORE = new CountDownLatch(1);
try {
cf.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(PATH + "/lock");
LOG.info("master選舉成功");
work();
removeMaster(cf);
LOG.info("等待選舉");
SEMAPHORE.await();
LOG.info("下一次選舉開始");
select(cf);
} catch (Exception e) {
LOG.info("master選舉失敗");
LOG.info("等待選舉");
SEMAPHORE.await();
LOG.info("下一次選舉開始");
select(cf);
}
}
/**
* 移除master
*
* @param cf
*/
public static void removeMaster(CuratorFramework cf) {
try {
cf.delete().forPath(PATH + "/lock");
} catch (Exception e) {
e.printStackTrace();
}
LOG.info("邏輯完成,master退出");
}
/**
* 執行業務邏輯
*/
public static void work() {
LOG.info("開始執行業務邏輯");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
注意:curator框架的curator-recipes包中內置了master選舉,分佈式鎖,分佈式計數器,分佈式barrier的實現,這裏的代碼只是爲了演示思路。
方法2:利用順序臨時節點實現分佈式鎖
實現思路:
每個客戶端創建一個臨時順序節點,然後獲取所有的順序節點,如果當前節點排序後在第一個位置,那麼當前client獲得鎖。 如果不是在第一個位置,監聽前一個節點。當前一個刪除後,當前client獲得鎖。
/**
* 利用順序臨時節點實現分佈式鎖
*
* @author leiqian
*
*/
public class DistributedLock {
private static Logger LOG = LoggerFactory.getLogger(MasterSelect.class);
private CuratorFramework cf;
private String rootPath = "/dirtributed_lock";
private String path = rootPath + "/lock";
private CountDownLatch semaphore;
/** 當前節點 */
private String curNode;
/** 需要的監聽節點 */
private String lockNode;
private DistributedLock(String connectStr, int seesionTimeOut) {
// 1 重試策略:初試時間爲1s 重試10次
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 10);
// 2 通過工廠創建連接
cf = CuratorFrameworkFactory.builder().connectString(connectStr).sessionTimeoutMs(seesionTimeOut)
.retryPolicy(retryPolicy).build();
// 3 開啓連接
cf.start();
}
public static void main(String[] args) throws Exception {
// zookeeper地址
String connectStr = "192.168.99.100:2181";
// session超時時間
int seesionTimeOut = 20000;// ms
DistributedLock lock = new DistributedLock(connectStr, seesionTimeOut);
// 獲取分佈式鎖
lock.acquireLock();
Thread.sleep(Long.MAX_VALUE);
}
/**
* 註冊節點監聽器
*
* @param cf
*/
public void registerListener(String nodePath) {
// 監聽節點
NodeCache cache = new NodeCache(cf, nodePath, false);
try {
cache.start(true);
} catch (Exception e) {
e.printStackTrace();
}
cache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
semaphore.countDown();
}
});
}
private void acquireLock() throws Exception {
if (!tryLock()) {
LOG.info("等待鎖");
waitForLock();
}
LOG.info("獲得鎖..");
work();
releaseLock();
}
/**
* 嘗試獲取鎖
*
* @return
* @throws Exception
*/
private boolean tryLock() throws Exception {
this.curNode = cf.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(this.path);
PathAndNode pn = ZKPaths.getPathAndNode(this.curNode);
// 獲取父節點下面的所有子節點
List<String> childNodes = cf.getChildren().forPath(this.rootPath);
// 節點排序
Collections.sort(childNodes);
if (pn.getNode().equals(childNodes.get(0))) {
return true;
} else {
// 獲取前一個node
this.lockNode = this.rootPath + "/" + childNodes.get(childNodes.lastIndexOf(pn.getNode()) - 1);
return false;
}
}
/**
* 等待鎖
*
* @throws Exception
*/
private void waitForLock() throws Exception {
Stat stat = cf.checkExists().forPath(this.lockNode);
if (stat != null) {
semaphore = new CountDownLatch(1);
// 監聽節點
registerListener(lockNode);
semaphore.await();
}
}
/**
* 釋放鎖
*/
private void releaseLock() {
try {
LOG.info("釋放鎖");
cf.delete().forPath(this.curNode);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 執行業務邏輯
*/
private void work() {
LOG.info("執行業務邏輯");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
redis和zk實現分佈式鎖的優劣
1.redis分佈式鎖,其實需要自己不斷去嘗試獲取鎖,比較消耗性能;
zk分佈式鎖,獲取不到鎖,註冊個監聽器即可,不需要不斷主動嘗試獲取鎖,性能開銷較小
2. 如果是redis獲取鎖的那個客戶端bug了或者掛了,那麼只能等待超時時間之後才能釋放鎖;而zk的話,因爲創建的是臨時znode,只要客戶端掛了,znode就沒了,此時就自動釋放鎖
總得來說利用zk來實現分佈式鎖更好更安全。