[TOC]html
不少時候咱們須要保證同一時間一個方法只能被同一個線程調用,在單機環境中,Java中其實提供了不少併發處理相關的API,可是這些API在分佈式場景中就無能爲力了。也就是說單純的Java Api並不能提供分佈式鎖的能力。java
針對分佈式鎖的實現目前有多種方案:redis
直接建一張表,裏面記錄鎖定的方法名
時間
便可。
須要加鎖時,就插入一條數據,釋放鎖時就刪除數據。算法
CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '備註信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數據時間,自動生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';複製代碼
當咱們想要鎖住某個方法時,執行如下SQL:sql
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)複製代碼
由於咱們對method_name作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲
操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。
當方法執行完畢以後,想要釋放鎖的話,須要執行如下Sql:數據庫
delete from methodLock where method_name ='method_name'複製代碼
數據庫實現分佈式鎖的優勢: 直接藉助數據庫,容易理解。apache
數據庫實現分佈式鎖的缺點: 會有各類各樣的問題,在解決問題的過程當中會使整個方案變得愈來愈複雜。緩存
操做數據庫須要必定的開銷,性能問題須要考慮。安全
相比於用數據庫來實現分佈式鎖,基於緩存實現的分佈式鎖的性能會更好一些。bash
目前有不少成熟的分佈式產品,包括Redis、memcache、Tair等。
public Object around(ProceedingJoinPoint joinPoint) {
try {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
DLock dLock = method.getAnnotation(DLock.class);
if (dLock != null) {
String lockedPrefix = buildLockedPrefix(dLock, method, joinPoint.getArgs());
long timeOut = dLock.timeOut();
int expireTime = dLock.expireTime();
long value = System.currentTimeMillis();
if (lock(lockedPrefix, timeOut, expireTime, value)) {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
unlock(lockedPrefix, value);
}
} else {
recheck(lockedPrefix, expireTime);
}
}
} catch (Exception e) {
logger.error("DLockAspect around error", e);
}
return null;
}
/** * 檢查是否設置過超時 * * @param lockedPrefix * @param expireTime */
public void recheck(String lockedPrefix, int expireTime) {
try {
Result<Long> ttl = cacheFactory.getFactory().ttl(getLockedPrefix(lockedPrefix));
if (ttl.isSuccess() && ttl.getValue() == -1) {
Result<String> get = cacheFactory.getFactory().get(getLockedPrefix(lockedPrefix));
//沒有超時設置則設置超時
if (get.isSuccess() && !StringUtils.isEmpty(get.getValue())) {
long oldTime = Long.parseLong(get.getValue());
long newTime = expireTime * 1000 - (System.currentTimeMillis() - oldTime);
if (newTime < 0) {
//已過超時時間 設默認最小超時時間
cacheFactory.getFactory().expire(getLockedPrefix(lockedPrefix), MIX_EXPIRE_TIME);
} else {
//未超過 設置爲剩餘超時時間
cacheFactory.getFactory().expire(getLockedPrefix(lockedPrefix), (int) newTime);
}
logger.info(lockedPrefix + "recheck:" + newTime);
}
}
logger.info(String.format("執行失敗lockedPrefix:%s count:%d", lockedPrefix, count++));
} catch (Exception e) {
logger.error("DLockAspect recheck error", e);
}
}
public boolean lock(String lockedPrefix, long timeOut, int expireTime, long value) {
long millisTime = System.currentTimeMillis();
try {
//在timeOut的時間範圍內不斷輪詢鎖
while (System.currentTimeMillis() - millisTime < timeOut * 1000) {
//鎖不存在的話,設置鎖並設置鎖過時時間,即加鎖
Result<Long> result = cacheFactory.getFactory().setnx(getLockedPrefix(lockedPrefix), String.valueOf(value));
if (result.isSuccess() && result.getValue() == 1) {
Result<Long> result1 = cacheFactory.getFactory().expire(getLockedPrefix(lockedPrefix), expireTime);
logger.info(lockedPrefix + "locked and expire " + result1.getValue());
return true;
}
//短暫休眠,避免可能的活鎖
Thread.sleep(100, RANDOM.nextInt(50000));
}
} catch (Exception e) {
logger.error("lock error " + getLockedPrefix(lockedPrefix), e);
}
return false;
}
public void unlock(String lockedPrefix, long value) {
try {
Result<String> result = cacheFactory.getFactory().get(getLockedPrefix(lockedPrefix));
String kvValue = result.getValue();
if (!StringUtils.isEmpty(kvValue) && kvValue.equals(String.valueOf(value))) {
cacheFactory.getFactory().del(getLockedPrefix(lockedPrefix));
}
logger.info(lockedPrefix + "unlock:" + kvValue + "----" + value);
} catch (Exception e) {
logger.error("unlock error" + getLockedPrefix(lockedPrefix), e);
}
}複製代碼
Redlock是Redis的做者antirez給出的集羣模式的Redis分佈式鎖,它基於N個徹底獨立的Redis節點(一般狀況下N能夠設置成5)。
客戶端1成功鎖住了A, B, C,獲取鎖成功(但D和E沒有鎖住);節點C崩潰重啓了,但客戶端1在C上加的鎖沒有持久化下來,丟失了;節點C重啓後,客戶端2鎖住了C, D, E,獲取鎖成功。客戶端1和客戶端2同時得到了鎖(針對同一資源)。
這個問題能夠延遲節點的恢復時間,時間長度應大於等於一個鎖的過時時間。
關於RedLock的更多內容能夠看:
一個比較好的實現:
無單點問題。ZK是集羣部署的,只要集羣中有半數以上的機器存活,就能夠對外提供服務。
持有鎖任意長的時間,可自動釋放鎖。使用Zookeeper能夠有效的解決鎖沒法釋放的問題,由於在建立鎖的時候,客戶端會在ZK中建立一個臨時節點,一旦客戶端獲取到鎖以後忽然掛掉(Session鏈接斷開),那麼這個臨時節點就會自動刪除掉。其餘客戶端就能夠再次得到鎖。這避免了基於Redis的鎖對於有效時間(lock validity time)到底設置多長的兩難問題。實際上,基於ZooKeeper的鎖是依靠Session(心跳)來維持鎖的持有狀態的,而Redis不支持Sesion。
可阻塞。使用Zookeeper能夠實現阻塞的鎖,客戶端能夠經過在ZK中建立順序節點,而且在節點上綁定監聽器,一旦節點有變化,Zookeeper會通知客戶端,客戶端能夠檢查本身建立的節點是否是當前全部節點中序號最小的,若是是,那麼本身就獲取到鎖,即可以執行業務邏輯了。
可重入。客戶端在建立節點的時候,把當前客戶端的主機信息和線程信息直接寫入到節點中,下次想要獲取鎖的時候和當前最小的節點中的數據比對一下就能夠了。若是和本身的信息同樣,那麼本身直接獲取到鎖,若是不同就再建立一個臨時的順序節點,參與排隊。
羊羣效應
,從而下降鎖的性能。一個比較好的實現:
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
try {
return interProcessMutex.acquire(timeout, unit);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
public boolean unlock() {
try {
interProcessMutex.release();
} catch (Throwable e) {
log.error(e.getMessage(), e);
} finally {
executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
}
return true;
}複製代碼
acquire方法用戶獲取鎖,release方法用於釋放鎖。
使用Zookeeper實現分佈式鎖的優勢: 有效的解決單點問題,不可重入問題,非阻塞問題以及鎖沒法釋放的問題。實現起來較爲簡單。
使用Zookeeper實現分佈式鎖的缺點 : 性能上不如使用緩存實現分佈式鎖。 須要對ZK的原理有所瞭解。
從理解的難易程度角度(從低到高): 數據庫 > 緩存 > Zookeeper
從實現的複雜性角度(從低到高): Zookeeper >= 緩存 > 數據庫
從性能角度(從高到低): 緩存 > Zookeeper >= 數據庫
從可靠性角度(從高到低): Zookeeper > 緩存 > 數據庫