https://zhuanlan.zhihu.com/p/87347441
微服务兴起,分布式事务也成为亟需解决的难题,业界解决方案很多,今天介绍一个我目前觉得最好用的TX-LCN。
官网地址:http://www.txlcn.org/zh-cn/
一、TX-LCN介绍
TX-LCN由两大模块组成, TxClient、TxManager,TxClient就是你自己的服务,TxManager作为分布式事务的服务端。事务发起方或者参与反都由TxClient端来控制。
ServerA调用ServerB,同属于一个共同业务逻辑,比如买东西的业务流程:下单(订单服务)-扣除钱包金额(钱包服务)-减库存(库存服务),涉及到3个服务的调用,这个买东西的操作,下单-扣钱-减库存三个操作必须保证原子性。这里涉及到3个服务怎么保证原子性呢?
TX-LCN是这样处理的:
ServerA(事务发起方)发起调用时,创建一个事务组,会生成一个唯一的GroupId,这个GroupId会顺着服务调用链传递,每调用一个参与方服务,就会把这个参与方的事务信息通知给TxManager,加入该事务组。发起方收到调用返回(有可能是成功执行或者报错),将发起方执行结果状态通知给TxManager,TxManager将根据事务最终状态和事务组的信息来通知相应的参与模块提交或回滚事务,并返回结果给事务发起方。
二、版本说明
本文主要环境及依赖版本:
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,注册中心用的Nacos,TX-LCN官网只有Dubbo和Consul的实例,不过差别不太大。
三、准备数据库
创建数据库:tx-manager
数据库脚本:
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;
自己的服务端的数据库就自行设计了。
启动本地的Redis,TxManager默认配置就是连接了本地Redis。TxManager是基于Redis做统一事务控制的。
四、准备TxManager
TxManager是一个单独的服务端。
从这里下载官网Demo:codingapi/txlcn-demo
我们只用到这里的这个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://你的数据库服务地址: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
然后启动服务,启动类:TransactionManagerApplication
五、微服务整合TX-LCN
进行这一步的前提是,你的微服务是搭建好的,能正常运行,包括服务注册中心,服务之间Feign调用是测试通过了的。然后就在这基础上整合。我这里注册中心是Nacos。
模拟场景:ServerA要通过Feign调用ServerB,在A服务的方法里进行数据插入,方法最后调用Feign请求B服务,B服务的方法也进行数据插入。结尾抛一个异常,查看两个服务的插入数据是否都回滚了
1、加依赖
在2个服务都加上依赖:
com.codingapi.txlcn
txlcn-tc
5.0.2.RELEASE
com.codingapi.txlcn
txlcn-txmsg-netty
5.0.2.RELEASE
2、加注解注解
2个服务的应用启动类都加上注解:
@EnableDistributedTransaction
3、写配置
指定TM地址:
tx-lcn:
client:
manager-address: 127.0.0.1:8070
开启Feign的熔断Fallback:
feign:
hystrix:
enabled: true
关闭Ribbon重试机制:
ribbon:
ConnectTimeout: 5000
ReadTimeout: 60000
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 0
4、ServerA(服务发起方)
关于事务的配置:
@Configuration
@EnableTransactionManagement
public class AopTypeDTXConfiguration {
/**
* 本地事务配置
*
* @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;
}
/**
* 分布式事务配置 设置为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 {
/**
* 分布式事物测试
*/
@GetMapping("/user/v1/basic/distributedTest")
void distributedTest();
}
Feign的Fallback类:
@Component
@Slf4j
public class UserServerFeignClientFallback implements UserServerFeignClient{
@Override
public void distributedTest() {
log.debug("Feign调用失败,事务回滚!");
DTXUserControls.rollbackGroup(TracingContext.tracing().groupId());
}
}
这里我就不列Controller层代码了,直接上Service层:
方法先插入了一条数据,然后执行Feign调用。
启动服务,控制台看到输出:
与TM通信ok。
5、ServerB(服务参与方) 直接上Service层代码:
最后抛个业务异常。 整合完毕。
六、测试
再次确保本地Redis是开启的。 启动ServerB服务。 Postman测试,访问ServerA的接口,查看ServerA输出:
Feign的Fallback方法被调用了,并且没有插入任何数据。说明ServerA调用ServerB,B抛了异常,导致A和B的事务都回滚了。
把这个异常注释掉:
@LcnTransaction
@Transactional
@Override
public void distributedTest() {
BackendUser backendUser = new BackendUser();
backendUser.setAccount("test");
backendUser.setTel("15788888888");
backendUser.setCreateTime(new Date());
backendUser.setName("被调用者数据");
backendUser.setRoleId(1);
backendUser.setStatus(1);
backendUser.setPassword("123456");
backendUser.setRoleName("测试角色1");
backendUserMapper.insert(backendUser);
// throw new BusinessException(BusinessErrorCode.FAIL);
}
重启ServerB,测试,
数据库插入了数据:
测试完成,符合预期目标。
TX-LCN就整合完成了!
访问 http://localhost:7970/admin/index.html#/task
密码 : codingapi