分散式鎖中-基於 Redis 的實現需避坑 - Jedis 篇
一、redis 介紹
Redis 應該是目前最受歡迎的高效能的快取資料庫了,不由得感慨 Reids 發展之迅速。蒐集了一下 3.0 及之後各版本的知名特性,整理出來方便讀者朋友們有個簡單瞭解(感興趣的朋友還需自行深入研究),情況大致如下:
3.0 開始支援 cluster 叢集模式 4.0 開發的 lazyfree 和 PSYNC2 解決了 Redis 長久的大 key 刪除阻塞問題及同步中斷無法續傳的問題 5.0 新增了 stream 資料結構使 Redis 具備功能完整的輕量級訊息佇列能力 6.0 更是釋出了諸多企業級特性如 threaded-io、TLS 和 ACL 等,大幅提升了 Redis 的效能和安全性 7.0 Function 徹底解決了過去 Lua 指令碼同步丟失的問題;Multi Part AOF 增強了 Redis 的資料持久化的可靠性
1.1 特性介紹
為滿足本篇目標所需,這裡著重介紹以下幾個關鍵特性:
資料組織:Redis 中支援多種資料結構,將他們靈活組合搭配即可滿足分散式鎖在不同場景下的功能需求: Jedis 和 Lettuce 這類框架中常使用 String 來做簡易的鎖資訊儲存 Redisson 中使用 Hash 結構來儲存更多維度的鎖資訊,如:業務名稱作為 key,uuid + 執行緒 id 作為 field,加鎖次數作為 value Redisson 中在公平鎖的場景下引入 List 和 ZSet, List 型別用於執行緒排隊,Zset 型別存放等待執行緒的順序,分數 score 是等待執行緒的超時時間戳。
Redis 的資料結構(來自網路)
叢集模式:Redis 採用叢集模式分片儲存資料,整個叢集擁有固定的 2 的 32 次方個槽位,資料被分配到這些槽位中,每個例項只分管一部分槽位,而非如 etcd、ZK 這種每個例項中的資料都一致;叢集模式提供的是資料規模擴大後的橫向 AP 能力,應對單節點的風險需再加上主從模式,但當某個 master 節點掛之後,slave 節點可能還未同步到全部資料,會導致資料丟失;一致性保障能力偏弱
Redis 的叢集模式(來自網路)
順序變更:一種簡單的搶鎖邏輯是判斷 key 是否已存在,Redis 中沒有給變更操作附加順序資訊(如 etcd 中的 Revision),但服務端以序列方式處理資料的變更,那就可以結合其他資料結構來記錄請求順序資訊,如公平鎖的實現也會依賴其他資料結構儲存資訊,用於判斷鎖狀態;但當用到的資料型別和指令變多後,由於是非原子性操作,自然就會遇到結果與預期不一致這類問題,Redis 提供的 lua 指令碼機制可用於解決此類問題 ,使用者在客戶端編排自定義指令碼邏輯:可用多個指令操控多個資料,然後將指令碼傳送給服務端,服務端執行 lua 指令碼,並保障一個 lua 指令碼內的所有操作是原子性的
Redis lua 指令碼的工作機制(來自網路)
TTL 機制:TTL(Time To Live)機制是給單個 key 設定存活時間,超過時間後 Redis 自動刪除這個 key
Redis 的分散式鎖正是基於以上特性來實現的,簡單來說是:
TTL 機制:用於支撐異常情況下的鎖自動釋放的能力
順序變更:用於支撐獲取鎖和排隊等待的能力
叢集+主從模式:用於支撐鎖服務的高可用
Redis 沒有提供對分散式鎖親和的監聽機制,需要客戶端主動輪詢感知資料變更。
二. 加鎖解鎖的流程描述
使用 Jedis 指令實現分散式鎖的核心流程如下圖所示
準備客戶端、key 和 value
若 key 不存在,指定過期時間成功寫入 Key-Value 則搶鎖成功,並定時推後 key 的過期時間
若 key 已存在,則採用重試策略間歇性搶鎖。
解鎖時,刪除 key 並撤銷推後 key 過期時間的邏輯
其中第 2 和第 4 是核心環節,有幾個版本的演進很有趣味:
插入 key 和設定過期時間並非原子操作:setnx + expire 加鎖和設定過期是兩個分開的獨立操作;若發生異常,導致設定過期操作未執行,則此鎖就成了永恆鎖,其他客戶端就再也搶不到了
以原子性操作完成插入 key 和設定過期時間:使用 set 的擴充套件指令,如下:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
NX :當 key 不存在時,才插入 Key XX :當插入 key 時,指定值為固定的 lockValue EX second :設定 key 的過期時間單位秒(PX\EX 二選一) PX millisecond :設定鍵的過期時間單位毫秒(PX\EX 二選一)
if(jedis.set(key, lockValue, "NX", "EX", 100) == 1){ //加鎖成功
try {
do work //執行業務
//這裡缺點什麼?
}catch(Exception e){
//...
}finally {
jedis.del(key); //釋放鎖,這裡可能誤刪其他client的鎖key
}
}
引入 lockValue 的隨機值校驗,避免誤釋放其它客戶端的鎖,場景如下:
client1 加鎖成功,key 10s 後過期,完成邏輯後,刪除 key 之前,因 GC 導致持鎖超過 10s,Redis 自動刪除了 key,之後其他客戶端可以搶鎖 假如是 client2 接下來成功搶鎖,開始處理持鎖後的邏輯。而此時 client1 GC 結束了會繼續執行刪除 key 的操作,但此時釋放的其實是 client2 的 key
解決辦法是:加鎖時指定的 lockValue 為隨機值,每次加鎖時的值都是唯一的,釋放鎖時若 lockValue 與加鎖時的值一致才可釋放,否則什麼都不做,邏輯如下:
if(jedis.set(key, randomLockValue, "NX", "EX", 100) == 1){ //加鎖
try {
do something //業務處理
}catch(){
}
finally {
//判斷是不是當前執行緒加的鎖,是才釋放
//但判斷和釋放鎖兩個操作不是原子性的
if (randomLockValue.equals(jedis.get(key))) {
jedis.del(key); //釋放鎖
}
}
}
以上程式碼遺留的問題是判斷 randomlockValue 和釋放鎖兩個操作不是原子性的。
引入 lua 指令碼,保障判斷 randomlockValue 和刪除 key 這兩個操作的原子性,邏輯如下:
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
Object result = jedis.eval(script, Collections.singletonList(key),
Collections.singletonList(randomLockValue));
if("1".equals(result.toString())){
return true;
}
至此依然存在的一個問題是:若持鎖後,業務邏輯執行耗時 超過了 key 的過期時間,則鎖 Key 會被 Reids 主動刪除。
引入 watchDog 定時推後 key 的過期時間,避免業務未執行完時,key 過期被 Redis 刪除。
if(jedis.set(key, randomLockValue, "NX", "EX", 100) == 1){ //加鎖成功
try {
do work //執行業務
//watchDog定時延後Key的過期時間
}catch(Exception e){
//...
}finally {
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
try {
Object result = jedis.eval(script, Collections.singletonList(key),
Collections.singletonList(randomLockValue));
if("1".equals(result.toString())){
return true;
}
return false;
}catch(Exception e){
//...
}
}
}
三. Jedis 分散式鎖的能力
一個分散式鎖應具備這樣一些功能特點:
互斥性:在同一時刻,只有一個客戶端能持有鎖
安全性:避免死鎖,如果某個客戶端獲得鎖之後處理時間超過最大約定時間,或者持鎖期間發生了故障導致無法主動釋放鎖,其持有的鎖也能夠被其他機制正確釋放,並保證後續其它客戶端也能加鎖,整個處理流程繼續正常執行
可用性:也被稱作容錯性,分散式鎖需要有高可用能力,避免單點故障,當提供鎖的服務節點故障(當機)時不影響服務執行,這裡有兩種模式:一種是分散式鎖服務自身具備叢集模式,遇到故障能自動切換恢復工作;另一種是客戶端向多個獨立的鎖服務發起請求,當某個鎖服務故障時仍然可以從其他鎖服務讀取到鎖資訊(Redlock)
可重入性:對同一個鎖,加鎖和解鎖必須是同一個執行緒程,即不能把其他執行緒持有的鎖給釋放了
高效靈活:加鎖、解鎖的速度要快;支援阻塞和非阻塞;支援公平鎖和非公平鎖
表格中標題使用 Redis-簡單鎖,主要是跟 RedLock 做區分,這種簡單鎖使用 Jedis 、Lettuce、Redisson 都能實現,任何一把鎖的資訊只儲存在一個 Redis master 例項中,而 RedLock 是 Redisson 提供的高階分散式鎖,它需要客戶端同時跟多個 Redis master 例項協作才能完成,即一把鎖的資訊同時存在於多個 master 例項中。
能力 | ZK | etcd | Redis-簡單鎖 | Redlock | MySql |
---|---|---|---|---|---|
互斥 | 是 | 是 | 是 | ||
安全 | 連結異常時,session 丟失自動釋放鎖 | 基於租約,超時自動釋放鎖 | 基於 TTL,超時自動釋放鎖 | ||
可用性 | 相對可用性還好 | 好 | 好 | ||
可重入 | 服務端非可重入,本地執行緒可重入 | 服務端非可重入,本地執行緒可重入需自研 | 服務端非可重入,本地執行緒可重入需自研 | ||
加解鎖速度 | 速度不算快 | 速度快,GRPC 協議優勢以及服務端能力的優勢 | 速度快 | ||
阻塞非阻塞 | 客戶端兩種能力都提供 | jetcd-core 中,阻塞非阻塞由 Future#get 支撐 | Jedis非阻塞,Redission提供阻塞能力 | ||
公平非公平 | 公平鎖 | 公平鎖 | 非公平鎖,Redission提供公平鎖 | ||
可續期 | 天然支援 | 天然支援 | Jedis需自研 watchDog,Redission自帶 | ||
其他因素 | 技術棧偏老,效能不佳 | 多數公司不熟悉 | 容易受業務快取操作干擾 |
四、Jedis 庫實現分散式鎖
Jedis 是 Redis 官方推出的用於透過 Java 連線 Redis 客戶端的一個工具包,提供了 Redis 的各種命令支援。
1.pom 依賴
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.0</version>
</dependency>
2. 相關的 API 介紹
使用 SET 的擴充套件指令加鎖(SET key value [EX seconds][px milliseconds] [NX|XX])
SetParams params = SetParams.setParams().nx().ex(lockState.getLeaseTTL());
String result = client.set(lockState.getLockKey(), lockState.getLockValue(), params);
使用 lua 解鎖
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = client.eval(script, 1, lockState.getLockKey(), lockState.getLockValue());
3. 分散式鎖示例
鎖的封裝
package com.rock.dlock.jedis;
import com.rock.dlock.common.DtLockException;
import com.rock.dlock.common.KeepAliveAction;
import com.rock.dlock.common.KeepAliveTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.JedisPooled;
import redis.clients.jedis.params.SetParams;
import java.net.SocketTimeoutException;
import java.util.concurrent.TimeUnit;
/**
* @author zs
* @date 2022/11/13 4:44 PM
*/
public class DemoJedisLock {
private final static Logger log = LoggerFactory.getLogger(DemoJedisLock.class);
private JedisPooled client;
private LockState lockState;
private KeepAliveTask keepAliveTask;
private int sleepMillisecond;
private final static String RESULT_OK = "OK";
private static final Long UNLOCK_SUCCESS = 1L;
class LockState {
private String lockKey;
private String lockValue;
private String errorMsg;
private int leaseTTL;
private long leaseId;
private boolean lockSuccess;
public LockState(String lockKey, int leaseTTL) {
this.lockKey = lockKey;
this.leaseTTL = leaseTTL;
}
public LockState(String lockKey, String value, int leaseTTL) {
this.lockKey = lockKey;
this.lockValue = value;
this.leaseTTL = leaseTTL;
}
public String getLockKey() {
return lockKey;
}
public void setLockKey(String lockKey) {
this.lockKey = lockKey;
}
public String getLockValue() {
return lockValue;
}
public void setLockValue(String lockValue) {
this.lockValue = lockValue;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
public long getLeaseId() {
return leaseId;
}
public void setLeaseId(long leaseId) {
this.leaseId = leaseId;
}
public boolean isLockSuccess() {
return lockSuccess;
}
public void setLockSuccess(boolean lockSuccess) {
this.lockSuccess = lockSuccess;
}
public int getLeaseTTL() {
return leaseTTL;
}
public void setLeaseTTL(int leaseTTL) {
this.leaseTTL = leaseTTL;
}
}
public DemoJedisLock(JedisPooled client, String key, String value, int ttlSeconds) {
//1.準備客戶端
this.client = client;
this.lockState = new LockState(key, value, ttlSeconds);
this.sleepMillisecond = (ttlSeconds * 1000) / 3; //搶鎖的重試間隔可由使用者指定
}
public boolean tryLock(long waitTime, TimeUnit waitUnit) throws DtLockException {
long totalMillisSeconds = waitUnit.toMillis(waitTime);
long start = System.currentTimeMillis();
//重試,直到成功或超過指定時間
while (true) {
// 搶鎖
try {
SetParams params = SetParams.setParams().nx().ex(lockState.getLeaseTTL());
String result = client.set(lockState.getLockKey(), lockState.getLockValue(), params);
if (RESULT_OK.equals(result)) {
manualKeepAlive();
log.info("[jedis-lock] lock success 執行緒:{} 加鎖成功,key:{} , value:{}", Thread.currentThread().getName(), lockState.getLockKey(), lockState.getLockValue());
lockState.setLockSuccess(true);
return true;
} else {
if (System.currentTimeMillis() - start >= totalMillisSeconds) {
return false;
}
Thread.sleep(sleepMillisecond);
}
} catch (Exception e) {
Throwable cause = e.getCause();
if (cause instanceof SocketTimeoutException) {//忽略網路抖動等異常
}
log.error("[jedis-lock] lock failed:" + e);
throw new DtLockException("[jedis-lock] lock failed:" + e.getMessage(), e);
}
}
}
//此實現中忽略,網路通訊異常部分的處理,可參考tryLock
public void unlock() throws DtLockException {
try {
// 首先停止續約
if (keepAliveTask != null) {
keepAliveTask.close();
}
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = client.eval(script, 1, lockState.getLockKey(), lockState.getLockValue());
if (UNLOCK_SUCCESS.equals(result)) {
log.info("[jedis-lock] unlock success 執行緒 : {} 解鎖成功,鎖key : {} ,路徑:{}", Thread.currentThread().getName(), lockState.getLockKey(), lockState.getLockValue());
} else {
log.info("[jedis-lock] unlock del key failed ,執行緒 : {} 解鎖成功,鎖key : {} ,路徑:{}", Thread.currentThread().getName(), lockState.getLockKey(), lockState.getLockValue());
}
} catch (Exception e) {
log.error("[jedis-lock] unlock failed:" + e.getMessage(), e);
throw new DtLockException("[jedis-lock] unlock failed:" + e.getMessage(), e);
}
}
// 定時將Key的過期推遲
private void manualKeepAlive() {
final String t_key = lockState.getLockKey();
final int t_ttl = lockState.getLeaseTTL();
keepAliveTask = new KeepAliveTask(new KeepAliveAction() {
@Override
public void run() throws DtLockException {
// 重新整理值
try {
client.expire(t_key, t_ttl);
} catch (Exception e) {
e.printStackTrace();
}
}
}, t_ttl);
keepAliveTask.start();
}
}
異常類的簡單實現
package com.rock.dlock.common;
public class DtLockException extends RuntimeException{
public DtLockException(String message) {
super(message);
}
public DtLockException(String message, Throwable cause) {
super(message, cause);
}
public static DtLockException clientException(){
return new DtLockException("client is empty");
}
}
watchDog 的任務抽象
package com.rock.dlock.common;
public interface KeepAliveAction {
void run() throws DtLockException;
}
watchDog 的簡單實現
package com.rock.dlock.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
/**
* @author zs
* @date 2022/11/7 4:20 PM
*/
public class KeepAliveTask extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(KeepAliveTask.class);
public volatile boolean isRunning = true;
/**
* 過期時間,單位s
*/
private long ttlSeconds;
private KeepAliveAction action;
public KeepAliveTask(KeepAliveAction action, long ttlSeconds) {
this.ttlSeconds = ttlSeconds;
this.action = action;
this.setDaemon(true);
}
@Override
public void run() {
final long sleep = this.ttlSeconds * 1000 / 3; // 每隔三分之一過期時間,續租一次
while (isRunning) {
try {
// 1、續租,重新整理值
action.run();
LOGGER.debug("續租成功!");
TimeUnit.MILLISECONDS.sleep(sleep);
} catch (InterruptedException e) {
close();
} catch (DtLockException e) {
close();
}
}
}
public void close() {
isRunning = false;
this.interrupt();
}
}
4. 測試鎖
import com.rock.dlock.jedis.DemoJedisLock;
import redis.clients.jedis.JedisPooled;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @author zs
* @date 2022/11/13 4:51 PM
*/
public class TestJedisLock {
public static void main(String[] args) {
JedisPooled jedis = new JedisPooled("127.0.0.1", 6379);
DemoJedisLock demoEtcdLock1 = new DemoJedisLock(jedis, "rock", UUID.randomUUID().toString(), 10);
DemoJedisLock demoEtcdLock2 = new DemoJedisLock(jedis, "rock", UUID.randomUUID().toString(), 10);
boolean lock1 = demoEtcdLock1.tryLock(20, TimeUnit.SECONDS);
if (lock1) {
try {
System.out.printf("do something");
} finally {
demoEtcdLock1.unlock();
}
}
demoEtcdLock1.tryLock(20, TimeUnit.SECONDS);
demoEtcdLock2.tryLock(20, TimeUnit.SECONDS);//等待鎖,超時後放棄
}
}
五、使用 Jedis 的一些注意事項
通常分散式鎖服務會和業務邏輯使用同一個Redis 叢集,自然也使用同一個 Jedis 客戶端;當業務邏輯側對 Redis 的讀寫併發提高時,會給 Redis 叢集和 Jedis 客戶度帶來壓力;為應對一些異常情況,我們除了解功能層面的 API,還需要了解一下客戶端的一些配置調優,主要是池化管理和網路通訊兩個方面
5.1 池化管理
在使用 Jedis 時可以配置 JedisPool 連線池,池化處理有許多好處,如:提高響應的速度、降低資源的消耗、方便管理和維護;JedisPool 配置引數大部分是由 JedisPoolConfig 的對應項來賦值的,在生產中我們需要關注它的配置併合理的賦值,如此能夠提升 Redis 的服務效能,降低資源開銷。下邊是對一些重要引數的說明、預設及設定建議:
引數 | 說明 | 預設值 | 建議 |
---|---|---|---|
maxTotal | 資源池中的最大連線數 | 8 | |
maxIdle | 資源池允許的最大空閒連線數 | 8 | |
minIdle | 資源池確保的最少空閒連線數 | 0 | |
blockWhenExhausted | 當資源池用盡後,呼叫者是否要等待。只有當值為 true 時,下面的maxWaitMillis才會生效。 | true | 建議使用預設值。 |
maxWaitMillis | 當資源池連線用盡後,呼叫者的最大等待時間(單位為毫秒)。 | -1(表示永不超時) | 不建議使用預設值。 |
testOnBorrow | 向資源池借用連線時是否做連線有效性檢測(ping)。檢測到的無效連線將會被移除。 | false | 業務量很大時候建議設定為 false,減少一次 ping 的開銷。 |
testOnReturn | 向資源池歸還連線時是否做連線有效性檢測(ping)。檢測到無效連線將會被移除。 | false | 業務量很大時候建議設定為 false,減少一次 ping 的開銷。 |
jmxEnabled | 是否開啟 JMX 監控 | true | 建議開啟,請注意應用本身也需要開啟。 |
空閒 Jedis 物件的回收檢測由以下四個引數組合完成,testWhileIdle是該功能的開關。
名稱 | 說明 | 預設值 | 建議 |
---|---|---|---|
testWhileIdle | 是否開啟空閒資源檢測。 | false | true |
timeBetweenEvictionRunsMillis | 空閒資源的檢測週期(單位為毫秒) | -1(不檢測) | 建議設定,週期自行選擇,也可以預設也可以使用下方JedisPoolConfig 中的配置。 |
minEvictableIdleTimeMillis | 資源池中資源的最小空閒時間(單位為毫秒),達到此值後空閒資源將被移除。 | 180000(即 30 分鐘) | 可根據自身業務決定,一般預設值即可,也可以考慮使用下方JeidsPoolConfig中的配置。 |
numTestsPerEvictionRun | 做空閒資源檢測時,每次檢測資源的個數。 | 3 | 可根據自身應用連線數進行微調,如果設定為 -1,就是對所有連線做空閒監測。 |
透過原始碼可以發現這些配置是 GenericObjectPoolConfig 物件的屬性,這個類實際上是 rg.apache.commons.pool2.impl apache 提供的,也就是說 jedis 的連線池是依託於 apache 提供的物件池來,這個物件池的宣告週期如下圖,感興趣的可以看下:
5.2 網路調優
max-redirects:這個是叢集模式下,重定向的最大數量;舉例說明,比如第一臺掛了,連第二臺,第二臺掛了連第三臺,重新連線的次數不能超過這個值
timeout:客戶端超時時間,單位是毫秒
Rsdis 節點故障或者網路抖動時,這兩個值如果不合理可能會導致很嚴重的問題,比如 timeout 設定為 1000,maxRedirect 為 2,一旦出現 redis 連線問題,將會導致請求阻塞 3s 左右。而這個 3 秒的阻塞在可能導致常規業務流量下的執行緒池耗盡,需根據業務場景調整。
五、總結
本篇介紹瞭如何基於 Redis 的特性來實現一個分散式鎖,並基於 Jedis 庫提供了一個分散式鎖的示例,呈現了其關鍵 API 的用法;此示例尚未達到生產級可用,如異常、可重入、可重試、超時控制等功能都未補全,計劃在下一篇介紹完 redlock 之後,再介紹一個健壯的分散式鎖客戶端要如何抽象設計,如何適配 ZK 、Redis 、etcd 。
參考和感謝
https://view.inews.qq.com/a/20220211A01JGQ00 https://cloud.tencent.com/developer/article/2052387
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2924363/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 基於redis的分散式鎖實現Redis分散式
- 基於redis實現分散式鎖Redis分散式
- 基於 Redis 實現簡單的分散式鎖Redis分散式
- 【進階篇】基於 Redis 實現分散式鎖的全過程Redis分散式
- 基於redis分散式鎖實現“秒殺”Redis分散式
- 基於Redis實現一個分散式鎖Redis分散式
- 分散式鎖與實現(一)基於Redis實現!分散式Redis
- 基於Redis的分散式鎖的簡單實現Redis分散式
- Redis避坑指南:為什麼要有分散式鎖?Redis分散式
- java 實現開箱即用基於 redis 的分散式鎖JavaRedis分散式
- 基於redis和zookeeper的分散式鎖實現方式Redis分散式
- 基於 Redis 的分散式鎖Redis分散式
- 基於redis的分散式鎖Redis分散式
- 基於 Redis 分散式鎖Redis分散式
- Golang 基於單節點 Redis 實現的分散式鎖GolangRedis分散式
- 基於redis做分散式鎖Redis分散式
- 基於AOP和Redis實現的簡易版分散式鎖Redis分散式
- 基於資料庫、redis和zookeeper實現的分散式鎖資料庫Redis分散式
- 基於 Zookeeper 的分散式鎖實現分散式
- redis分散式鎖的實現Redis分散式
- 基於ZK實現分散式鎖分散式
- 分散式鎖----Redis實現分散式Redis
- Redis實現分散式鎖Redis分散式
- [翻譯]基於redis的分散式鎖Redis分散式
- 手把手教你實現一個基於Redis的分散式鎖Redis分散式
- 【Redis】利用 Redis 實現分散式鎖Redis分散式
- 教你一招:基於Redis實現一個分散式鎖Redis分散式
- 基於Redis實現分散式鎖,Redisson使用及原始碼分析Redis分散式原始碼
- Redis 中的原子操作(3)-使用Redis實現分散式鎖Redis分散式
- NodeJS 基於redis的分散式鎖的實現(Redlock演算法)NodeJSRedis分散式演算法
- Redis之分散式鎖實現Redis分散式
- 分散式鎖之Redis實現分散式Redis
- redis分散式鎖-java實現Redis分散式Java
- Redis如何實現分散式鎖Redis分散式
- 分散式鎖實現(一):Redis分散式Redis
- 利用Redis實現分散式鎖Redis分散式
- redis分散式鎖-SETNX實現Redis分散式
- 使用 Redis 實現分散式鎖Redis分散式