分布式CAP理論
在介紹分布式鎖之前,先說一下CAP理論。因為現(xiàn)在提到分布式系統(tǒng)一定離不開CAP理論。C(Consistency)一致性、A(Availability)可用性、P(Partition tolerance)分區(qū)容錯性。三者不能同時存在,由于P是必要因素,所以分為CP和AP兩種模型。下面我們就根據(jù)AP和CP模型來分析一下分布式鎖以及使用場景。
AP模型的分布式鎖
AP模型的分布式鎖是基于Redis來實現(xiàn)的。Redis集群在分布式系統(tǒng)中是一種AP模型,無法保證在主節(jié)點宕機時自動完成數(shù)據(jù)一致性的同步操作,因此在業(yè)務(wù)要求保證一致性的場景中,Redis的分布式鎖會在主節(jié)點宕機的情況下丟失鎖信息而出現(xiàn)重復(fù)上鎖的極端情況。
Redis分布式鎖原理
- SETNX:Redis的分布式鎖主要是使用Redis的SETNX命令來完成上鎖的操作。此條命令的官方解釋為:只在鍵 key 不存在的情況下, 將鍵 key 的值設(shè)置為 value 。若鍵 key 已經(jīng)存在, 則 SETNX 命令不做任何動作。在設(shè)置成功時返回 1 , 設(shè)置失敗時返回 0 。
- expire:Redis支持key設(shè)置過期時間,因此我們在設(shè)計鎖的時候會設(shè)置一個過期時間來使得key有自動過期的機制。但是單純的只設(shè)置過期時間會有問題,在下一個小結(jié)介紹問題所在。
- 續(xù)租:上面提到了單純的設(shè)置過期時間會產(chǎn)生在持有鎖的期間內(nèi)邏輯沒有處理完而自動釋放鎖的問題。例如當前線程在獲得鎖后,設(shè)置了過期時間為1秒,但是由于某些原因?qū)е禄蛘呤谴a的bug使得此次代碼邏輯時間超過了1秒,這時導(dǎo)致鎖被釋放,而此時下一個線程重新獲得了鎖,導(dǎo)致最終業(yè)務(wù)受到影響,波及整個系統(tǒng)的數(shù)據(jù)問題。因此需要續(xù)租的機制來保證在當前線程沒有執(zhí)行完的時候不會自動釋放鎖,從而保證業(yè)務(wù)數(shù)據(jù)的安全。
Redis分布式鎖的實現(xiàn)
一、 基于jedis分布式鎖的實現(xiàn)
為了保證SETNX和expire的原子操作,可以通過redis的一條命令來完成。

上圖中的NX和XX參數(shù)介紹一下,NX為如果key不存在則設(shè)置一個value。XX為只在鍵已經(jīng)存在時, 才對鍵進行設(shè)置操作,XX為續(xù)租來做準備。此條命令可以省去了寫lua腳本來保證setnx和expire的原子性操作。
二、基于redisson分布式鎖的實現(xiàn)
redisson是redis的一個客戶端,封裝了基本的redis操作還有對分布式鎖的支持,redisson實現(xiàn)了自動續(xù)租的操作,上手更加容易,操作簡單。redisson的看門狗機制就是實現(xiàn)了對過期時間的自動續(xù)租功能,如果在業(yè)務(wù)中出現(xiàn)了死循環(huán)代碼或者是處理時間過長的問題,會導(dǎo)致看門狗無限續(xù)租的情況出現(xiàn),此時我們需要保證業(yè)務(wù)代碼的健壯性以及增加對鎖的監(jiān)控手段,避免線上出現(xiàn)死鎖問題導(dǎo)致排查困難。
Redis分布式鎖代碼
一、jedis的分布式鎖代碼,注解實現(xiàn)
@Around("lockPoint()")
public Object redisDistributedLock(ProceedingJoinPoint pjp) throws Throwable {
//獲取RedisLock注解信息
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
RedisLock lockInfo = method.getAnnotation(RedisLock.class);
String lockKey = lockInfo.value();
if (StringUtils.isBlank(lockKey)) {
throw new IllegalArgumentException("配置參數(shù)錯誤,lockKey不能為空!");
}
boolean lock = false;
Object obj = null;
try {
// 獲取鎖的最大超時時間
long maxSleepMills = System.currentTimeMillis() lockInfo.maxSleepMills();
while (!lock) {
//持鎖時間
String keepMills = String.valueOf(System.currentTimeMillis() lockInfo.keepMills());
//上鎖
lock = jedisService.setNX(lockKey, keepMills, lockInfo.keepMills());
// 得到鎖,沒有人加過相同的鎖
if (lock) {
logger.info("得到鎖...");
obj = pjp.proceed();
}
// 已過期,并且getAndSet后舊的時間戳依然是過期的,可以認為獲取到了鎖
else if (System.currentTimeMillis() > jedisService.get(lockKey) &&
(System.currentTimeMillis() > jedisService.getAndSet(lockKey, keepMills))) {
lock = true;
logger.info("得到鎖...");
obj = pjp.proceed();
}
// 沒有得到任何鎖
else {
// 繼續(xù)等待獲取鎖
if (lockInfo.action().equals(RedisLock.LockFailAction.CONTINUE)) {
// 如果超過最大等待時間拋出異常
logger.info("稍后重新請求鎖...");
if (lockInfo.maxSleepMills() > 0 && System.currentTimeMillis() > maxSleepMills) {
throw new TimeoutException("獲取鎖資源等待超時");
}
TimeUnit.MILLISECONDS.sleep(lockInfo.sleepMills());
} else {
// 放棄等待
logger.info("放棄鎖...");
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
// 如果獲取到了鎖,釋放鎖
if (lock) {
//鎖沒有過期就刪除key
if (System.currentTimeMillis() < (System.currentTimeMillis() lockInfo.keepMills())) {
logger.info("釋放鎖...");
jedisService.delete(lockKey);
}
}
}
return obj;
}
public boolean setNX(String key, String value, long time) {
boolean res = false;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
String set = jedis.set(key, value, NX, PX, time);
if (StringUtils.isBlank(set)) {
res = true;
}
} catch (Exception e) {
logger.error(e.getMessage());
} finally {
returnResource(jedis);
}
return res;
}
private void returnResource(Jedis jedis) {
if (jedis != null) {
jedisPool.returnResource(jedis);
}
}
一、redisson的分布式鎖代碼,注解實現(xiàn)
@Around("lockPoint()")
public Object redisDistributedLock(ProceedingJoinPoint pjp) throws Throwable {
//獲取Lock注解信息
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
RedissonLock lockInfo = method.getAnnotation(RedissonLock.class);
String lockKey = lockInfo.key();
if (StringUtils.isBlank(lockKey)) {
throw new IllegalArgumentException("配置參數(shù)錯誤,lockKey不能為空!");
}
long leaseTime = lockInfo.leaseTime();
long waitTime = lockInfo.waitTime();
TimeUnit unit = lockInfo.unit();
Object obj = null;
boolean lock = false;
RLock rLock = redissonClient.getLock(lockKey);
try {
//嘗試去上鎖
if (leaseTime > 0) {
//設(shè)置過期時間,到期自動釋放的鎖
lock = rLock.tryLock(waitTime, leaseTime, unit);
} else {
//不設(shè)置過期時間,看門狗自動續(xù)租的鎖
lock = rLock.tryLock(waitTime, unit);
}
if (lock && rLock.isHeldByCurrentThread()) {
logger.info("當前線程得到鎖...");
obj = pjp.proceed();
}
}catch (Exception e){
e.printStackTrace();
throw e;
}finally {
if (lock && rLock.isHeldByCurrentThread()) {
//當前線程是否持有此鎖,持有就刪除鎖
logger.info("釋放鎖...");
rLock.unlock();
}
}
return obj;
}
AP模式分布式鎖總結(jié)
以上是我對redis實現(xiàn)的分布式鎖的一些介紹。redis鎖的機制理解起來比較簡單,現(xiàn)有的redisson客戶端可以很好的支持分布式鎖的操作,也基本滿足了分布式鎖的場景需要。redis分布式鎖的最致命的問題就是無法保證數(shù)據(jù)的一致性,如果一旦主節(jié)點宕機,數(shù)據(jù)沒有同步到從節(jié)點中,會出現(xiàn)再次上鎖的問題,如果業(yè)務(wù)一定需要數(shù)據(jù)的一致性在高并發(fā)的場景下是不建議選擇redis鎖的實現(xiàn),可以選擇CP模型的zk或者etcd來實現(xiàn)分布式鎖。以上的例子以及代碼都是基于單機的redis來實現(xiàn)的,如有不足望大家指正。
PS:
在文章的最后為大家推薦一個公眾號《架構(gòu)之美》,上面會不定期的推薦一些技術(shù)文章,希望大家喜歡。
來源:https://www./content-2-711551.html
|