集成源碼深度剖析:Fescar x Spring Cloud

Fescar 簡介

常見的分佈式事務方式有基於 2PC 的 XA (e.g. atomikos),從業務層入手的 TCC( e.g. byteTCC)、事務消息 ( e.g. RocketMQ Half Message) 等等。XA 是須要本地數據庫支持的分佈式事務的協議,資源鎖在數據庫層面致使性能較差,而支付寶做爲佈道師引入的 TCC 模式須要大量的業務代碼保證,開發維護成本較高。git

分佈式事務是業界比較關注的領域,這也是短短期 Fescar 能收穫6k Star的緣由之一。Fescar 名字取自 Fast & Easy Commit And Rollback ,簡單來講Fescar經過對本地 RDBMS 分支事務的協調來驅動完成全局事務,是工做在應用層的中間件。主要優勢是相對於XA模式是性能較好不長時間佔用鏈接資源,相對於 TCC 方式開發成本和業務侵入性較低。github

相似於 XA,Fescar 將角色分爲 TC、RM、TM,事務總體過程模型以下:web

1\. TM 向 TC 申請開啓一個全局事務,全局事務建立成功並生成一個全局惟一的 XID。
2\. XID 在微服務調用鏈路的上下文中傳播。
3\. RM 向 TC 註冊分支事務,將其歸入 XID 對應全局事務的管轄。
4\. TM 向 TC 發起針對 XID 的全局提交或回滾決議。
5\. TC 調度 XID 下管轄的所有分支事務完成提交或回滾請求。

其中在目前的實現版本中 TC 是獨立部署的進程,維護全局事務的操做記錄和全局鎖記錄,負責協調並驅動全局事務的提交或回滾。TM RM 則與應用程序工做在同一應用進程。RM對 JDBC 數據源採用代理的方式對底層數據庫作管理,利用語法解析,在執行事務保留快照,並生成 undo log。大概的流程和模型劃分就介紹到這裏,下面開始對 Fescar 事務傳播機制的分析。spring

Fescar 事務傳播機制

Fescar 事務傳播包括應用內事務嵌套調用和跨服務調用的事務傳播。Fescar 事務是怎麼在微服務調用鏈中傳播的呢?Fescar 提供了事務 API 容許用戶手動綁定事務的 XID 並加入到全局事務中,因此咱們根據不一樣的服務框架機制,將 XID 在鏈路中傳遞便可實現事務的傳播。數據庫

RPC 請求過程分爲調用方與被調用方兩部分,咱們將 XID 在請求與響應時作相應的處理便可。大體過程爲:調用方即請求方將當前事務上下文中的 XID 取出,經過RPC協議傳遞給被調用方;被調用方從請求中的將 XID 取出,並綁定到本身的事務上下文中,歸入全局事務。微服務框架通常都有相應的 Filter 和 Interceptor 機制,咱們來分析下 Spring Cloud 與Fescar 的整合過程。架構

Fescar 與 Spring Cloud Alibaba 集成部分源碼解析

本部分源碼所有來自於 spring-cloud-alibaba-fescar. 源碼解析部分主要包括AutoConfiguration、微服務被調用方和微服務調用方三大部分。對於微服務調用方方式具體分爲 RestTemplate 和 Feign,對於 Feign 請求方式又進一步細分爲結合 Hystrix 和 Sentinel 的使用模式。併發

Fescar AutoConfiguration

對於 AutoConfiguration 部分的解析此處只介紹與 Fescar 啓動相關的部分,其餘部分的解析將穿插於【微服務被調用方】和【微服務調用方】章節進行介紹。app

Fescar 的啓動須要配置 GlobalTransactionScanner,GlobalTransactionScanner 負責初始化 Fescar 的 RM client、TM client 和 自動代理標註 GlobalTransactional 註解的類。負載均衡

GlobalTransactionScanner bean 的啓動經過 GlobalTransactionAutoConfiguration 加載並注入FescarProperties。框架

FescarProperties 包含了 Fescar的重要屬性 txServiceGroup ,此屬性的可經過 application.properties 文件中的 key: spring.cloud.alibaba.fescar.txServiceGroup 讀取,默認值爲 ${spring.application.name}-fescar-service-group 。txServiceGroup 表示Fescar 的邏輯事務分組名,此分組名經過配置中心(目前支持文件、Apollo)獲取邏輯事務分組名對應的 TC 集羣名稱,進一步經過集羣名稱構造出 TC 集羣的服務名,經過註冊中心(目前支持Nacos、Redis、ZooKeeper和Eureka)和服務名找到可用的 TC 服務節點,而後 RM client、TM client 與 TC 進行 RPC 交互。

微服務被調用方

因爲調用方的邏輯比較多一點,咱們先分析被調用方的邏輯。針對於 Spring Cloud 項目,默認採用的 RPC 傳輸協議時 HTTP 協議,因此使用了 HandlerInterceptor 機制來對HTTP的請求作攔截。

HandlerInterceptor 是 Spring 提供的接口, 它有如下三個方法能夠被覆寫。

/**
     * Intercept the execution of a handler. Called after HandlerMapping determined
     * an appropriate handler object, but before HandlerAdapter invokes the handler.
     */
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        return true;
    }

    /**
     * Intercept the execution of a handler. Called after HandlerAdapter actually
     * invoked the handler, but before the DispatcherServlet renders the view.
     * Can expose additional model objects to the view via the given ModelAndView.
     */
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable ModelAndView modelAndView) throws Exception {
    }

    /**
     * Callback after completion of request processing, that is, after rendering
     * the view. Will be called on any outcome of handler execution, thus allows
     * for proper resource cleanup.
     */
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable Exception ex) throws Exception {
    }

根據註釋,咱們能夠很明確的看到各個方法的做用時間和經常使用用途。對於 Fescar 集成來說,它須要重寫了 preHandle、afterCompletion 方法。

FescarHandlerInterceptor 的做用是將服務鏈路傳遞過來的 XID,綁定到服務節點的事務上下文中,而且在請求完成後清理相關資源。FescarHandlerInterceptorConfiguration 中配置了全部的 url 均進行攔截,對全部的請求過來均會執行該攔截器,進行 XID 的轉換與事務綁定。

/**
 * @author xiaojing
 *
 * Fescar HandlerInterceptor, Convert Fescar information into
 * @see com.alibaba.fescar.core.context.RootContext from http request's header in
 * {@link org.springframework.web.servlet.HandlerInterceptor#preHandle(HttpServletRequest , HttpServletResponse , Object )},
 * And clean up Fescar information after servlet method invocation in
 * {@link org.springframework.web.servlet.HandlerInterceptor#afterCompletion(HttpServletRequest, HttpServletResponse, Object, Exception)}
 */
public class FescarHandlerInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory
            .getLogger(FescarHandlerInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
            Object handler) throws Exception {

        String xid = RootContext.getXID();
        String rpcXid = request.getHeader(RootContext.KEY_XID);
        if (log.isDebugEnabled()) {
            log.debug("xid in RootContext {} xid in RpcContext {}", xid, rpcXid);
        }

        if (xid == null && rpcXid != null) {
            RootContext.bind(rpcXid);
            if (log.isDebugEnabled()) {
                log.debug("bind {} to RootContext", rpcXid);
            }
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
            Object handler, Exception e) throws Exception {

        String rpcXid = request.getHeader(RootContext.KEY_XID);

        if (StringUtils.isEmpty(rpcXid)) {
            return;
        }

        String unbindXid = RootContext.unbind();
        if (log.isDebugEnabled()) {
            log.debug("unbind {} from RootContext", unbindXid);
        }
        if (!rpcXid.equalsIgnoreCase(unbindXid)) {
            log.warn("xid in change during RPC from {} to {}", rpcXid, unbindXid);
            if (unbindXid != null) {
                RootContext.bind(unbindXid);
                log.warn("bind {} back to RootContext", unbindXid);
            }
        }
    }

}

preHandle 在請求執行前被調用,xid 爲當前事務上下文已經綁定的全局事務的惟一標識,rpcXid 爲請求經過 HTTP Header 傳遞過來須要綁定的全局事務標識。preHandle 方法中判斷若是當前事務上下文中沒有 XID,且 rpcXid 不爲空,那麼就將 rpcXid 綁定到當前的事務上下文。

afterCompletion 在請求完成後被調用,該方法用來執行資源的相關清理動做。Fescar 經過 RootContext.unbind() 方法對事務上下文涉及到的 XID 進行解綁。下面 if 中的邏輯是爲了代碼的健壯性考慮,若是遇到 rpcXid和 unbindXid 不相等的狀況,再將 unbindXid 從新綁定回去。

對於 Spring Cloud 來說,默認採用的 RPC 方式是 HTTP 的方式,因此對被調用方來說,它的請求攔截方式不用作任何區分,只須要從 Header 中將 XID 就能夠取出綁定到本身的事務上下文中便可。可是對於調用方因爲請求組件的多樣化,包括熔斷隔離機制,因此要區分不一樣的狀況作處理,後面咱們來具體分析一下。

微服務調用方

Fescar 將請求方式分爲:RestTemplate、Feign、Feign+Hystrix 和 Feign+Sentinel 。不一樣的組件經過 Spring Boot 的 Auto Configuration 來完成自動的配置,具體的配置類能夠看 spring.factories ,下文也會介紹相關的配置類。

RestTemplate

先來看下若是調用方若是是是基於 RestTemplate 的請求,Fescar 是怎麼傳遞 XID 的。

public class FescarRestTemplateInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes,
            ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
        HttpRequestWrapper requestWrapper = new HttpRequestWrapper(httpRequest);

        String xid = RootContext.getXID();

        if (!StringUtils.isEmpty(xid)) {
            requestWrapper.getHeaders().add(RootContext.KEY_XID, xid);
        }
        return clientHttpRequestExecution.execute(requestWrapper, bytes);
    }
}

FescarRestTemplateInterceptor 實現了 ClientHttpRequestInterceptor 接口的 intercept 方法,對調用的請求作了包裝,在發送請求時若存在 Fescar 事務上下文 XID 則取出並放到 HTTP Header 中。

FescarRestTemplateInterceptor 經過 FescarRestTemplateAutoConfiguration 實現將 FescarRestTemplateInterceptor 配置到 RestTemplate 中去。

@Configuration
public class FescarRestTemplateAutoConfiguration {

    @Bean
    public FescarRestTemplateInterceptor fescarRestTemplateInterceptor() {
        return new FescarRestTemplateInterceptor();
    }

    @Autowired(required = false)
    private Collection<RestTemplate> restTemplates;

    @Autowired
    private FescarRestTemplateInterceptor fescarRestTemplateInterceptor;

    @PostConstruct
    public void init() {
        if (this.restTemplates != null) {
            for (RestTemplate restTemplate : restTemplates) {
                List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>(
                        restTemplate.getInterceptors());
                interceptors.add(this.fescarRestTemplateInterceptor);
                restTemplate.setInterceptors(interceptors);
            }
        }
    }

}

init 方法遍歷全部的 restTemplate ,並將原來 restTemplate 中的攔截器取出,增長 fescarRestTemplateInterceptor 後置入並重排序。

Feign

接下來看下 Feign 的相關代碼,該包下面的類仍是比較多的,咱們先從其 AutoConfiguration 入手。

@Configuration
@ConditionalOnClass(Client.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FescarFeignClientAutoConfiguration {

    @Bean
    @Scope("prototype")
    @ConditionalOnClass(name = "com.netflix.hystrix.HystrixCommand")
    @ConditionalOnProperty(name = "feign.hystrix.enabled", havingValue = "true")
    Feign.Builder feignHystrixBuilder(BeanFactory beanFactory) {
        return FescarHystrixFeignBuilder.builder(beanFactory);
    }

    @Bean
    @Scope("prototype")
    @ConditionalOnClass(name = "com.alibaba.csp.sentinel.SphU")
    @ConditionalOnProperty(name = "feign.sentinel.enabled", havingValue = "true")
    Feign.Builder feignSentinelBuilder(BeanFactory beanFactory) {
        return FescarSentinelFeignBuilder.builder(beanFactory);
    }

    @Bean
    @ConditionalOnMissingBean
    @Scope("prototype")
    Feign.Builder feignBuilder(BeanFactory beanFactory) {
        return FescarFeignBuilder.builder(beanFactory);
    }

    @Configuration
    protected static class FeignBeanPostProcessorConfiguration {

        @Bean
        FescarBeanPostProcessor fescarBeanPostProcessor(
                FescarFeignObjectWrapper fescarFeignObjectWrapper) {
            return new FescarBeanPostProcessor(fescarFeignObjectWrapper);
        }

        @Bean
        FescarContextBeanPostProcessor fescarContextBeanPostProcessor(
                BeanFactory beanFactory) {
            return new FescarContextBeanPostProcessor(beanFactory);
        }

        @Bean
        FescarFeignObjectWrapper fescarFeignObjectWrapper(BeanFactory beanFactory) {
            return new FescarFeignObjectWrapper(beanFactory);
        }
    }

}

FescarFeignClientAutoConfiguration 在存在 Client.class 時生效,且要求做用在 FeignAutoConfiguration 以前。因爲FeignClientsConfiguration 是在 FeignAutoConfiguration 生成 FeignContext 生效的,因此根據依賴關係, FescarFeignClientAutoConfiguration 一樣早於 FeignClientsConfiguration。

FescarFeignClientAutoConfiguration 自定義了 Feign.Builder,針對於 feign.sentinel,feign.hystrix 和 feign 的狀況作了適配,目的是自定義 feign 中 Client 的真正實現爲 FescarFeignClient。

HystrixFeign.builder().retryer(Retryer.NEVER_RETRY)
      .client(new FescarFeignClient(beanFactory))
SentinelFeign.builder().retryer(Retryer.NEVER_RETRY)
                .client(new FescarFeignClient(beanFactory));
Feign.builder().client(new FescarFeignClient(beanFactory));

FescarFeignClient 是對原來的 Feign 客戶端代理加強,具體代碼見下圖:

public class FescarFeignClient implements Client {

    private final Client delegate;
    private final BeanFactory beanFactory;

    FescarFeignClient(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
        this.delegate = new Client.Default(null, null);
    }

    FescarFeignClient(BeanFactory beanFactory, Client delegate) {
        this.delegate = delegate;
        this.beanFactory = beanFactory;
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {

        Request modifiedRequest = getModifyRequest(request);

        try {
            return this.delegate.execute(modifiedRequest, options);
        }
        finally {

        }
    }

    private Request getModifyRequest(Request request) {

        String xid = RootContext.getXID();

        if (StringUtils.isEmpty(xid)) {
            return request;
        }

        Map<String, Collection<String>> headers = new HashMap<>();
        headers.putAll(request.headers());

        List<String> fescarXid = new ArrayList<>();
        fescarXid.add(xid);
        headers.put(RootContext.KEY_XID, fescarXid);

        return Request.create(request.method(), request.url(), headers, request.body(),
                request.charset());
    }

上面的過程當中咱們能夠看到,FescarFeignClient 對原來的 Request 作了修改,它首先將 XID 從當前的事務上下文中取出,若是 XID 不爲空的狀況下,將 XID 放到了 Header 中。

FeignBeanPostProcessorConfiguration 定義了3個bean:FescarContextBeanPostProcessor、FescarBeanPostProcessor 和 FescarFeignObjectWrapper。其中 FescarContextBeanPostProcessor FescarBeanPostProcessor 實現了Spring BeanPostProcessor 接口。

如下爲 FescarContextBeanPostProcessor 實現。

@Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        if (bean instanceof FeignContext && !(bean instanceof FescarFeignContext)) {
            return new FescarFeignContext(getFescarFeignObjectWrapper(),
                    (FeignContext) bean);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName)
            throws BeansException {
        return bean;
    }

BeanPostProcessor 中的兩個方法能夠對 Spring 容器中的 Bean 作先後處理,postProcessBeforeInitialization 處理時機是初始化以前,postProcessAfterInitialization 的處理時機是初始化以後,這2個方法的返回值能夠是原先生成的實例 bean,或者使用 wrapper 包裝後的實例。

FescarContextBeanPostProcessor 將 FeignContext 包裝成 FescarFeignContext。

FescarBeanPostProcessor 將 FeignClient 根據是否繼承了LoadBalancerFeignClient 包裝成 FescarLoadBalancerFeignClient 和 FescarFeignClient。

FeignAutoConfiguration 中的 FeignContext 並無加 ConditionalOnXXX 的條件,因此 Fescar 採用預置處理的方式將 FeignContext 包裝成 FescarFeignContext。

@Bean
    public FeignContext feignContext() {
        FeignContext context = new FeignContext();
        context.setConfigurations(this.configurations);
        return context;
    }

而對於 Feign Client,FeignClientFactoryBean 中會獲取 FeignContext 的實例對象。對於開發者採用 @Configuration 註解的自定義配置的 Feign Client 對象,這裏會被配置到 builder,致使 FescarFeignBuilder 中加強後的 FescarFeignCliet 失效。FeignClientFactoryBean 中關鍵代碼以下:

/**
     * @param <T> the target type of the Feign client
     * @return a {@link Feign} client created with the specified data and the context information
     */
    <T> T getTarget() {
        FeignContext context = applicationContext.getBean(FeignContext.class);
        Feign.Builder builder = feign(context);

        if (!StringUtils.hasText(this.url)) {
            if (!this.name.startsWith("http")) {
                url = "http://" + this.name;
            }
            else {
                url = this.name;
            }
            url += cleanPath();
            return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
                    this.name, url));
        }
        if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
            this.url = "http://" + this.url;
        }
        String url = this.url + cleanPath();
        Client client = getOptional(context, Client.class);
        if (client != null) {
            if (client instanceof LoadBalancerFeignClient) {
                // not load balancing because we have a url,
                // but ribbon is on the classpath, so unwrap
                client = ((LoadBalancerFeignClient)client).getDelegate();
            }
            builder.client(client);
        }
        Targeter targeter = get(context, Targeter.class);
        return (T) targeter.target(this, builder, context, new HardCodedTarget<>(
                this.type, this.name, url));
    }

上述代碼根據是否指定了註解參數中的 URL 來選擇直接調用 URL 仍是走負載均衡,targeter.target 經過動態代理建立對象。大體過程爲:將解析出的feign方法放入map,再經過將其做爲參數傳入生成InvocationHandler,進而生成動態代理對象。

FescarContextBeanPostProcessor 的存在,即便開發者對 FeignClient 自定義操做,依舊能夠完成 Fescar 所需的全局事務的加強。

對於 FescarFeignObjectWrapper,咱們重點關注下Wrapper方法:

Object wrap(Object bean) {
        if (bean instanceof Client && !(bean instanceof FescarFeignClient)) {
            if (bean instanceof LoadBalancerFeignClient) {
                LoadBalancerFeignClient client = ((LoadBalancerFeignClient) bean);
                return new FescarLoadBalancerFeignClient(client.getDelegate(), factory(),
                        clientFactory(), this.beanFactory);
            }
            return new FescarFeignClient(this.beanFactory, (Client) bean);
        }
        return bean;
    }

wrap 方法中,若是 bean 是 LoadBalancerFeignClient 的實例對象,那麼首先經過 client.getDelegate() 方法將 LoadBalancerFeignClient 代理的實際 Client 對象取出後包裝成 FescarFeignClient,再生成 LoadBalancerFeignClient 的子類 FescarLoadBalancerFeignClient 對象。若是 bean 是 Client 的實例對象且不是 FescarFeignClient LoadBalancerFeignClient,那麼 bean 會直接包裝生成 FescarFeignClient。

上面的流程設計仍是比較巧妙的,首先根據 Spring boot 的 Auto Configuration 控制了配置的前後順序,同時自定義了 Feign Builder的Bean,保證了 Client 均是通過加強後的 FescarFeignClient 。再經過 BeanPostProcessor 對Spring 容器中的 Bean 作了一遍包裝,保證容器內的Bean均是加強後 FescarFeignClient ,避免 FeignClientFactoryBean getTarget 方法的替換動做。

Hystrix 隔離

下面咱們再來看下 Hystrix 部分,爲何要單獨把 Hystrix 拆出來看呢,並且 Fescar 代碼也單獨實現了個策略類。目前事務上下文 RootContext 的默認實現是基於 ThreadLocal 方式的 ThreadLocalContextCore,也就是上下文實際上是和線程綁定的。Hystrix 自己有兩種隔離狀態的模式,基於信號量或者基於線程池進行隔離。Hystrix 官方建議是採起線程池的方式來充分隔離,也是通常狀況下在採用的模式:

Thread or Semaphore
The default, and the recommended setting, is to run HystrixCommands using thread isolation (THREAD) and HystrixObservableCommands using semaphore isolation (SEMAPHORE).

Commands executed in threads have an extra layer of protection against latencies beyond what network timeouts can offer.

Generally the only time you should use semaphore isolation for HystrixCommands is when the call is so high volume (hundreds per second, per instance) that the overhead of separate threads is too high; this typically only applies to non-network calls.

service 層的業務代碼和請求發出的線程確定不是同一個,那麼 ThreadLocal 的方式就沒辦法將 XID 傳遞給 Hystrix 的線程並傳遞給被調用方的。怎麼處理這件事情呢,Hystrix 提供了個機制讓開發者去自定義併發策略,只須要繼承 HystrixConcurrencyStrategy 重寫 wrapCallable 方法便可。

public class FescarHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {

    private HystrixConcurrencyStrategy delegate;

    public FescarHystrixConcurrencyStrategy() {
        this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
        HystrixPlugins.reset();
        HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
    }

    @Override
    public <K> Callable<K> wrapCallable(Callable<K> c) {
        if (c instanceof FescarContextCallable) {
            return c;
        }

        Callable<K> wrappedCallable;
        if (this.delegate != null) {
            wrappedCallable = this.delegate.wrapCallable(c);
        }
        else {
            wrappedCallable = c;
        }
        if (wrappedCallable instanceof FescarContextCallable) {
            return wrappedCallable;
        }

        return new FescarContextCallable<>(wrappedCallable);
    }

    private static class FescarContextCallable<K> implements Callable<K> {

        private final Callable<K> actual;
        private final String xid;

        FescarContextCallable(Callable<K> actual) {
            this.actual = actual;
            this.xid = RootContext.getXID();
        }

        @Override
        public K call() throws Exception {
            try {
                RootContext.bind(xid);
                return actual.call();
            }
            finally {
                RootContext.unbind();
            }
        }

    }
}

Fescar 也提供一個 FescarHystrixAutoConfiguration,在存在 HystrixCommand 的時候生成FescarHystrixConcurrencyStrategy。

@Configuration
@ConditionalOnClass(HystrixCommand.class)
public class FescarHystrixAutoConfiguration {

    @Bean
    FescarHystrixConcurrencyStrategy fescarHystrixConcurrencyStrategy() {
        return new FescarHystrixConcurrencyStrategy();
    }

}

參考資料

本文做者

郭樹抗,社區暱稱 ywind,曾就任於華爲終端雲,現搜狐智能媒體中心Java工程師,目前主要負責搜狐號相關開發,對分佈式事務、分佈式系統和微服務架構有異常濃厚的興趣。

季敏(清銘),社區暱稱 slievrly,Fescar 開源項目負責人,阿里巴巴中件間 TXC/GTS 核心研發成員,長期從事於分佈式中間件核心研發工做,在分佈式事務領域有着較豐富的技術積累。

延伸閱讀

微服務架構下,解決數據一致性問題的實踐



本文做者:中間件小哥

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索