https://zhuanlan.zhihu.com/p/87347441
微服務(wù)興起,分布式事務(wù)也成為亟需解決的難題,業(yè)界解決方案很多,今天介紹一個(gè)我目前覺得最好用的TX-LCN。
官網(wǎng)地址:http://www.txlcn.org/zh-cn/
一、TX-LCN介紹



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

我們只用到這里的這個(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

五、微服務(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)用。

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

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

最后拋個(gè)業(yè)務(wù)異常。 整合完畢。
六、測(cè)試
再次確保本地Redis是開啟的。 啟動(dòng)ServerB服務(wù)。 Postman測(cè)試,訪問ServerA的接口,查看ServerA輸出:

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ù):

測(cè)試完成,符合預(yù)期目標(biāo)。
TX-LCN就整合完成了!
訪問 http://localhost:7970/admin/index.html#/task
密碼 : codingapi