分散式鎖實現原理與最佳實踐
來源:阿里雲開發者
阿里妹導讀
一、超賣問題復現
1.1 現象
商品表
訂單表
訂單item表
(rollbackFor = Exception.class)
public Long createOrder() throws Exception {
Product product = productMapper.selectByPrimaryKey(purchaseProductId);
// ... 忽略校驗邏輯
//商品當前庫存
Integer currentCount = product.getCount();
//校驗庫存
if (purchaseProductNum > currentCount) {
throw new Exception("商品" + purchaseProductId + "僅剩" + currentCount + "件,無法購買");
}
// 計算剩餘庫存
Integer leftCount = currentCount - purchaseProductNum;
// 更新庫存
product.setCount(leftCount);
product.setGmtModified(new Date());
productMapper.updateByPrimaryKeySelective(product);
Order order = new Order();
// ... 省略 Set
orderMapper.insertSelective(order);
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(order.getId());
// ... 省略 Set
return order.getId();
}
(rollbackFor = Exception.class)
public Long createOrder() throws Exception {
Product product = productMapper.selectByPrimaryKey(purchaseProductId);
// ... 忽略校驗邏輯
//商品當前庫存
Integer currentCount = product.getCount();
//校驗庫存
if (purchaseProductNum > currentCount) {
throw new Exception("商品" + purchaseProductId + "僅剩" + currentCount + "件,無法購買");
}
// 使用 set count = count - #{purchaseProductNum,jdbcType=INTEGER}, 更新庫存
productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId());
Order order = new Order();
// ... 省略 Set
orderMapper.insertSelective(order);
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(order.getId());
// ... 省略 Set
return order.getId();
}
(rollbackFor = Exception.class)
public synchronized Long createOrder() throws Exception {
Product product = productMapper.selectByPrimaryKey(purchaseProductId);
// ... 忽略校驗邏輯
//商品當前庫存
Integer currentCount = product.getCount();
//校驗庫存
if (purchaseProductNum > currentCount) {
throw new Exception("商品" + purchaseProductId + "僅剩" + currentCount + "件,無法購買");
}
// 使用 set count = count - #{purchaseProductNum,jdbcType=INTEGER}, 更新庫存
productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId());
Order order = new Order();
// ... 省略 Set
orderMapper.insertSelective(order);
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(order.getId());
// ... 省略 Set
return order.getId();
}
1.2 解決辦法
單體應用:使用本地鎖 + 資料庫中的行鎖解決
分散式應用:
使用資料庫中的樂觀鎖,加一個 version 欄位,利用CAS來實現,會導致大量的 update 失敗
使用資料庫維護一張鎖的表 + 悲觀鎖 select,使用 select for update 實現
使用Redis 的 setNX實現分散式鎖
使用zookeeper的watcher + 有序臨時節點來實現可阻塞的分散式鎖
使用Redisson框架內的分散式鎖來實現
使用curator 框架內的分散式鎖來實現
二、單體應用解決超賣的問題
正確示例:將事務包含在鎖的控制範圍內
//@Transactional(rollbackFor = Exception.class)
public synchronized Long createOrder() throws Exception {
TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);
Product product = productMapper.selectByPrimaryKey(purchaseProductId);
if (product == null) {
platformTransactionManager.rollback(transaction1);
throw new Exception("購買商品:" + purchaseProductId + "不存在");
}
//商品當前庫存
Integer currentCount = product.getCount();
//校驗庫存
if (purchaseProductNum > currentCount) {
platformTransactionManager.rollback(transaction1);
throw new Exception("商品" + purchaseProductId + "僅剩" + currentCount + "件,無法購買");
}
productMapper.updateProductCount(purchaseProductNum, new Date(), product.getId());
Order order = new Order();
// ... 省略 Set
orderMapper.insertSelective(order);
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(order.getId());
// ... 省略 Set
return order.getId();
platformTransactionManager.commit(transaction1);
}
正確示例:使用synchronized的程式碼塊
public Long createOrder() throws Exception {
Product product = null;
//synchronized (this) {
//synchronized (object) {
synchronized (DBOrderService2.class) {
TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);
product = productMapper.selectByPrimaryKey(purchaseProductId);
if (product == null) {
platformTransactionManager.rollback(transaction1);
throw new Exception("購買商品:" + purchaseProductId + "不存在");
}
//商品當前庫存
Integer currentCount = product.getCount();
System.out.println(Thread.currentThread().getName() + "庫存數:" + currentCount);
//校驗庫存
if (purchaseProductNum > currentCount) {
platformTransactionManager.rollback(transaction1);
throw new Exception("商品" + purchaseProductId + "僅剩" + currentCount + "件,無法購買");
}
productMapper.updateProductCount(purchaseProductNum, new Date(), product.getId());
platformTransactionManager.commit(transaction1);
}
TransactionStatus transaction2 = platformTransactionManager.getTransaction(transactionDefinition);
Order order = new Order();
// ... 省略 Set
orderMapper.insertSelective(order);
OrderItem orderItem = new OrderItem();
// ... 省略 Set
orderItemMapper.insertSelective(orderItem);
platformTransactionManager.commit(transaction2);
return order.getId();
正確示例:使用Lock
private Lock lock = new ReentrantLock();
public Long createOrder() throws Exception{
Product product = null;
lock.lock();
TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);
try {
product = productMapper.selectByPrimaryKey(purchaseProductId);
if (product==null){
throw new Exception("購買商品:"+purchaseProductId+"不存在");
}
//商品當前庫存
Integer currentCount = product.getCount();
System.out.println(Thread.currentThread().getName()+"庫存數:"+currentCount);
//校驗庫存
if (purchaseProductNum > currentCount){
throw new Exception("商品"+purchaseProductId+"僅剩"+currentCount+"件,無法購買");
}
productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId());
platformTransactionManager.commit(transaction1);
} catch (Exception e) {
platformTransactionManager.rollback(transaction1);
} finally {
// 注意拋異常的時候鎖釋放不掉,分散式鎖也一樣,都要在這裡刪掉
lock.unlock();
}
TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
Order order = new Order();
// ... 省略 Set
orderMapper.insertSelective(order);
OrderItem orderItem = new OrderItem();
// ... 省略 Set
orderItemMapper.insertSelective(orderItem);
platformTransactionManager.commit(transaction);
return order.getId();
}
三、常見分散式鎖的使用
3.1 資料庫樂觀鎖
3.2 資料庫分散式鎖
// 加上事務就是為了 for update 的鎖可以一直生效到事務執行結束// 預設回滾的是 RunTimeException@Transactional(rollbackFor = Exception.class)public String singleLock() throws Exception { log.info("我進入了方法!"); DistributeLock distributeLock = distributeLockMapper. selectDistributeLock("demo"); if (distributeLock==null) { throw new Exception("分散式鎖找不到"); } log.info("我進入了鎖!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return "我已經執行完成!";}
<select id="selectDistributeLock" resultType="com.deltaqin.distribute.model.DistributeLock"> select * from distribute_lock where businessCode = #{businessCode,jdbcType=VARCHAR} for update</select>
private MethodlockMapper methodlockMapper;
public boolean tryLock() {
try {
//插入一條資料 insert into
methodlockMapper.insert(new Methodlock("lock"));
}catch (Exception e){
//插入失敗
return false;
}
return true;
}
public void waitLock() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void unlock() {
//刪除資料 delete
methodlockMapper.deleteByMethodlock("lock");
System.out.println("-------釋放鎖------");
}
3.3 Redis setNx
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency>
spring.redis.host=localhost
public class RedisLock implements AutoCloseable {
private RedisTemplate redisTemplate;
private String key;
private String value;
//單位:秒
private int expireTime;
/**
* 沒有傳遞 value,因為直接使用的是隨機值
*/
public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){
this.redisTemplate = redisTemplate;
this.key = key;
this.expireTime=expireTime;
this.value = UUID.randomUUID().toString();
}
/**
* JDK 1.7 之後的自動關閉的功能
*/
public void close() throws Exception {
unLock();
}
/**
* 獲取分散式鎖
* SET resource_name my_random_value NX PX 30000
* 每一個執行緒對應的隨機值 my_random_value 不一樣,用於釋放鎖的時候校驗
* NX 表示 key 不存在的時候成功,key 存在的時候設定不成功,Redis 自己是單執行緒,序列執行的,第一個執行的才可以設定成功
* PX 表示過期時間,沒有設定的話,忘記刪除,就會永遠不過期
*/
public boolean getLock(){
RedisCallback<Boolean> redisCallback = connection -> {
//設定NX
RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
//設定過期時間
Expiration expiration = Expiration.seconds(expireTime);
//序列化key
byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
//序列化value
byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
//執行setnx操作
Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
return result;
};
//獲取分散式鎖
Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
return lock;
}
/**
* 釋放鎖的時候隨機數相同的時候才可以釋放,避免釋放了別人設定的鎖(自己的已經過期了所以別人才可以設定成功)
* 釋放的時候採用 LUA 指令碼,因為 delete 沒有原生支援刪除的時候校驗值,證明是當前執行緒設定進去的值
* 指令碼是在官方文件裡面有的
*/
public boolean unLock() {
// key 是自己才可以釋放,不是就不能釋放別人的鎖
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
List<String> keys = Arrays.asList(key);
// 執行指令碼的時候傳遞的 value 就是對應的值
Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);
log.info("釋放鎖的結果:"+result);
return result;
}
}
public String redisLock(){ log.info("我進入了方法!"); try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){ if (redisLock.getLock()) { log.info("我進入了鎖!!"); Thread.sleep(15000); } } catch (InterruptedException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } log.info("方法執行完成"); return "方法執行完成";}
3.4 zookeeper 瞬時znode節點 + watcher監聽機制
多執行緒併發建立瞬時節點的時候,得到有序的序列,序號最小的執行緒可以獲得鎖;
其他的執行緒監聽自己序號的前一個序號。前一個執行緒執行結束之後刪除自己序號的節點;
下一個序號的執行緒得到通知,繼續執行;
以此類推,建立節點的時候,就確認了執行緒執行的順序。
<dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.14</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions></dependency>
zk 的觀察器只可以監控一次,資料發生變化之後可以傳送給客戶端,之後需要再次設定監控。exists、create、getChildren三個方法都可以新增watcher ,也就是在呼叫方法的時候傳遞true就是新增監聽。注意這裡Lock 實現了Watcher和AutoCloseable:
/**
* 自己本身就是一個 watcher,可以得到通知
* AutoCloseable 實現自動關閉,資源不使用的時候
*/
4j
public class ZkLock implements AutoCloseable, Watcher {
private ZooKeeper zooKeeper;
/**
* 記錄當前鎖的名字
*/
private String znode;
public ZkLock() throws IOException {
this.zooKeeper = new ZooKeeper("localhost:2181",
10000,this);
}
public boolean getLock(String businessCode) {
try {
//建立業務 根節點
Stat stat = zooKeeper.exists("/" + businessCode, false);
if (stat==null){
zooKeeper.create("/" + businessCode,businessCode.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
}
//建立瞬時有序節點 /order/order_00000001
znode = zooKeeper.create("/" + businessCode + "/" + businessCode + "_", businessCode.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
//獲取業務節點下 所有的子節點
List<String> childrenNodes = zooKeeper.getChildren("/" + businessCode, false);
//獲取序號最小的(第一個)子節點
Collections.sort(childrenNodes);
String firstNode = childrenNodes.get(0);
//如果建立的節點是第一個子節點,則獲得鎖
if (znode.endsWith(firstNode)){
return true;
}
//如果不是第一個子節點,則監聽前一個節點
String lastNode = firstNode;
for (String node:childrenNodes){
if (znode.endsWith(node)){
zooKeeper.exists("/"+businessCode+"/"+lastNode,true);
break;
}else {
lastNode = node;
}
}
synchronized (this){
wait();
}
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public void close() throws Exception {
zooKeeper.delete(znode,-1);
zooKeeper.close();
log.info("我已經釋放了鎖!");
}
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted){
synchronized (this){
notify();
}
}
}
}
3.5 zookeeper curator
<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.2.0</version> <exclusions> <exclusion> <artifactId>slf4j-api</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions></dependency>
@Bean(initMethod="start",destroyMethod = "close")public CuratorFramework getCuratorFramework() { RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); CuratorFramework client = CuratorFrameworkFactory. newClient("localhost:2181", retryPolicy); return client;}
框架已經實現了分散式鎖。zk的Java客戶端升級版。使用的時候直接指定重試的策略就可以。
@Autowired
private CuratorFramework client;
@Test
public void testCuratorLock(){
InterProcessMutex lock = new InterProcessMutex(client, "/order");
try {
if ( lock.acquire(30, TimeUnit.SECONDS) ) {
try {
log.info("我獲得了鎖!!!");
}
finally {
lock.release();
}
}
} catch (Exception e) {
e.printStackTrace();
}
client.close();
}
3.6 Redission
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.11.2</version> <exclusions> <exclusion> <artifactId>slf4j-api</artifactId> <groupId>org.slf4j</groupId> </exclusion> </exclusions></dependency>
編寫相應的redisson.xml
<beans xmlns="
xmlns:xsi="
xmlns:context="
xmlns:redisson="
xsi:schemaLocation="
/spring-beans.xsd
/spring-context.xsd
/redisson.xsd
">
<redisson:client>
<redisson:single-server address="redis://127.0.0.1:6379"/>
</redisson:client>
</beans>
配置對應@ImportResource("classpath*:redisson.xml")資原始檔。
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.19.1</version></dependency>
@Beanpublic RedissonClient getRedissonClient() { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); return Redisson.create(config);}
@Testpublic void testRedissonLock() { RLock rLock = redisson.getLock("order"); try { rLock.lock(30, TimeUnit.SECONDS); log.info("我獲得了鎖!!!"); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); }finally { log.info("我釋放了鎖!!"); rLock.unlock(); }}
3.7 Etcd
四、常見分散式鎖的原理
4.1 Redisson
//lua指令碼命令執行方式:redis-cli --eval /tmp/test.lua , 10jedis.set("product_stock_10016", "15"); //初始化商品10016的庫存String script = " local count = redis.call('get', KEYS[1]) " + " local a = tonumber(count) " + " local b = tonumber(ARGV[1]) " + " if a >= b then " + " redis.call('set', KEYS[1], a-b) " + " return 1 " + " end " + " return 0 ";Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10"));System.out.println(obj);
4.2 RedLock解決非單體專案的Redis主從架構的鎖失效
獲取鎖
獲取當前時間T1,作為後續的計時依據;
按順序地,依次向5個獨立的節點來嘗試獲取鎖 SET resource_name my_random_value NX PX 30000;
計算獲取鎖總共花了多少時間,判斷獲取鎖成功與否;
時間:T2-T1;
多數節點的鎖(N/2+1);
當獲取鎖成功後的有效時間,要從初始的時間減去第三步算出來的消耗時間;
如果沒能獲取鎖成功,儘快釋放掉鎖。
釋放鎖
向所有節點發起釋放鎖的操作,不管這些節點有沒有成功設定過。
public String redlock() {
String lockKey = "product_001";
//這裡需要自己例項化不同redis例項的redisson客戶端連線,這裡只是虛擬碼用一個redisson客戶端簡化了
RLock lock1 = redisson.getLock(lockKey);
RLock lock2 = redisson.getLock(lockKey);
RLock lock3 = redisson.getLock(lockKey);
/**
* 根據多個 RLock 物件構建 RedissonRedLock (最核心的差別就在這裡)
*/
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
/**
* waitTimeout 嘗試獲取鎖的最大等待時間,超過這個值,則認為獲取鎖失敗
* leaseTime 鎖的持有時間,超過這個時間鎖會自動失效(值應設定為大於業務處理的時間,確保在鎖有效期內業務能處理完)
*/
boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (res) {
//成功獲得鎖,在這裡處理業務
}
} catch (Exception e) {
throw new RuntimeException("lock fail");
} finally {
//無論如何, 最後都要解鎖
redLock.unlock();
}
return "end";
}
但是,它的實現建立在一個不安全的系統模型上的,它依賴系統時間,當時鍾發生跳躍時,也可能會出現安全性問題。分散式儲存專家Martin對RedLock的分析文章,Redis作者的也專門寫了一篇文章進行了反駁。
Martin Kleppmann:How to do distributed locking
Antirez:Is Redlock safe?
4.3 Curator
五、業務中使用分散式鎖的注意點
5.1 自己實現分散式鎖的坑
public String deductStock() {
String lockKey = "lock:product_101";
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "deltaqin");
stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣減成功,剩餘庫存:" + realStock);
} else {
System.out.println("扣減失敗,庫存不足");
}
} finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
public String deductStock() { String lockKey = "lock:product_101"; String clientId = UUID.randomUUID().toString(); Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v) if (!result) { return "error_code"; } try { int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock") if (stock > 0) { int realStock = stock - 1; stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value) System.out.println("扣減成功,剩餘庫存:" + realStock); } else { System.out.println("扣減失敗,庫存不足"); } } finally { if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) { // 卡在這裡,鎖過期了,其他執行緒又可以加鎖,此時又把其他執行緒新加的鎖刪掉了 stringRedisTemplate.delete(lockKey); } } return "end";}
5.2 鎖最佳化:分段加鎖邏輯
product_10111_stock = 100
product_10111_stock1 = 20
product_10111_stock2 = 20
product_10111_stock3 = 20
product_10111_stock4 = 20
product_10111_stock5 = 20
六、分散式鎖的真相與選擇
6.1 分散式鎖的真相
互斥:不同執行緒、程式互斥。
超時機制:臨界區程式碼耗時導致,網路原因導致。可以使用額外的執行緒續命保證。
完備的鎖介面:阻塞的和非阻塞的介面都要有,lock和tryLock。
可重入性:當前請求的節點+ 執行緒唯一標識。
公平性:鎖喚醒時候,按照順序喚醒。
正確性:程式內的鎖不會因為報錯死鎖,因為崩潰的時候整個程式都會結束。但是多例項部署時死鎖就很容易發生,如果粗暴使用超時機制解決死鎖問題,就預設了下面這個假設:
鎖的超時時間 >> 獲取鎖的時延 + 執行臨界區程式碼的時間 + 各種程式的暫停(比如 GC)
但上述假設其實無法保證的。
6.2 分散式鎖的選擇
資料庫:db操作效能較差,並且有鎖表的風險,一般不考慮。
優點:實現簡單、易於理解
缺點:對資料庫壓力大
Redis:適用於併發量很大、效能要求很高而可靠性問題可以透過其他方案去彌補的場景。
優點:易於理解
缺點:自己實現、不支援阻塞
Redisson:相對於Jedis其實更多用在分散式的場景。
優點:提供鎖的方法,可阻塞
Zookeeper:適用於高可靠(高可用),而併發量不是太高的場景。
優點:支援阻塞
缺點:需理解Zookeeper、程式複雜
Curator
優點:提供鎖的方法
缺點:Zookeeper,強一致,慢
Etcd:安全和可靠性上有保證,但是比較重。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024924/viewspace-2996453/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Redis、Zookeeper實現分散式鎖——原理與實踐Redis分散式
- Redisson實現分散式鎖---原理Redis分散式
- redisson實現分散式鎖原理Redis分散式
- Redis分散式鎖的使用與實現原理Redis分散式
- 分散式鎖的實現及原理分散式
- 分散式鎖實踐分散式
- Redis分散式鎖的原理和實現Redis分散式
- zookeeper 分散式鎖的原理及實現分散式
- redisson之分散式鎖實現原理(三)Redis分散式
- memcached分散式原理與實現分散式
- R2M分散式鎖原理及實踐分散式
- 實現分散式鎖分散式
- 分散式鎖實現分散式
- 分散式鎖與實現(一)基於Redis實現!分散式Redis
- 輕量級分散式鎖的設計原理分析與實現分散式
- 分散式鎖----Redis實現分散式Redis
- Redis實現分散式鎖Redis分散式
- 分散式鎖及其實現分散式
- 分散式鎖的實現分散式
- 用 Redis 實現分散式鎖與實現任務佇列Redis分散式佇列
- Redis分散式實現原理Redis分散式
- memcached 分散式實現原理分散式
- Elasticsearch系列---實現分散式鎖Elasticsearch分散式
- Redis之分散式鎖實現Redis分散式
- 分散式鎖之Redis實現分散式Redis
- 分散式鎖之Zookeeper實現分散式
- 分散式鎖實現(二):Zookeeper分散式
- Redisson實現分散式鎖—RedissonLockRedis分散式
- 分散式鎖的實現方案分散式
- ZooKeeper分散式鎖的實現分散式
- redis分散式鎖-java實現Redis分散式Java
- Redis如何實現分散式鎖Redis分散式
- 分散式鎖實現(一):Redis分散式Redis
- 利用Redis實現分散式鎖Redis分散式
- etcd實現分散式鎖分散式
- 6 zookeeper實現分散式鎖分散式
- redis分散式鎖的實現Redis分散式
- redis分散式鎖-SETNX實現Redis分散式