11 【分布式事務(wù)----LCN】LCN原理及使用方式

https://zhuanlan.zhihu.com/p/87347441

微服務(wù)興起,分布式事務(wù)也成為亟需解決的難題,業(yè)界解決方案很多,今天介紹一個(gè)我目前覺得最好用的TX-LCN。

官網(wǎng)地址:http://www.txlcn.org/zh-cn/

一、TX-LCN介紹

image
image
image

TX-LCN由兩大模塊組成, TxClient、TxManager,TxClient就是你自己的服務(wù),TxManager作為分布式事務(wù)的服務(wù)端。事務(wù)發(fā)起方或者參與反都由TxClient端來控制。

ServerA調(diào)用ServerB,同屬于一個(gè)共同業(yè)務(wù)邏輯,比如買東西的業(yè)務(wù)流程:下單(訂單服務(wù))-扣除錢包金額(錢包服務(wù))-減庫存(庫存服務(wù)),涉及到3個(gè)服務(wù)的調(diào)用,這個(gè)買東西的操作,下單-扣錢-減庫存三個(gè)操作必須保證原子性。這里涉及到3個(gè)服務(wù)怎么保證原子性呢?

TX-LCN是這樣處理的:

ServerA(事務(wù)發(fā)起方)發(fā)起調(diào)用時(shí),創(chuàng)建一個(gè)事務(wù)組,會(huì)生成一個(gè)唯一的GroupId,這個(gè)GroupId會(huì)順著服務(wù)調(diào)用鏈傳遞,每調(diào)用一個(gè)參與方服務(wù),就會(huì)把這個(gè)參與方的事務(wù)信息通知給TxManager,加入該事務(wù)組。發(fā)起方收到調(diào)用返回(有可能是成功執(zhí)行或者報(bào)錯(cuò)),將發(fā)起方執(zhí)行結(jié)果狀態(tài)通知給TxManager,TxManager將根據(jù)事務(wù)最終狀態(tài)和事務(wù)組的信息來通知相應(yīng)的參與模塊提交或回滾事務(wù),并返回結(jié)果給事務(wù)發(fā)起方。

二、版本說明

本文主要環(huán)境及依賴版本:

JDK8

SpringBoot - 2.1.5.RELEASE

SpringCloud - Greenwich.SR1

Spring-cloud-alibaba-dependencies - 0.9.0.RELEASE

TX-LCN - 5.0.2.RELEASE

整合的Spring-cloud-alibaba,注冊(cè)中心用的Nacos,TX-LCN官網(wǎng)只有Dubbo和Consul的實(shí)例,不過差別不太大。

三、準(zhǔn)備數(shù)據(jù)庫

創(chuàng)建數(shù)據(jù)庫:tx-manager

數(shù)據(jù)庫腳本:

CREATE TABLE `t_tx_exception`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `group_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `unit_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `mod_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `transaction_state` tinyint(4) NULL DEFAULT NULL,
  `registrar` tinyint(4) NULL DEFAULT NULL,
  `remark` varchar(4096) NULL DEFAULT  NULL,
  `ex_state` tinyint(4) NULL DEFAULT NULL COMMENT '0 未解決 1已解決',
  `create_time` datetime(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

自己的服務(wù)端的數(shù)據(jù)庫就自行設(shè)計(jì)了。

啟動(dòng)本地的Redis,TxManager默認(rèn)配置就是連接了本地Redis。TxManager是基于Redis做統(tǒng)一事務(wù)控制的。

四、準(zhǔn)備TxManager

TxManager是一個(gè)單獨(dú)的服務(wù)端。

從這里下載官網(wǎng)Demo:codingapi/txlcn-demo

image

我們只用到這里的這個(gè)Module - 也就是TM。其他的是官方示例,可以看看。

修改application.properties配置:

spring.application.name=TransactionManager
server.port=7970
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://你的數(shù)據(jù)庫服務(wù)地址:3306/tx-manager?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=update

mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.use-generated-keys=true
# 開啟日志
tx-lcn.logger.enabled=true
logging.level.com.codingapi=debug

然后啟動(dòng)服務(wù),啟動(dòng)類:TransactionManagerApplication

image

五、微服務(wù)整合TX-LCN

進(jìn)行這一步的前提是,你的微服務(wù)是搭建好的,能正常運(yùn)行,包括服務(wù)注冊(cè)中心,服務(wù)之間Feign調(diào)用是測(cè)試通過了的。然后就在這基礎(chǔ)上整合。我這里注冊(cè)中心是Nacos。

模擬場(chǎng)景:ServerA要通過Feign調(diào)用ServerB,在A服務(wù)的方法里進(jìn)行數(shù)據(jù)插入,方法最后調(diào)用Feign請(qǐng)求B服務(wù),B服務(wù)的方法也進(jìn)行數(shù)據(jù)插入。結(jié)尾拋一個(gè)異常,查看兩個(gè)服務(wù)的插入數(shù)據(jù)是否都回滾了

1、加依賴

在2個(gè)服務(wù)都加上依賴:

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tc</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-txmsg-netty</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

2、加注解注解

2個(gè)服務(wù)的應(yīng)用啟動(dòng)類都加上注解:

@EnableDistributedTransaction

3、寫配置

指定TM地址:

tx-lcn:
  client:
    manager-address: 127.0.0.1:8070

開啟Feign的熔斷Fallback:

feign:
  hystrix:
    enabled: true

關(guān)閉Ribbon重試機(jī)制:

ribbon:
  ConnectTimeout: 5000
  ReadTimeout: 60000
  MaxAutoRetries: 0
  MaxAutoRetriesNextServer: 0

4、ServerA(服務(wù)發(fā)起方)

關(guān)于事務(wù)的配置:

@Configuration
@EnableTransactionManagement
public class AopTypeDTXConfiguration {

    /**
     * 本地事務(wù)配置
     *
     * @param transactionManager
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public TransactionInterceptor transactionInterceptor(PlatformTransactionManager transactionManager) {
        Properties properties = new Properties();
        properties.setProperty("*", "PROPAGATION_REQUIRED,-Throwable");
        TransactionInterceptor transactionInterceptor = new TransactionInterceptor();
        transactionInterceptor.setTransactionManager(transactionManager);
        transactionInterceptor.setTransactionAttributes(properties);
        return transactionInterceptor;
    }

    /**
     * 分布式事務(wù)配置 設(shè)置為LCN模式
     *
     * @param dtxLogicWeaver
     * @return
     */
    @ConditionalOnBean(DTXLogicWeaver.class)
    @Bean
    public TxLcnInterceptor txLcnInterceptor(DTXLogicWeaver dtxLogicWeaver) {
        TxLcnInterceptor txLcnInterceptor = new TxLcnInterceptor(dtxLogicWeaver);
        Properties properties = new Properties();
        properties.setProperty(Transactions.DTX_TYPE, Transactions.LCN);
        properties.setProperty(Transactions.DTX_PROPAGATION, "REQUIRED");
        txLcnInterceptor.setTransactionAttributes(properties);
        return txLcnInterceptor;
    }

    @Bean
    public BeanNameAutoProxyCreator beanNameAutoProxyCreator() {
        BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator();
        beanNameAutoProxyCreator.setInterceptorNames("txLcnInterceptor", "transactionInterceptor");
        beanNameAutoProxyCreator.setBeanNames("*Impl");
        return beanNameAutoProxyCreator;
    }
}

Feign:

@FeignClient(name = "common-user", fallback = UserServerFeignClientFallback.class)
public interface UserServerFeignClient {
    /** 
     * 分布式事物測(cè)試
     */
    @GetMapping("/user/v1/basic/distributedTest")
    void distributedTest();
}

Feign的Fallback類:

@Component
@Slf4j
public class UserServerFeignClientFallback implements UserServerFeignClient{

    @Override 
    public void distributedTest() {
        log.debug("Feign調(diào)用失敗,事務(wù)回滾!");
        DTXUserControls.rollbackGroup(TracingContext.tracing().groupId());
    }
}

這里我就不列Controller層代碼了,直接上Service層:

方法先插入了一條數(shù)據(jù),然后執(zhí)行Feign調(diào)用。

image

啟動(dòng)服務(wù),控制臺(tái)看到輸出:

image

與TM通信ok。

5、ServerB(服務(wù)參與方) 直接上Service層代碼:

image

最后拋個(gè)業(yè)務(wù)異常。 整合完畢。

六、測(cè)試

再次確保本地Redis是開啟的。 啟動(dòng)ServerB服務(wù)。 Postman測(cè)試,訪問ServerA的接口,查看ServerA輸出:

image

Feign的Fallback方法被調(diào)用了,并且沒有插入任何數(shù)據(jù)。說明ServerA調(diào)用ServerB,B拋了異常,導(dǎo)致A和B的事務(wù)都回滾了。

把這個(gè)異常注釋掉:

 @LcnTransaction
 @Transactional
 @Override
 public void distributedTest()  {
     BackendUser backendUser = new BackendUser();
     backendUser.setAccount("test");
     backendUser.setTel("15788888888");
     backendUser.setCreateTime(new Date());
     backendUser.setName("被調(diào)用者數(shù)據(jù)");
     backendUser.setRoleId(1);
     backendUser.setStatus(1);
     backendUser.setPassword("123456");
     backendUser.setRoleName("測(cè)試角色1");
     backendUserMapper.insert(backendUser);
//        throw new BusinessException(BusinessErrorCode.FAIL);
    }

重啟ServerB,測(cè)試,

數(shù)據(jù)庫插入了數(shù)據(jù):

image

測(cè)試完成,符合預(yù)期目標(biāo)。

TX-LCN就整合完成了!
訪問 http://localhost:7970/admin/index.html#/task
密碼 : codingapi

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容