此文爲林淮川(鄙人)和玄姐(轉轉首席架構師)共同編寫,發佈在csdn和 架構之美工作號上。
https://blog.csdn.net/jianxian89/article/details/103584137 轉載請保留!
1. 分佈式鎖本質
提到分佈式鎖,有很多實現,比如Redis分佈式鎖、ZooKeeper分佈式鎖、etcd分佈式鎖等。但是選擇哪個更適合你的項目?在《基於CAP模型設計企業級真正高可用的分佈式鎖》一文深入分析過分佈式鎖的哲學本質,以及如何結合場景來選擇合適的分佈式鎖。分析業務場景,得到業務本質,就是架構思維。思維最終是需要落地的,接下去分享一下對分佈式鎖的思考和實踐。。
鎖的本質是對共享資源的處理,表現很多,有以下作用:
- 業務協調
- 業務冪等(需配合業務代碼實現)
- 共享資源競爭
在單體應用時代表現爲的就是同步塊,lock。隨着需求和業務量的增長,系統走向了分佈式、微服務時代,多服務/多實例下的應用無法使用本地鎖進行控制資源共享。這時就出現了分佈式鎖,我分析了分佈式場景下的情況,可以歸納出分佈式鎖的要求:
- 強一致性
- 服務高可用、系統穩健
- 鎖自動續約、自動釋放
- 業務可重入
2. 分佈式鎖存儲選型及場景
目前網上常見分佈式鎖的實現有redis、zookeeper、etcd,先來個總體比較。
redis | zookeeper | etcd | |
一致性算法 | 無 | ZAB(paxos自有實現) | raft |
CAP | AP | CP | CP |
高可用 | 主從 | N+1可用 | N+1可用 |
實現 | setNX | createEphemeral | http/grpc |
接下來,咱們對比較指標進行一一解釋:
- 一致性算法(CAP):在分佈式場景下,CAP理論是很多設計的指導思想。CAP思想下有兩個分支,cp與ap;cp:不管啥情況下,都要求各服務之間的數據一致;AP:高可用下的數據最終一致性。雖然鎖原本要求強一致性,但基於CAP理論也對AP有了一定的容忍度。AP分佈式鎖的使用取決於 業務場景對 髒數據的最大容忍度,比如SNS場景,這時AP鎖存在,從性能上有很大的優勢。CP仍然保持原有的一致性要求,保證了業務資源串行競爭,更加適合於金融交易場景的強數據要求。redis自身無一致性算法來保證多節點的數據一致性,所以是AP模式;zk、etcd都有一致性算法,所以都是CP模式。
- 高可用:redis是一個k-v的存儲;使用主從模式進行集羣,redis-cluster 底層也是主從模式的組合;性能高,也保證了靠可用。zk是tree的數據結構,節點要求N+1,N必須大於2;通過 ZAB選舉 保障主的可用。etcd是一個k-v的存儲,節點要求N+1,N必須大於2;通過 raft選舉 保障主的可用存在。
3. 分佈式鎖接口設計
根據要求,可以設計出鎖接口,首先鎖的基本方法:
/**
* @方法名稱 lock
* @功能描述 <pre>獲取鎖</pre>
* @param ttl 鎖過期時間,單位毫秒
* @return true-獲取鎖,false-未獲得鎖
* @throws RuntimeException 操作鎖失敗,需要業務判斷是否重試
*/
boolean lock(int ttl)
throws RuntimeException;
因爲分佈式鎖可以處理業務冪等,可用作爲消息去重等場景,所以設計競爭鎖方法
/**
* @方法名稱 acquire
* @功能描述 <pre>競爭鎖,並自動續租</pre>
* @param ttl 鎖過期時間,單位毫秒
* @return true-獲取鎖,false-未獲得鎖
* @throws RuntimeException 操作鎖失敗,需要業務判斷是否重試
*/
default boolean acquire(int ttl)
throws RuntimeException {
if (lock(ttl)) {
logger.debug(MSG_LOCK, getName());
startHeartBeatThread();
return true;
}
return false;
}
其次作爲鎖的基本要求,業務的串行執行,所以設計等待鎖方法
/**
* @方法名稱 acquireOrWait
* @功能描述 <pre>競爭鎖或等待鎖</pre>
* @param ttl 鎖過期時間,單位毫秒
* @param waitTime 等待時間,單位毫秒
* @return true-獲取鎖,false-未獲得鎖
* @throws InterruptedException
* @throws RuntimeException 操作鎖失敗,需要業務判斷是否重試
*/
default boolean acquireOrWait(int ttl, int waitTime)
throws InterruptedException, RuntimeException {
while (!lock(ttl)) {
waitTime = waitTime - ttl / 2;
Thread.sleep(ttl / 2);
if (waitTime <= 0) {
logger.debug(MSG_LOCK_TIMEOUT, getName());
return false;
}
}
startHeartBeatThread();
return true;
}
因爲是分佈式場景,所以鎖需要存在自動續租方法,保障鎖內業務的完整執行。
/**
* @方法名稱 startHeartBeatThread
* @功能描述 <pre>續租心跳</pre>
*/
void startHeartBeatThread();
最後,鎖需要自動釋放,爲保證使用簡單,所以重寫Closeable接口:
/**
* @方法名稱 close
* @功能描述 <pre>釋放鎖</pre>
*/
@Override
void close();
/**
* @方法名稱 release
* @功能描述 <pre>釋放鎖</pre>
*/
default void release() {
close();
}
4. AP模型的Redis分佈式鎖實現
/**
* 加鎖腳本
*/
private static final String SCRIPT_LOCK = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end";
/**
* 解鎖腳本
*/
private static final String SCRIPT_UNLOCK = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";/**
* 加鎖腳本sha1值
*/
private static final String SCRIPT_LOCK_SHA1 = "8525317db2b346fffb9050797aee5fa8b8231872";
/**
* 解鎖腳本sha1值
*/
private static final String SCRIPT_UNLOCK_SHA1 = "e9f69f2beb755be68b5e456ee2ce9aadfbc4ebf4";@Override
public boolean lock(int ttl)
throws RuntimeException {
this.ttl = ttl;
// 鎖不存在時:上鎖並過期時間,最後跳出。
Long result = redisTemplate.execute(new RedisScript<Long>() {
@Override
public String getSha1() {
return SCRIPT_LOCK_SHA1;
}
@Override
public Class<Long> getResultType() {
return Long.class;
}
@Override
public String getScriptAsString() {
return SCRIPT_LOCK;
}
}, Collections.singletonList(lockKey), "1", String.valueOf(ttl));
if (SUCCESS.equals(result)) {
return true;
}
return false;
}
@Override
public void startHeartBeatThread() {
scheduledpool = new ScheduledThreadPoolExecutor(1, new NamingThreadFactory(Thread.currentThread().getName().concat(LOCK_HEART_BEAT), true));
scheduledpool.scheduleAtFixedRate(() -> {
redisTemplate.expire(lockKey, ttl, TimeUnit.MILLISECONDS);
logger.debug(MSG_RELET, Thread.currentThread().getName(), lockKey);
}, 0L, ttl / 3, TimeUnit.MILLISECONDS);
}
@Override
public void close() {
if (null != scheduledpool && redisTemplate.hasKey(lockKey)) {
try {
redisTemplate.execute(new RedisScript<Long>() {
@Override
public Class<Long> getResultType() {
return Long.class;
}
@Override
public String getScriptAsString() {
return SCRIPT_UNLOCK;
}
@Override
public String getSha1() {
return SCRIPT_UNLOCK_SHA1;
}
}, Collections.singletonList(lockKey), "1");
} catch (Exception e) {
logger.warn(WARN_MSG_EVAL, e.getMessage());
redisTemplate.delete(lockKey);
}
}
}
5. CP模型的etcd分佈式鎖實現
etcd有V2和V3兩種接口:v2接口可以使用http直接訪問,天然客戶端物理解耦,但需要自動續租保證鎖的完整性。v3接口默認grpc形式,是長鏈接機制,天然續租,但grpc有客戶端依賴要求。可以根據場景要求,適度選擇合適版本接口。
鎖參數有:
a、prevExits
檢查是否存在,true--新增,false--更新b、prevIndex
檢查上一個的key,既操作返回的uuid
c、prevValue
檢查上一個的值
curl鎖操作
a、取鎖
curl http://ip:port/v2/keys/鎖名 -XPUT -d ttl=10 -d prevExits=false -d value=鎖值b、續租
curl http://ip:port/v2/keys/鎖名?prevValue=鎖值 -XPUT -d ttl=3 -d prevExits=true -d refresh=true通過心跳線程,每隔 ttl/3時間,更新過期時間
c、放鎖
curl http://ip:port/v2/keys/鎖名?prevValue=鎖值 -XDELETE
6. CP模型的zk分佈式公平重入鎖
使用zk 的臨時順序節點,可以充當競鎖隊列,保障鎖的公平性,源碼基於原生zk-client進行實現。用