自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

分布式鎖實(shí)戰(zhàn)-基于Etcd的實(shí)現(xiàn)很優(yōu)雅

開發(fā) 后端
雖然Kubernetes 給云原生時(shí)代帶來了顛覆性的新氣象,但卻很少人了解被欽定作為其后端存儲的 etcd ,本篇從分布式鎖視角梳理etcd的各種機(jī)制,探索基于etcd的鎖實(shí)現(xiàn)是怎樣。

一、etcd 簡介

1. etcd 的背景

雖然Kubernetes 給云原生時(shí)代帶來了顛覆性的新氣象,但卻很少人了解被欽定作為其后端存儲的 etcd ,本篇從分布式鎖視角梳理etcd的各種機(jī)制,探索基于etcd的鎖實(shí)現(xiàn)是怎樣。

etcd 能被Kubernetes 如此青睞,是因?yàn)樗恢痹隈雎犐鐓^(qū)的聲音并快速改進(jìn),積極配合 Kubernetes 項(xiàng)目向前推進(jìn),解決社區(qū)反饋的痛點(diǎn);發(fā)起 V2 到 V3 的重大版本更新,尤其是 19 年由 Google、Alibaba 等公司聯(lián)合打造的 3.4 版本,滿足了 Kubernetes 在超大型公司大規(guī)模使用中嚴(yán)苛的可用性、擴(kuò)展性和性能等要求。

圖片

上圖描述了 etcd 名字的由來,其寓意是為大規(guī)模分布式系統(tǒng)提供存儲配置信息;華為 Kubernetes 專家杜軍老師專門為 etcd 著書《云原生分布式存儲基石-etcd》,稱它基石足見何等重要,另一位大咖etcd 的作者李響老師曾講:“etcd 就是用來存儲云上最重要的數(shù)據(jù)的”;相信隨著云原生架構(gòu)的演進(jìn),etcd 會(huì)擔(dān)任越來越多重要的角色;技術(shù)人員在云原生時(shí)代,除了擁抱 Kubernetes,也要擁抱 etcd。

2.etcd vs ZooKeeper

很長一段時(shí)間 ZooKeeper(后文簡稱 ZK) 被作為默認(rèn)首選項(xiàng),用于解決分布式系統(tǒng)的協(xié)同和元數(shù)據(jù)存儲,但其太復(fù)雜、迭代慢、難維護(hù)、性能缺陷等問題逐漸成為槽點(diǎn);而 etcd 吸取了 ZK 的教訓(xùn),從設(shè)計(jì)和實(shí)現(xiàn)上具有后見之明,提供了更好的工程和運(yùn)維體驗(yàn),其主要改進(jìn)在于如下幾個(gè)方面:

  • 動(dòng)態(tài)的集群節(jié)點(diǎn)關(guān)系重配置
  • 高負(fù)載條件下的穩(wěn)定讀寫
  • 多版本并發(fā)控制的數(shù)據(jù)模型
  • 持久、穩(wěn)定的監(jiān)聽機(jī)制
  • 租約 (lease) 原語實(shí)現(xiàn)了連接與會(huì)話的解耦
  • 安全的分布式共享鎖 API
  • 普適的 HTTP 、GRPC 通信協(xié)議

據(jù)說在一個(gè)由 3 臺 8 核節(jié)點(diǎn)組成的云服務(wù)器上, etcd v3 版本可以做到每秒數(shù)萬次的寫操作和數(shù)十萬次的讀操作(ZK:圖片);后續(xù)會(huì)有其他篇章結(jié)合QA的測試結(jié)果來探討etcd服務(wù)端的設(shè)計(jì)和性能情況。

3.etcd 特性介紹

為滿足本篇目標(biāo)所需,也考慮到對 etcd 熟悉的讀者不多,這里著重介紹以下幾個(gè)關(guān)鍵特性:

  • 數(shù)據(jù)組織:etcd 在 V2 版本時(shí)跟 ZK 一樣以樹形目錄結(jié)構(gòu)來組織數(shù)據(jù),V3 版本則通過半開區(qū)間( Key range)取代 V2 中的目錄結(jié)構(gòu),優(yōu)化成扁平的 Key-Value 結(jié)構(gòu),Key 仍采用之前的格式如/lock/lock1/uuid1、/lock/lock1/uuid2?,感官跟樹形目錄形式保持一致,但實(shí)際是一個(gè)完整獨(dú)立的 key;可以基于 前綴(Prefix)機(jī)制通過/lock/lock1/?查詢一批擁有相同前綴的數(shù)據(jù),但其語義是前綴匹配查詢而非目錄子節(jié)點(diǎn)查詢;這個(gè)改造也是為了支撐更多的特性和達(dá)到更好的性能。

圖片

  • 集群模式:通常是由 3、5 個(gè)基數(shù)實(shí)例組成集群,當(dāng)超過半數(shù)服務(wù)實(shí)例正常工作就能對外提供服務(wù),既能避免單點(diǎn)故障,又盡量高可用,每個(gè)服務(wù)實(shí)例都有一個(gè)數(shù)據(jù)備份,通過 raft 協(xié)議實(shí)現(xiàn)數(shù)據(jù)全局一致。

圖片

  • 順序更新:每個(gè) etcd 節(jié)點(diǎn)都可接收讀寫請求,但變更類(增刪改)請求會(huì)在集群內(nèi)轉(zhuǎn)給 leader 執(zhí)行,來所有客戶端的變更請求將按照 leader 接收的順序被處理,在 ZK 中插入同名節(jié)點(diǎn),ZK 會(huì)自動(dòng)為同名節(jié)點(diǎn)名后添加遞增序號,即避免沖突又具有順序管控的意義;但 etcd 中不能插入同名 Key,它是采用 revision 機(jī)制來管控更新類請求的順序,使用一個(gè)全局計(jì)數(shù)器,單調(diào)遞增,每當(dāng)有數(shù)據(jù)變更,revision 就會(huì)加一(具有全局唯一性),而每個(gè) revision 都關(guān)聯(lián)對應(yīng)修改的數(shù)據(jù),可以通過 revision 的大小推斷數(shù)據(jù)變更的順序,利用這個(gè)特性可以實(shí)現(xiàn)高級協(xié)調(diào)服務(wù)。

圖片

  • 租約(Lease)機(jī)制:常見的 TTL(Time To Live)機(jī)制是給單個(gè) Key-Value 設(shè)置存活時(shí)間,超過時(shí)間后自動(dòng)刪除這個(gè) Key-Value,V3 中加入的租約機(jī)制更高級一些,針對每個(gè) Lease 設(shè)置了一個(gè) TTL 時(shí)間,存儲 Key-Value 時(shí)可指定一個(gè) Lease,多個(gè)擁有相同 TTL 的 key 綁定到同一個(gè) Lease,實(shí)現(xiàn) Lease 的復(fù)用。同時(shí)也支持續(xù)約,可以通過客戶端在 Lease 到期之前續(xù)約,以避免 Key-Value 過期后失效,也可以通過客戶端主動(dòng)解約,這比 ZK 中基于 session 的狀態(tài)保持更靈活。

圖片

  • 監(jiān)聽機(jī)制:客戶端可以注冊對某一個(gè) Key 或一批相同前綴(前綴機(jī)制,似 ZK 中的父節(jié)點(diǎn))Key 的監(jiān)聽,當(dāng)被監(jiān)聽的這些 Key 發(fā)生變更,客戶端將收到通知,感知到變更。

圖片

etcd 的分布式鎖正是基于以上特性來實(shí)現(xiàn)的,簡單來說是:

  • 租約機(jī)制:用于支撐異常情況下的鎖自動(dòng)釋放能力
  • 前綴和 Revision 機(jī)制:用于支撐公平獲取鎖和排隊(duì)等待的能力
  • 監(jiān)聽機(jī)制:用于支撐搶鎖能力
  • 集群模式:用于支撐鎖服務(wù)的高可用

二、加解鎖的流程描述

圖片

1.準(zhǔn)備客戶端和 Key

  • 客戶端連接 Etcd,獲取 LockClient,獲取 LeaseClient
  • 以/lock/lock1? 為前綴 + / + uuid 創(chuàng)建全局唯一的 key,如:

client-a 線程 1 的 key 為"/lock/lock1/uuid1",申請 lock1

client-a 線程 2 的 key 為"/lock/lock1/uuid2",申請 lock1

client-c 線程 1 的 key 為"/lock/lock2/uuid3",申請 lock2

client-b 線程 1 的 key 為"/lock/lock2/uuid4",申請 lock2

  • 客戶端 a、b、c 分別創(chuàng)建租約,租約的時(shí)長由業(yè)務(wù)輸入確定

2.創(chuàng)建租約并保持續(xù)租

  • 方案 1-創(chuàng)建定時(shí)任務(wù)定時(shí)續(xù)租,無論客戶端是持鎖狀態(tài)還是等待鎖狀態(tài) Key 都必須存在,而 Key 是否釋放由租約管控,所以都需要保持租約的活性

持鎖狀態(tài):當(dāng)業(yè)務(wù)未完成時(shí),不能讓租約到期,需定時(shí)續(xù)租;當(dāng)業(yè)務(wù)完成時(shí)可主動(dòng)解除租約,持鎖 Key 會(huì)被刪除;若客戶端異常,租約到期后持鎖 Key 也會(huì)被刪除;等鎖的客戶端監(jiān)聽到持鎖 Key 被刪除后,可開始搶鎖。

等鎖狀態(tài):等鎖超時(shí)會(huì)主動(dòng)解除租約,或客戶端異常時(shí)等鎖 key 被刪除,后邊排隊(duì)的就前進(jìn)一步,嘗試搶鎖。

  • 方案 2-使用自動(dòng)續(xù)約

3.綁定租約寫 key

  • 每個(gè)客戶端在執(zhí)行 put 操作時(shí),將第 1 步中準(zhǔn)備的具有唯一性的 Key 綁定租約寫入 etcd

4.獲取競爭鎖的 key-Value 列表

  • 以 lock1 為例,客戶端以前綴"/lock/lock1" 讀取所有匹配此前綴的的 key-Value 列表(key-Value 中帶有 key 對應(yīng)的 Revision)

5.對所獲取的 Key-Value 列表按 revision 從小到大排序

6.判斷自己是不是第一個(gè)(revision 最小),若是,則成功獲取鎖

7.若不是,則監(jiān)聽自己的前一個(gè) Key-Value 的刪除事件

  • 刪除事件由主動(dòng)釋放租約或者因租約過期失效而觸發(fā)
  • 這種只監(jiān)聽前一個(gè) Key-Value 的方式避免了驚群效應(yīng)問題

8.若是阻塞申請鎖,則申請鎖的操作可增加阻塞等待

9.若監(jiān)聽事件生效,則回到第 4 步重新進(jìn)行判斷,直到獲取到鎖

10解鎖時(shí),將第一個(gè) Key-Value 的租約釋放

三、etcd 分布式鎖的能力

可能讀者是單篇閱讀,這里引入第一篇《分布式鎖上-初探》中的一些內(nèi)容,一個(gè)分布式鎖應(yīng)具備這樣一些功能特點(diǎn):

  • 互斥性:在同一時(shí)刻,只有一個(gè)客戶端能持有鎖
  • 安全性:避免死鎖,如果某個(gè)客戶端獲得鎖之后處理時(shí)間超過最大約定時(shí)間,或者持鎖期間發(fā)生了故障導(dǎo)致無法主動(dòng)釋放鎖,其持有的鎖也能夠被其他機(jī)制正確釋放,并保證后續(xù)其它客戶端也能加鎖,整個(gè)處理流程繼續(xù)正常執(zhí)行
  • 可用性:也被稱作容錯(cuò)性,分布式鎖需要有高可用能力,避免單點(diǎn)故障,當(dāng)提供鎖的服務(wù)節(jié)點(diǎn)故障(宕機(jī))時(shí)不影響服務(wù)運(yùn)行,這里有兩種模式:一種是分布式鎖服務(wù)自身具備集群模式,遇到故障能自動(dòng)切換恢復(fù)工作;另一種是客戶端向多個(gè)獨(dú)立的鎖服務(wù)發(fā)起請求,當(dāng)某個(gè)鎖服務(wù)故障時(shí)仍然可以從其他鎖服務(wù)讀取到鎖信息(Redlock)
  • 可重入性:對同一個(gè)鎖,加鎖和解鎖必須是同一個(gè)線程,即不能把其他線程持有的鎖給釋放了
  • 高效靈活:加鎖、解鎖的速度要快;支持阻塞和非阻塞;支持公平鎖和非公平鎖

基于上邊對 etcd 分布式鎖的介紹,這里簡單總結(jié)一下 etcd 的能力矩陣,ZK 的情況請看《分布式鎖中-基于 Zookeeper 的實(shí)現(xiàn)》,redis鎖的情況會(huì)在后續(xù)文章中補(bǔ)充

能力

ZK

etcd

Redis 原生

Redlock

互斥



安全

鏈接異常時(shí),session 丟失自動(dòng)釋放鎖

基于租約,租約過期后自動(dòng)釋放鎖,不用像ZK那樣釋放鏈接



可用性

相對可用性還好



可重入

服務(wù)端非可重入,本地線程可重入

服務(wù)端非可重入,本地線程可重入需自研



加解鎖速度

速度不算快

速度快,如GRPC 協(xié)議優(yōu)勢、服務(wù)端性能的優(yōu)勢



阻塞非阻塞

客戶端兩種能力都提供

jetcd-core 中,阻塞非阻塞由 Future#get 的超時(shí)控制能力支撐



公平非公平

公平鎖

公平鎖



可續(xù)租

天然支持,基于session

天然支持,基于Lease



四、jetcd 庫實(shí)現(xiàn)分布式鎖

etcd 針對 java 語言的客戶端有官方的 jetcd-core 還有 IBM 的 etcd-java,下文使用 jetcd-core 做示例。

jetcd-core 中提供了高階的 Lock API(無需使用者準(zhǔn)備唯一 key、前綴查詢 Key-Value 列表、排序判斷自己是否 revision 最小的、監(jiān)聽前一個(gè) Key-Value 的刪除),使用者只需關(guān)注最原始的訴求:申請的鎖是什么名稱、用多久,申請不到就等多久;使用高階 API 實(shí)現(xiàn)分布式鎖的流程會(huì)比第 2 部分原生的流程要簡單許多,流程如下:

  • 連接 etcd 集群
  • 獲取 LockClient,獲取 LeaseClient,不需要準(zhǔn)備 key
  • 創(chuàng)建租約并保持續(xù)租, 你覺得哪一種方案會(huì)更好呢

方案 1-創(chuàng)建定時(shí)任務(wù)定時(shí)續(xù)租

方案 2-使用自動(dòng)續(xù)約

  • 綁定租約搶鎖
  • 若搶到鎖,則返回鎖路徑信息
  • 若沒搶到鎖,則阻塞等待(可設(shè)置超時(shí)返回)
  • 解鎖時(shí),傳入鎖信息解鎖;根據(jù)業(yè)務(wù)場景決定如何解約

1.pom 依賴

<dependency>
<groupId>io.etcd</groupId>
<artifactId>jetcd-core</artifactId>
<version>0.7.3</version>
</dependency>

2.Lease 相關(guān)的 API 介紹

public interface Lease extends CloseableClient {

//創(chuàng)建一個(gè)租約,過期時(shí)間是ttl,單位秒;沒有請求超時(shí)控制
CompletableFuture<LeaseGrantResponse> grant(long ttl);

//創(chuàng)建一個(gè)租約,過期時(shí)間是ttl,單位秒;后兩個(gè)參數(shù)是請求超時(shí)控制
CompletableFuture<LeaseGrantResponse> grant(long ttl, long timeout, TimeUnit unit);

//解約
CompletableFuture<LeaseRevokeResponse> revoke(long leaseId);

//主動(dòng)續(xù)約1次
CompletableFuture<LeaseKeepAliveResponse> keepAliveOnce(long leaseId);

//查詢租約信息
CompletableFuture<LeaseTimeToLiveResponse> timeToLive(long leaseId, LeaseOption leaseOption);

//自動(dòng)續(xù)約
CloseableClient keepAlive(long leaseId, StreamObserver<LeaseKeepAliveResponse> observer);
}

3.lock 相關(guān)的 API 介紹

public interface Lock extends CloseableClient {

//綁定租約申請指定名稱的鎖,搶鎖成功返回鎖秘鑰,
CompletableFuture<LockResponse> lock(ByteSequence name, long leaseId);

//持鎖秘鑰解鎖
CompletableFuture<UnlockResponse> unlock(ByteSequence lockKey);
}

4.分布式鎖示例

package com.rock.dlock;

import io.etcd.jetcd.ByteSequence;
import io.etcd.jetcd.Client;
import io.etcd.jetcd.Lease;
import io.etcd.jetcd.Lock;
import io.etcd.jetcd.lease.LeaseKeepAliveResponse;
import io.etcd.jetcd.lock.LockResponse;
import io.grpc.stub.StreamObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
* @author zs
* @date 2022/11/6 12:23 PM
*/
public class DemoEtcdLock {
private final static Logger log = LoggerFactory.getLogger(DemoEtcdLock.class);
private Client client;
private Lock lockClient;
private Lease leaseClient;

private LockState lockState;

class LockState{
private String lockKey;
private String lockPath;
private String errorMsg;
private long leaseTTL;
private long leaseId;
private boolean lockSuccess;

public LockState(String lockKey, long leaseTTL) {
this.lockKey = lockKey;
this.leaseTTL = leaseTTL;
}

public String getLockKey() {
return lockKey;
}

public void setLockKey(String lockKey) {
this.lockKey = lockKey;
}

public String getLockPath() {
return lockPath;
}

public void setLockPath(String lockPath) {
this.lockPath = lockPath;
}

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 long getLeaseTTL() {
return leaseTTL;
}

public void setLeaseTTL(long leaseTTL) {
this.leaseTTL = leaseTTL;
}
}


public DemoEtcdLock(Client client, String lockKey, Long leaseTTL, TimeUnit unit) {
this.client = client;
//1.準(zhǔn)備客戶端
this.lockClient = client.getLockClient();
this.leaseClient = client.getLeaseClient();
this.lockState = new LockState(lockKey,unit.toSeconds(leaseTTL));
}


public boolean lock() {
try {
//2.創(chuàng)建租約,并自動(dòng)續(xù)約
createLease();

//3.執(zhí)行加鎖,并為鎖對應(yīng)的Key綁定租約
createLock();
}catch (InterruptedException | ExecutionException e) {
//todo:異常處理
}
return lockState.isLockSuccess();
}

public void unlock() {
try {
//正常釋放鎖
if (this.lockState.getLockPath() != null) {
lockClient.unlock(ByteSequence.from(lockState.getLockPath().getBytes())).get();
}
//如果是主動(dòng)續(xù)約,則關(guān)閉續(xù)約的定時(shí)任務(wù)

//刪除租約
if (lockState.getLeaseId() != 0L) {
leaseClient.revoke(lockState.getLeaseId());
}
} catch (InterruptedException | ExecutionException e) {
//todo:異常處理
}
log.info("線程:{} 釋放鎖", Thread.currentThread().getName());
}

// 創(chuàng)建一個(gè)租約
private void createLease() throws ExecutionException, InterruptedException {
log.debug("[etcd-lock]: start to createLease." + this.lockState.getLockKey() + Thread.currentThread().getName());
try {
long leaseId = leaseClient.grant(this.lockState.getLeaseTTL()).get().getID();
lockState.setLeaseId(leaseId);
//自動(dòng)續(xù)約
StreamObserver<LeaseKeepAliveResponse> observer = new StreamObserver<LeaseKeepAliveResponse>() {
@Override
public void onNext(LeaseKeepAliveResponse value) {
log.trace("cluster node lease remaining ttl: {}, lease id: {}", value.getTTL(), value.getID());
}

@Override
public void onError(Throwable t) {
log.error("cluster node lease keep alive failed. exception info: {}", t);
}

@Override
public void onCompleted() {
log.trace("cluster node lease completed");
}
};
// 設(shè)置自動(dòng)續(xù)約
leaseClient.keepAlive(leaseId, observer);
}catch (InterruptedException | ExecutionException e) {
log.error("[etcd-lock] Create lease failed:" + e);
lockState.setErrorMsg("Create lease failed:" + e);
throw e;
}
}

private void createLock() throws ExecutionException, InterruptedException {
String lockKey = this.lockState.getLockKey();
log.debug("[etcd-lock]: start to createLock." + lockKey + Thread.currentThread().getName());
try {
LockResponse lockResponse = lockClient.lock(ByteSequence.from(lockKey.getBytes()), lockState.getLeaseId()).get();
if (lockResponse != null) {
String lockPath = lockResponse.getKey().toString(StandardCharsets.UTF_8);
this.lockState.setLockPath(lockPath);
log.info("線程:{} 加鎖成功,鎖路徑:{}", Thread.currentThread().getName(), lockPath);
this.lockState.setLockSuccess(true);
}
}
catch (InterruptedException | ExecutionException e) {
log.error("[etcd-lock] lock failed:" + e);
lockState.setErrorMsg("[etcd-lock] lock failed:" + e);
leaseClient.revoke(this.lockState.getLeaseId());
throw e;
}
}
}

5.測試鎖

package com.rock.dlock;

import io.etcd.jetcd.Client;

import java.util.concurrent.TimeUnit;

/**
* @author zs
* @date 2022/11/6 12:23 PM
*/
public class TestEtcdLock {
public static void main(String[] args) {
Client client = Client.builder().endpoints("http://localhost:2379").build();

DemoEtcdLock demoEtcdLock1 = new DemoEtcdLock(client,"rock",30L, TimeUnit.SECONDS);
DemoEtcdLock demoEtcdLock2 = new DemoEtcdLock(client,"rock",30L, TimeUnit.SECONDS);

boolean lock1 = demoEtcdLock1.lock();
if(lock1) {
try {
System.out.printf("do something");
} finally {
demoEtcdLock1.unlock();
}
}
demoEtcdLock1.lock();//demoEtcdLock1 持鎖未釋放
demoEtcdLock2.lock();//demoEtcdLock2 客戶端無可重入設(shè)計(jì),這里將會(huì)阻塞等待demoEtcdLock1釋放鎖
}
}

五、總結(jié)

本篇從 etcd V3 版本的 Lease、Prefix 、Watch 等關(guān)鍵特性切入,介紹了如何基于這些特性來實(shí)現(xiàn)一個(gè)分布式鎖,并基于jetcd-core庫提供了一個(gè)分布式鎖的示例,呈現(xiàn)了其關(guān)鍵API的用法;此示例尚未達(dá)到生產(chǎn)級可用,如異常、可重入、可重試、超時(shí)控制等功能都未補(bǔ)全,計(jì)劃在下一篇介紹完redis之后,再介紹一個(gè)健壯的分布式鎖客戶端要如何抽象設(shè)計(jì),如何適配 ZK 、Redis 、etcd 。

本文轉(zhuǎn)載自微信公眾號「架構(gòu)染色」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系【架構(gòu)染色】公眾號作者。


責(zé)任編輯:武曉燕 來源: 架構(gòu)染色
相關(guān)推薦

2017-04-13 10:51:09

Consul分布式

2022-10-27 10:44:14

分布式Zookeeper

2023-09-04 08:12:16

分布式鎖Springboot

2017-10-24 11:28:23

Zookeeper分布式鎖架構(gòu)

2023-01-13 07:39:07

2024-11-28 15:11:28

2025-01-07 08:37:35

2019-06-19 15:40:06

分布式鎖RedisJava

2021-02-28 07:49:28

Zookeeper分布式

2017-01-16 14:13:37

分布式數(shù)據(jù)庫

2018-04-03 16:24:34

分布式方式

2022-04-08 08:27:08

分布式鎖系統(tǒng)

2019-02-26 09:51:52

分布式鎖RedisZookeeper

2021-05-19 08:12:39

etcd分布式鎖分布式系統(tǒng)

2023-08-21 19:10:34

Redis分布式

2022-01-06 10:58:07

Redis數(shù)據(jù)分布式鎖

2021-10-25 10:21:59

ZK分布式鎖ZooKeeper

2021-11-01 12:25:56

Redis分布式

2023-12-01 10:49:07

Redis分布式鎖

2024-01-26 13:17:00

rollbackMQ訂單系統(tǒng)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號