SpringCloud分布式架构给我们带来开发上的便利,同时增加了我们对事务管理的难度,微服务的遍地开花,本地事务已经无法满足分布式的要求,由此分布式事务问题诞生。 分布式事务被称为世界性的难题。 更多分布式事务介绍请看这篇文章:再有人问你分布式事务,把这篇扔给他 本文记录整合TX-LCN分布式事务框架管理分布式事务,用的版本是5.0.2.RELEASE
在分布式产生之前,互联网公司的项目都是传统的单体项目,整个项目都共用了一个数据源,那么进行业务执行对数据库进行操作的时候,不会产生这种事务不一致问题,因为是同一个数据源用的一个本地事务。
但随着我们架构的演变,从单体架构到分布式架构到SOA架构项目再到如今公司采用的微服务架构,我们对服务进行了业务拆分,那么数据源也被拆分,一般微服务项目是一个服务对应一个数据源。
那么在这种架构下,每个服务都有自己独立的数据源有自己的本地事务,从而便会产生分布式事务,可能造成服务间数据不一致问题。
在单体的项目中,多个不同业务逻辑都是在同一个数据源中实现事务管理,是不存在分布式事务的问题,因为同一数据源的情况下都是采用事务管理器,相当于每个事务管理器对应一个数据源
在单体的项目中,有多个不同的数据源,每个数据源中都有自己独立的事务管理器,互不影响,那么这时候也会存在多数据源事务管理:解决方案jta+ Atomikos。
在分布式/微服务架构,每个服务都有自己独立的数据源和事务管理器,那么在这种情况下如果有业务要进行RPC远程调用的时候,那就必然可能产生分布式事务。目前主要解决方案有:MQ、LCN、Seata等方案。
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模式是通过代理Connection的方式实现对本地事务的操作,然后在由TxManager统一协调控制事务。当本地事务提交回滚或者关闭连接时将会执行假操作,该代理的连接将由LCN连接池管理。 该模式的特点:
该模式对代码的嵌入性为低。该模式仅限于本地存在连接对象且可通过连接对象控制事务的模块。该模式下的事务提交与回滚是由本地事务方控制,对于数据一致性上有较高的保障。该模式缺陷在于代理的连接需要随事务发起方一共释放连接,增加了连接占用的时间。TCC事务机制相对于传统事务机制(X/Open XA Two-Phase-Commit),其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。主要由三步操作,Try: 尝试执行业务、 Confirm:确认执行业务、 Cancel: 取消执行业务。
该模式的特点:
该模式对代码的嵌入性高,要求每个业务需要写三种步骤的操作。该模式对有无本地事务控制都可以支持使用面广。数据一致性控制几乎完全由开发者控制,对业务开发难度要求高。TXC模式命名来源于淘宝,实现原理是在执行SQL之前,先查询SQL的影响数据,然后保存执行的SQL快走信息和创建锁。当需要回滚的时候就采用这些记录数据回滚数据库,目前锁实现依赖redis分布式锁控制。 该模式的特点:
该模式同样对代码的嵌入性低。该模式仅限于对支持SQL方式的模块支持。该模式由于每次执行SQL之前需要先查询影响数据,因此相比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协调者再告知所有的参与方进行真正的回滚操作,这样就解决了分布式事务的问题
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注意:个人修改了数据库的名称,和用户名密码,根据自己的实际情况修改
不知道怎么启动的可以自行查阅Springboot运行方式 启动后打开后台地址http://localhost:7970,初始密码是codingapi,我这里改成了123456 登陆后
TC端参照官网一步步操作:https://www.txlcn.org/zh-cn/docs/start.html
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>在提交本地事务的地方添加@LcnTransaction,分布式事务注解,PS:@LcnTransaction的target是在方法上的,@Target({ElementType.METHOD})
我们挑选之前的两个项目myspringboot、springdatejpa,按照步骤设置成TC, 并且在两个TC添加测试接口
这个原先就已经有对应的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