LCN解决分布式事务原理解析+项目实战(原创精华版)
前言
SpringCloud分布式架构给我们带来开发上的便利,同时增加了我们对事务管理的难度,微服务的遍地开花,本地事务已经无法满足分布式的要求,由此分布式事务问题诞生。 分布式事务被称为世界性的难题。
更多分布式事务介绍请看这篇文章:再有人问你分布式事务,把这篇扔给他
本文记录整合TX-LCN分布式事务框架管理分布式事务,用的版本是5.0.2.RELEASE
了解分布式事务
分布式事务产生的背景
在分布式产生之前,互联网公司的项目都是传统的单体项目,整个项目都共用了一个数据源,那么进行业务执行对数据库进行操作的时候,不会产生这种事务不一致问题,因为是同一个数据源用的一个本地事务。
但随着我们架构的演变,从单体架构到分布式架构到SOA架构项目再到如今公司采用的微服务架构,我们对服务进行了业务拆分,那么数据源也被拆分,一般微服务项目是一个服务对应一个数据源。
那么在这种架构下,每个服务都有自己独立的数据源有自己的本地事务,从而便会产生分布式事务,可能造成服务间数据不一致问题。
不同架构体系下解决事务的问题
单体架构(单数据源)
在单体的项目中,多个不同业务逻辑都是在同一个数据源中实现事务管理,是不存在分布式事务的问题,因为同一数据源的情况下都是采用事务管理器,相当于每个事务管理器对应一个数据源
单体架构(多数据源)
在单体的项目中,有多个不同的数据源,每个数据源中都有自己独立的事务管理器,互不影响,那么这时候也会存在多数据源事务管理:解决方案jta+ Atomikos。
分布式/微服务架构
在分布式/微服务架构,每个服务都有自己独立的数据源和事务管理器,那么在这种情况下如果有业务要进行RPC远程调用的时候,那就必然可能产生分布式事务。目前主要解决方案有:MQ、LCN、Seata等方案。
LCN简介和背景
LCN分布式事务框架其本身并不创建事务,而是基于对本地事务的协调从而达到事务一致性的效果。
LCN框架在2017年6月份发布第一个版本,从开始的1.0,已经发展到了5.0版本。
LCN名称是由早期版本的LCN框架命名,在设计框架之初的1.0 ~ 2.0的版本时框架设计的步骤是如下,各取其首字母得来的LCN命名。
5.0以后由于框架兼容了LCN、TCC、TXC三种事务模式,为了避免区分LCN模式,特此将LCN分布式事务改名为TX-LCN分布式事务框架。
TX-LCN分布式事务框架,LCN并不生产事务,LCN只是本地事务的协调工,LCN是一个高性能的分布式事务框架,兼容dubbo、springcloud框架,支持RPC框架拓展,支持各种ORM框架、NoSQL、负载均衡、事务补偿
定位
TX-LCN定位于一款事务协调性框架,框架其本身并不操作事务,而是基于对事务的协调从而达到事务一致性的效果。(LCN不生产事务,它只是事务的搬运工...woc这有点像农夫山泉的文案)
LCN模式
LCN模式是通过代理Connection的方式实现对本地事务的操作,然后在由TxManager统一协调控制事务。当本地事务提交回滚或者关闭连接时将会执行假操作,该代理的连接将由LCN连接池管理。
该模式的特点:
- 该模式对代码的嵌入性为低。
- 该模式仅限于本地存在连接对象且可通过连接对象控制事务的模块。
- 该模式下的事务提交与回滚是由本地事务方控制,对于数据一致性上有较高的保障。
- 该模式缺陷在于代理的连接需要随事务发起方一共释放连接,增加了连接占用的时间。
TCC模式
TCC事务机制相对于传统事务机制(X/Open XA Two-Phase-Commit),其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。主要由三步操作,Try: 尝试执行业务、 Confirm:确认执行业务、 Cancel: 取消执行业务。
该模式的特点:
- 该模式对代码的嵌入性高,要求每个业务需要写三种步骤的操作。
- 该模式对有无本地事务控制都可以支持使用面广。
- 数据一致性控制几乎完全由开发者控制,对业务开发难度要求高。
TXC模式
TXC模式命名来源于淘宝,实现原理是在执行SQL之前,先查询SQL的影响数据,然后保存执行的SQL快走信息和创建锁。当需要回滚的时候就采用这些记录数据回滚数据库,目前锁实现依赖redis分布式锁控制。
该模式的特点:
- 该模式同样对代码的嵌入性低。
- 该模式仅限于对支持SQL方式的模块支持。
- 该模式由于每次执行SQL之前需要先查询影响数据,因此相比LCN模式消耗资源与时间要多。
- 该模式不会占用数据库的连接资源。
LCN分布式事务原理(自己理解的,白话文通俗易懂)
1) 首先我们的lcn协调者(TM)会和lcn客户端(TC)通过引入的netty一直保持着长连接(持续监听)。
2) 当请求的发起方(调用方)进入接口业务之前,会通过AOP技术进到@LcnTransaction注解中去LCN协调者那边生成注册一个全局的事务组Id(groupId)。
3) 当发起方(调用方)通过rpc调用参与方(被调用方)的时候,lcn重写了Feign客户端,会从ThreadLocal中拿到该事务组Id(groupId),并将该事务组Id设置到请求头中。
4) 参与方(被调用方)在请求头中获取到了这个groupId的时候,lcn会标识该服务为参与方并加入到该事务组,并会被lcn代理数据源,当该服务业务逻辑执行完成后,进行数据源的假关闭,并不会真正的提交或回滚当前服务的事务。
5) 当发起方执行完全部业务逻辑的时候,如果无异常会告知lcn协调者,lcn协调者再分别告诉该请求链上的所有参与方可以提交了,再进行真正的提交。若发起方调用完参与方后报错了,也会告知lcn协调者,lcn协调者再告知所有的参与方进行真正的回滚操作,这样就解决了分布式事务的问题
快速撸代码
搭建tx-manager
创建数据库、表
- 创建MySQL数据库, 名称为:tx-manager(我们直接选择在我们自己的数据库下面创建表就行了,这里就不创建这个数据库)
- 创建数据表:t_tx_exception
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 NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
https://img2018.cnblogs.com/blog/1353055/201906/1353055-20190620151602501-133032900.png
下载源码并编译
源码下载地址:https://github.com/codingapi/tx-lcn
工程目录
修改txlcn-tm的配置文件application.properties
####################### 服务 ############################################
spring.application.name=TransactionManager
server.port=7970
####################### 数据库 ############################################
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://47.92.145.192:3306/scm_transaction?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
spring.datasource.username=db_user2
spring.datasource.password=db_pass
# 验证连接是否有效。此参数必须设置为非空字符串,下面三项设置成true才能生
spring.datasource.validationQuery=SELECT 1
# 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除.
spring.datasource.testWhileIdle=true
# 指明是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个
spring.datasource.testOnBorrow=true
# 指明是否在归还到池中前进行检验
spring.datasource.testOnReturn=false
# 以下可省略
# 初始化大小,最小,最大
spring.datasource.initialSize=5
spring.datasource.minIdle=10
spring.datasource.maxActive=1000
# 配置获取连接等待超时的时间
spring.datasource.maxWait=60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
#配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
#打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
#配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.filters=stat,wall,log4j
#通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=1000;druid.stat.logSlowSql=true
#合并多个DruidDataSource的监控数据
spring.datasource.useGlobalDataSourceStat=true
#spring.datasource.WebStatFilter.exclusions="*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
#spring.datasource.stat-view-servlet.login-username=admin
#spring.datasource.stat-view-servlet.login-password=admin
####################### 数据库方言 ############################################
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
# 第一次运行可以设置为: create, 为TM创建持久化数据库表
spring.jpa.hibernate.ddl-auto=update
####################### Redis ############################################
spring.redis.host=47.92.145.192
spring.redis.port=6379
spring.redis.password=WZTH@dev123
####################### 事务 ############################################
# TM监听IP. 默认为 127.0.0.1
tx-lcn.manager.host=127.0.0.1
# TM监听Socket端口. 默认为 ${server.port} - 100
tx-lcn.manager.port=8070
# 心跳检测时间(ms). 默认为 300000
tx-lcn.manager.heart-time=300000
# 分布式事务执行总时间(ms). 默认为36000
tx-lcn.manager.dtx-time=8000
# 参数延迟删除时间单位ms 默认为dtx-time值
tx-lcn.message.netty.attr-delay-time=${tx-lcn.manager.dtx-time}
# 事务处理并发等级. 默认为机器逻辑核心数5倍
tx-lcn.manager.concurrent-level=160
# TM后台登陆密码,默认值为codingapi
tx-lcn.manager.admin-key=123456
# 分布式事务锁超时时间 默认为-1,当-1时会用tx-lcn.manager.dtx-time的时间
tx-lcn.manager.dtx-lock-time=${tx-lcn.manager.dtx-time}
# 雪花算法的sequence位长度,默认为12位.
tx-lcn.manager.seq-len=12
# 异常回调开关。开启时请制定ex-url
tx-lcn.manager.ex-url-enabled=false
# 事务异常通知(任何http协议地址。未指定协议时,为TM提供内置功能接口)。默认是邮件通知
tx-lcn.manager.ex-url=/provider/email-to/306509906@qq.com
注意:个人修改了数据库的名称,和用户名密码,根据自己的实际情况修改
启动txlcn-tm模块
不知道怎么启动的可以自行查阅Springboot运行方式
启动后打开后台地址http://localhost:7970,初始密码是codingapi,我这里改成了123456
登陆后
搭建Tx-Client
TC端参照官网一步步操作:https://www.txlcn.org/zh-cn/docs/start.html
TC引入依赖
<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>
PS:如果你没有添加jdbc驱动,启动的时候会报错
Parameter 0 of constructor in com.codingapi.txlcn.tc.core.transaction.txc.analy.TableStructAnalyser required a bean of type 'javax.sql.DataSource' that could not be found.
因此要添加jdbc依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
配置文件添加TM地址跟监听端口,如果TM是默认8070端口,且跟TC部署在同一台机器,可以忽略这个配置,并且开启日志,开发阶段最好开启日志,并设置为debug等级,这样方便追踪排查问题
# 是否启动LCN负载均衡策略(优化选项,开启与否,功能不受影响)
tx-lcn.ribbon.loadbalancer.dtx.enabled=true
# 默认之配置为TM的本机默认端口
tx-lcn.client.manager-address=127.0.0.1:8070
# 开启日志,默认为false
tx-lcn.logger.enabled=true
tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name}
tx-lcn.logger.jdbc-url=${spring.datasource.url}
tx-lcn.logger.username=${spring.datasource.username}
tx-lcn.logger.password=${spring.datasource.password}
logging.level.com.codingapi.txlcn=DEBUG
在启动类上使用 @EnableDistributedTransaction
//省略其他代码...
@EnableDistributedTransaction
public class MyspringbootApplication {
public static void main(String[] args) {
SpringApplication.run(MyspringbootApplication.class, args);
}
}
在提交本地事务的地方添加@LcnTransaction,分布式事务注解,PS:@LcnTransaction的target是在方法上的,@Target({ElementType.METHOD})
演示demo
我们挑选之前的两个项目myspringboot、springdatejpa,按照步骤设置成TC,
并且在两个TC添加测试接口
myspringboot-controller
/**
* 测试分布式事务
*/
@GetMapping("feign/save")
Result<UserVo> save(UserVo userVo){
//模拟数据
Description description = new Description();
description.setUserId("111");
description.setDescription("测试用户描述");
Result<Description> save = descriptionService.save(description);
System.out.println(save);
return null;
}
myspringboot-service
@Override
@LcnTransaction//分布式事务
@Transactional //本地事务
public Result<Description> save(Description description) {
UserVo userVo = new UserVo();
userVo.setUsername("huanzi");
userVo.setPassword("123");
//调用springdatejpa服务保存userVo
Result<UserVo> result = myspringbootFeign.save(userVo);
System.out.println(result);
//myspringboot本地服务保存description
Description save = descriptionRepository.save(description);
System.out.println(save);
//模拟发生异常
throw new RuntimeException("business code error");
}
myspringboot-feign
@FeignClient(name = "springdatejpa", path = "/user/",fallback = MyspringbootFeignFallback.class,fallbackFactory = MyspringbootFeignFallbackFactory.class)
public interface MyspringbootFeign {
@RequestMapping(value = "save")
Result<UserVo> save(@RequestBody UserVo userVo);
}
springdatejpa
这个原先就已经有对应的save接口,其他的代码我们就不贴了,在UserServiceImpl类重写save方法,在save方法上添加@LcnTransaction注解
@LcnTransaction//分布式事务
@Transactional //本地事务
@Override
public Result<UserVo> save(UserVo entity) {
User user = userRepository.save(FastCopy.copy(entity, User.class));
return Result.of(FastCopy.copy(user, UserVo.class));
}
演示效果
启动所有项目,TM跟Redis服务也要记得启动
查看TM后台,可以看到成功注册了两个TC
访问http://localhost:10010/myspringboot/feign/save,被单点登录拦截,登录后跳转正常跳转该接口,这些就不再演示了,下面直接看后台debug日志
调用流程
myspringboot(A) ---> springdatejpa(B)
事务回滚
myspringboot(A)
springdatejpa(B)
到这里springdatejpa(B)已经响应了user数据给myspringboot(A),而后收到回滚通知
事务提交
我们看一下事务正常提交是怎么样的,我们把模拟异常注释起来,并返回保存后的数据
//模拟发生异常
//throw new RuntimeException("business code error");
return Result.of(save);
我们直接从springdatejpa(B)响应数据之后看myspringboot(A)
springdatejpa(B)
后记
要注意我们的springboot版本跟txlcn的版本是不是兼容,按照官网的快速开始(https://www.txlcn.org/zh-cn/docs/start.html),以及参考官方例子(https://github.com/codingapi/txlcn-demo),一路下来碰到了一下小问题在这里总结一下:
1、A调B,A抛出异常,A事务回滚,B事务没有回滚
原因:这个是因为刚开始我是在A的controller层调用B,相当于B是一个单独是事务组,A又是一个单独的事务组
解决:在A开启事务后再调用B