基于SpringCloud的分布式事务框架(LCN)
- 第一章 绪论
- 研究背景
Saga是1987年Hector&kenneth发表的一篇数据库论文Sagas里提到的一个概念。在论文中一个Saga事务是由多个本地事务所组成,每个本地事务有相应的执行模块和补偿模块,当saga事务中的任意一个本地事务出错了,可以通过调用相关的补充方法恢复之前的事务,达到事务的最终一致性。
Saga概念虽然提出来快30年了, 随着微服务的复杂,引出了分布式Saga问题,近些年也逐步受到大家的关注。
参考Caitie McCaffrey 在分布式Saga论文,以及Chris Richardson的研究,在Caitie的论文中对被Saga调用的服务提出两点要求,我们需要Saga调用的服务支持幂等。在服务请求的过程中,可能会出现超时重试的情况,我们需要通过幂等来避免多次请求所带来的问题。
对于重试取消的情况,补偿可交换原则是指Saga并行处理的过程中,如果发生了超时重试事件之后,并进行了补偿的操作,那么补偿操作是直接生效的。为了满足这个要求,需要我们在设计系统的过程中保留所有的事务数据。
因为saga事务没有准备阶段,事务没有隔离,如果两个saga事务同时操作同一资源就会遇到我们操作多线程临界资源的情况。因此会产生更新丢失,脏数据读取等问题。
为了解决隔离性带来的问题,我们可以参考一下TCC的解决方案,从业务层面入手加入Session以及锁的机制来保证能够串行化操作资源。在业务操作的过程中可以通过及时读取当前状态的方式获取到最新的更新。
目前业界提供了两类Saga的实现方式。 一个是集中式协调器的实现方式,一个分布式的实现方式。
- 集中式的实现方式:集中式协调器负责服务调用以及事务协调
- 分布式的实现方式:通过事件驱动的方式进行事务协调。
集中式的Saga实现一般是通过一个Saga对象来追踪所有的Saga子任务的调用情况,根据调用情况来决定是否需要调用对应的补偿方面,协调器和调用方是在一个进程中的。
布式的Saga一般是采用事件驱动方式让参与的服务方进行相关的交互。相关的业务方只需要订阅相关的领域事件即可。Chirs提供了基于事件溯源的实现,同时axonframework也提供了相关的实现。
分布式saga实现的好处: 采用事件源的方式降低系统复杂程度,提升系统扩展性,处理模块通过订阅事件的方式降低系统的耦合程度。
当然这也的实现也有一些问题:saga系统会涉及大量的业务事件,这样会对编码和调试会带来一些问题,还有就是相关的业务逻辑处理是基于事件,相关事件处理模块可能会有循环依赖的问题。
-
- 研究现状
- ServiceComb的Saga实现
- 研究现状
ServiceComb是2017年5月份华为开源的微服务框架,是华为云微服务框架引擎重要的一个组成部分。
目前ServiceComb主要有三个项目组成:一、ServiceCenter做服务发现的,是go语言在etcd 基础实现的服务注册中心;二、Java Chassis是java的一个微服务框架,是基于vertx的基础上实现了全异步操作接口;三、Saga项目是针对Saga模式提供的一个实现。
集中式的Saga协调器构建过程参考了Caitie的论文,实现了一个集中式Saga调用协调器。后来受到Zipkin的启发,又实现了一个分布式的Saga协调器。
- 集中式的协调器
集中式的协调器,包含了Saga调用请求接收,分析,以及执行和结果查询这部分的内容。任务代理模块需要预先知道Saga事务调用关系图。执行模块根据生成的调用图产生调用任务,调用相关微服务服务接口。如果服务调用执行出错,会调用服务的相关的补偿方法回滚。
Saga执行模块通过分析请求的Json数据,构建一个调用关系图,这里我们是通过JSon来描述Saga事务串行调用子事务或者并行调用子事务。关系调用图被Saga实现中的任务运行模块分解成为一个一个执行任务,执行任务由任务消费者获取并生成相关的调用 (这里同时支持串行和并行调用)。
Saga任务会根据执行的情况向Saga log中记录对应的Saga事务的关键事件,同时我们的事件查看器查看到Saga事务相关的执行情况。后续我们采用了Actor模型对任务的调度模块进行了重构,在不进行调优的情况下,系统性能提升一倍。
集中式Saga的实现的好处是易于监控和协调, 但是坏处就是需要依赖工具对Saga调用进行相关的描述。
- 分布式的Saga协调器
借助Zipkin实现了一套Saga调用关系追踪的模块。为了实现这个事务追踪模型,需要在应用端部署相关的监控模块,同时监控模块需要和后台进行协同交互,架构如下。
Omega会以切面编程的方式向应用程序注入相关的处理模块。这里有拦截请求的模块, 用来帮助我们构建分布式事务调用的上下文。 同时在事务处理初始阶段处理事务的相关准备的操作,例如创建Saga 起始事件,以及相关的子起始事件,根据事务的执行的成功或者失败生产相关的事务终止或者失败事件。
Omega会与Alpha进行链接会把这些事件通知给Alpha。Alpha可以在后台进行分析,根据Saga事务执行的情况给Omega下达相关的指令进行相关的回滚恢复。
这样设计的好处是Saga实现代码与用户的代码分离, 用户只需要添加几个annotation,Saga实现就能Saga事件的执行情况并进行相关的处理。
这是Omega和Alpha之间的正常业务逻辑的交互图,Omega通过分析调用上下文决定是否发生Saga事务的起始事件到Alpha,后续ServiceA在调用ServiceB,会将相关的调用上下文传递给ServiceB。ServiceB的Omega模块会截取这个调用上下文生成相关子事务事件信息。
如果服务调用的过程中抛出异常,Omega会将终止事件发送到Alpha端,Alpha的后台进程会定时做扫描,扫描过程中会发现有需要恢复的事件。Alpha会向Omega发消息调用相关的恢复操作,来保证整个Saga事务的原子性。目前Omega也开始提供重试功能,也就是事务调用如果失败了, Omega会根据设置进行重试尝试。
-
-
- Saca平台微服务事务管理
-
消息事务就是基于消息中间件的两阶段提交,实现对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性.
Saca平台微服务事务管理,基于业界应用最广泛的且适用于高扩展性分布式系统,创建事务最终一致性解决方案,该分布式事务管理方案,采用消息事务+最终一致性的策略。
最终一致性事务保证方案:
跨服务调用采取消息中间件传递(实现异步),使用单服务事务保证消息可靠传输(不丢、不重),跨服务调用的每一步,都是由四个单服务事务保证一致性的。
具体实现步骤如下:每个微服务应用,需要建立事件表;每个参与事务的业务方法,除了写入业务表外,还需要写入事件表;每个参与事务的业务方法,需要将直接调用转成发送消息中间件消息;每个微服务应用需要自行启动独立线程,处理事件接收与发送动作;微服务多实例(弹性扩展)时,需要保证事件发送与接收的唯一性;保证正逻辑、反逻辑消息顺序;在参与事务的每个步骤方法上,增加@BaseTransaction注解。
该方案采用最终一致的,牺牲了一致性,换来了性能的大幅度提升。存在造成数据不一致的风险
分布式事物问题,在互联网公司比较常见,分布式事物解决方案 可以使用全局事物2pc(两段提交协议)、3pc(三段提交协议),消息中间件、tcc、gts、提供回滚接口、分布式数据库等。
基于SpringCloud的分布式事务框架LCN,是基于TCP长连接的socket通讯,TxManager与事务控制方是基于netty框架完成的,LCN 核心采用3PC+TCC补偿机制。兼容SpringCloud、Dubbo,使用简单,低依赖,并且代码完全开源,LCN基于切面的强一致性事务框架,具有高可用特性,模块可以依赖Dubbo或SpringCloud的集群方式做集群化,TxManager也可以做集群化,同时支持本地事务和分布式事务共存,支持事务补偿机制,服务故障或挂机再启动时可恢复事务。
对比LCN和saga(华为apache孵化器项目)及其他基于消息队列实现的分布式事务控制机制,LCN使用代理连接池封装补偿方法,saga需要手工写补偿方法,相对来说LCN使用更加方便。
- 相关技术介绍
- 二阶段提交协议(2PC)
二阶段提交协议主要分为两个阶段:准备阶段和提交阶段。
协调者询问参与者是否能执行事务提交操作。如果参与者能够执行事务的提交,先执行事务操作,然后返回YES,如果没有成功执行事务操作,就返回NO。
当协调者接收到所有的参与者的反馈之后,开始进入事务提交阶段。如果所有参与者都返回YES,那就发送COMMIT请求,如果有一个人返回NO,那就返送roolback请求。
二阶段提交协议的第一阶段准备阶段不仅仅是回答YES or NO,还是要执行事务操作的,只是执行完事务操作,并没有进行commit或是roolback。一旦事务执行之后,在没有执行commit或者roolback之前,资源是被锁定的,这会造成阻塞。
2PC在执行过程中可能发生协调者或者参与者突然宕机的情况,在不同时期宕机可能有不同的现象。2PC协议中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致。
为了解决这个问题,衍生出了3PC。
-
- 三阶段提交协议(3PC)
3PC把2PC的准备阶段一分为二,这样三阶段提交就有CanCommit、preCommit、DoCommit三个阶段,在第一阶段询问所有参与者是否可以执行事务操作,并不在本阶段执行事务操作,当协调者收到所有参与者都返回yes时,在第二阶段才执行事务操作,然后在第三阶段执行commit或rollback.
- 3PC为什么比2PC好
直接分析协调者和参与者都挂的情况,看上去和二阶段提交的那种数据不一致的情况的现象是一样的,但仔细分析所有参与者的状态的话就会发现其实并不一样。我们假设挂掉的那台参与者执行的操作是commit。那么其他没挂的操作者的状态应该是什么?他们的状态要么是prepare-commit要么是commit。因为3PC的第三阶段一旦有机器执行了commit,那必然第一阶段大家都是同意commit。所以,这时,新选举出来的协调者一旦发现未挂掉的参与者中有人处于commit状态或者是prepare-commit的话,那就执行commit操作。否则就执行rollback操作。这样挂掉的参与者恢复之后就能和其他机器保持数据一致性了。
所以,再多引入一个阶段之后,3PC解决了2PC中存在的那种由于协调者和参与者同时挂掉有可能导致的数据一致性问题。
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。所以,由于网络原因,协调者发送的abort(rollback)响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
-
- CPA理论
CAP由Eric Brewer在2000年PODC会议上提出,是Eric Brewer在Inktomi期间研发搜索引擎、分布式web缓存时得出的关于数据一致性(consistency)、服务可用性(availability)、分区容错性(partition-tolerance)的猜想:
• 数据一致性(consistency):如果系统对一个写操作返回成功,那么之后的读请求都必须读到这个新数据;如果返回失败,那么所有读操作都不能读到这个数据,对调用者而言数据具有强一致性(strong consistency) (又叫原子性 atomic、线性一致性 linearizable consistency)
• 服务可用性(availability):所有读写请求在一定时间内得到响应,可终止、不会一直等待
• 分区容错性(partition-tolerance):在网络分区的情况下,被分隔的节点仍能正常对外服务
-
- Base理论
BASE理论是指,Basically Available(基本可用)、Soft-state( 软状态/柔性事务)、Eventual Consistency(最终一致性)。是基于CAP定理演化而来,是对CAP中一致性和可用性权衡的结果。
核心思想:即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一致性。
- 基本可用
指分布式系统在出现故障的时候,允许损失部分可用性,保证核心可用。但不等价于不可用。比如:搜索引擎0.5秒返回查询结果,但由于故障,2秒响应查询结果;网页访问过大时,部分用户提供降级服务等。
- 软状态
软状态是指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点间副本同步的时候存在延时。
- 最终一致性
系统中的所有数据副本经过一定时间后,最终能够达到一致的状态,不需要实时保证系统数据的强一致性。最终一致性是弱一致性的一种特殊情况。
BASE理论面向的是大型高可用可扩展的分布式系统,通过牺牲强一致性来获得可用性。ACID,指数据库事务正确执行的四个基本要素的缩写。包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。ACID是传统数据库常用的概念设计,追求强一致性模型。
-
- 柔性事务类型
柔性事务满足BASE理论(基本可用,最终一致),刚性事务满足ACID理论。围绕分布式事务当中的柔性事务的处理方式进行讨论。
柔性事务分为:
- 两阶段型
- 补偿型
- 异步确保型
- 最大努力通知型几种。
例如支付宝的SOA架构体系,传统单机环境下数据库的ACID事务满足了分布式环境下的业务需要,以上几种事务类型就是针对分布式环境下业务需要设定的。
-
- TCC事务补偿机制
TCC编程模式
所谓的TCC编程模式,也是两阶段提交的一个变种。TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel三个操作。TCC通过代码人为实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样。
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:
- Try 阶段主要是对业务系统做检测及资源预留。
- Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
- Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
第三章 LCN原理
-
- 架构设计
LCN事务控制原理是由事务模块TxClient下的代理连接池与TxManager的协调配合完成事务协调控制。
TxClient的代理连接池实现了javax.sql.DataSource接口,并重写了close方法,事务模块提交关闭以后,TxClient连接池执行“假关闭”操作,等待TxManager协调完成事务以后在关闭连接。
TxManager是LCN分布式事务框架的事务协调器,框架基于Netty做消息通讯,事务控制数据存储在redis中。TxManager采用高可用集群化部署方案,多个TM通过eureka集群完成服务注册,各个TxClient通过服务名称向TxManager发送事务信息。
Redis采用集群化配置方案(也可采用哨兵模式配置方案),保证事务控制数据的一致性及准确性。
-
- LCN分布式事务框架
- 框架介绍
LCN通讯是基于TCP长连接的socket通讯,TxManager与事务控制方是基于netty框架完成的。该协议只描述参与Socket通讯的协议。LCN分布式事务框架其本身并不创建事务,而是基于对本地事务的协调从而达到事务一致性的效果。
LCN 核心采用3PC+TCC补偿机制,采用强一致性方案,保证了事务的一致性。
- 框架特点
- 兼容SpringCloud、Dubbo;
- 使用简单,低依赖,代码完全开源;
- 基于切面的强一致性事务框架;
- 高可用,模块可以依赖Dubbo或SpringCloud的集群方式做集群化,TxManager也可以做集群化;
- 支持本地事务和分布式事务共存;
- 事务补偿机制,服务故障或挂机再启动时可恢复事务;
- 核心步骤
- 创建事务组
是指在事务发起方开始执行业务代码之前先调用TxManager创建事务组对象,然后拿到事务标示GroupId的过程。 - 添加事务组
添加事务组是指参与方在执行完业务方法以后,将该模块的事务信息添加通知给TxManager的操作。 - 关闭事务组
是指在发起方执行完业务代码以后,将发起方执行结果状态通知给TxManager的动作。当执行完关闭事务组的方法以后,TxManager将根据事务组信息来通知相应的参与模块提交或回滚事务。
- 事务控制原理
LCN事务控制原理是由事务模块TxClient下的代理连接池与TxManager的协调配合完成的事务协调控制。
TxClient的代理连接池实现了javax.sql.DataSource接口,并重写了close方法,事务模块在提交关闭以后TxClient连接池将执行"假关闭"操作,等待TxManager协调完成事务以后在关闭连接。
- 对于代理连接池的优化
- 自动超时机制
任何通讯都有最大超时限制,参与模块在等待通知的状态下也有最大超时限制,当超过时间限制以后事务模块将先确认事务状态,然后再决定执行提交或者回滚操作,主要为了给最大资源占用时间加上限制。
- 智能识别创建不同的连接
对于只读操作、非事务操作LCN将不开启代理功能,返回本地连接对象,对于补偿事务的启动方将开启回滚连接对象,执行完业务以后马上回滚事务。
- LCN连接重用机制
当模块在同一次事务下被重复执行时,连接资源会被重用,提高连接的使用率。
- 事务补偿机制
- 为什么需要事务补偿
事务补偿是指在执行某个业务方法时,本应该执行成功的操作却因为服务器挂机或者网络抖动等问题导致事务没有正常提交,此种场景就需要通过补偿来完成事务,从而达到事务的一致性。
- 补偿机制的触发条件
当执行关闭事务组步骤时,若发起方接受到失败的状态后将会把该次事务识别为待补偿事务,然后发起方将该次事务数据异步通知给TxManager。TxManager接受到补偿事务以后先通知补偿回调地址,然后再根据是否开启自动补偿事务状态来补偿或保存该次切面事务数据。
- 补偿事务机制
LCN的补偿事务原理是模拟上次失败事务的请求,然后传递给TxClient模块然后再次执行该次请求事务。
-
- TCC事务补偿机制
- 事务自动补偿
默认属性tm.compensate.auto=false,设置为true则开启事务自动补偿功能,设置为false则关闭事务补偿功能。
- 开启自动补偿以后,必须要配置 tm.compensate.notifyUrl 地址,仅当tm.compensate.notifyUrl 在请求补偿确认时返回success或者SUCCESS时,才会执行自动补偿,否则不会自动补偿。
- 关闭自动补偿,当出现数据时也需要 tm.compensate.notifyUrl 地址。
当tm.compensate.notifyUrl 无效时,不影响TxManager运行,仅会影响自动补偿。
- 事务补偿记录回调
请求补偿是在开启自动补偿时才会请求的地址,请求分为两种:1.补偿决策,2.补偿结果通知,可通过通过action参数区分compensate为补偿请求、notify为补偿通知。
- 当请求补偿决策时,需要补偿服务返回"SUCCESS"字符串以后才可以执行自动补偿。
- 请求补偿结果通知则只需要接受通知即可。
请求补偿的样例数据格式:
{"groupId":"TtQxTwJP","action":"compensate","json":"{\"address\":\"133.133.5.100:8081\",\"className\":\"com.example.demo.service.impl.DemoServiceImpl\",\"currentTime\":1511356150413,\"data\":\"C5IBLWNvbS5leGFtcGxlLmRlbW8uc2VydmljZS5pbXBsLkRlbW9TZXJ2aWNlSW1wbAwSBHNhdmUbehBqYXZhLmxhbmcuT2JqZWN0GAAQARwjeg9qYXZhLmxhbmcuQ2xhc3MYABABJCo/cHVibGljIGludCBjb20uZXhhbXBsZS5kZW1vLnNlcnZpY2UuaW1wbC5EZW1vU2VydmljZUltcGwuc2F2ZSgp\",\"groupId\":\"TtQxTwJP\",\"methodStr\":\"public int com.example.demo.service.impl.DemoServiceImpl.save()\",\"model\":\"demo1\",\"state\":0,\"time\":36,\"txGroup\":{\"groupId\":\"TtQxTwJP\",\"hasOver\":1,\"isCompensate\":0,\"list\":[{\"address\":\"133.133.5.100:8899\",\"isCompensate\":0,\"isGroup\":0,\"kid\":\"wnlEJoSl\",\"methodStr\":\"public int com.example.demo.service.impl.DemoServiceImpl.save()\",\"model\":\"demo2\",\"modelIpAddress\":\"133.133.5.100:8082\",\"channelAddress\":\"/133.133.5.100:64153\",\"notify\":1,\"uniqueKey\":\"bc13881a5d2ab2ace89ae5d34d608447\"}],\"nowTime\":0,\"startTime\":1511356150379,\"state\":1},\"uniqueKey\":\"be6eea31e382f1f0878d07cef319e4d7\"}"} |
请求补偿的返回数据样例数据格式:
SUCCESS
请求补偿结果通知的样例数据格式:
{"resState":true,"groupId":"TtQxTwJP","action":"notify"} tm.compensate.notifyUrl=http://ip:port/path |
- 补偿失败
补偿失败,再次尝试间隔(秒),最大尝试次数3次,当超过3次即为补偿失败,失败的数据依旧还会存在TxManager下。参数配置tm.compensate.tryTime=30
- 各事务模块自动补偿的时间上限(毫秒)
指的是模块执行自动超时的最大时间,该最大时间若过短会导致事务机制异常,该时间必须要超过模块之间通讯的最大超过时间。
例如,若模块A与模块B,请求超时的最大时间是5秒,则建议改时间至少大于5秒。 参数配置:tm.compensate.maxWaitTime=5000
-
- LCN注意事项
- 同一个模块单元下的事务是嵌套的。
- 不同事务模块下的事务是独立的。
框架在多个业务模块下操作更新同一个库下的同一张表下的同一条时,将会出现锁表的情况,会导致分布式事务异常,数据会丧失一致性。希望开发者在设计模块时避免出现多模块更新操作(insert update delete)同一条数据的情况。
禁止重名的bean对象。
事务的补偿机制是基于java反射的方式重新执行一次需要补偿的业务。因此执行的时候需要获取到业务的service对象,LCN是基于spring的ApplicationContent的getBean方法获取bean的对象的。因此不允许出现重名对象。
-
- 模拟场景演示
若存在事务发起方、参与方A、参与方B。那么他们正常执行业务的时序图为:
若参与方B出现异常,那么他们的业务时序图为:
若他们的调用关系如下,此时发生参与方B出现异常时他们的时序图为:
第四章 数据协议规则
-
- 概述
数据接口都分为请求与响应。数据格式为json格式的数据。
格式如下(心跳指令):
请求:{"p":"{}","a":"h","k":"h"};响应:{"d":"3","k":"h"}。
请求指令分为了三个参数:p a k,分别代指:param action key,也就是参数、指令标示、唯一标示。
名称 | 描述 |
参数 | 请求需要传递的参数 |
指令标示 | 区分不同的协议 |
唯一标示 | 指令协议的唯一性标示,返回数据时要带着这个参数 |
响应指令分为两个参数:d k,分别代指:data,key,也就是数据域,唯一标示。
名称 | 描述 |
数据域 | 响应数据 |
唯一标示 | 指令协议的唯一性标示,返回请求时传递的数值 |
指令参数及描述:
指令参数 | 指令描述 |
h | 心跳包 |
cg | 创建事务组 |
atg | 添加事务组 |
ctg | 关闭事务组 |
ckg | 检查事务组 |
t | 通知事务单元 |
c | 补偿事务 |
plb | 添加负载均衡模块 |
glb | 获取负载均衡模块 |
-
- 心跳包
{"p":"{}","a":"h","k":"h"}
{"d":"3","k":"h"}
调用关系 | 事务控制方->TxManager |
a指令标示 | h |
p请求参数 | 无“{}” |
d响应数据 | 数字"3" 响应数据数字的含义:业务模块与TxManager之间通讯的最大等待时间 |
-
- 创建事务组
{"a":"cg","k":"TOT1buMh","p":{"g":"xxxxxxxx"}}
{"d":"{"st":1504935628891,"s":0,"nt":1504935628905,"g":"Y6wP4irB","o":0}","k":"TOT1buMh"}
调用关系 | 事务控制方->TxManager |
指令标示 | cg |
请求参数 | g 事务组Id,该参数仅当补偿时才会出现 |
响应数据 | st 开始时间 s 事务状态 0 失败 1 成功 nt 当前时间 g 事务组ID o 是否结束 0 未结束 1 已结束 |
-
- 添加事务组
{"a":"atg","k":"gL6h4ihr","p":{"s":0,"t":"w8blIwLJ","ms":"public int com.example.demo.service.impl.DemoServiceImpl.save()","g":"CEgX4Wfq"}}
{"d":"{"st":1504936357828,"s":0,"nt":1504936358367,"g":"vSqCF5oq","o":0}","k":"bHcTk7bw"}
调用关系 | 事务控制方->TxManager |
指令标示 | atg |
请求参数 | s 事务是事务组 0 否 1 是 t 唤醒TaskId ms 切面方法名称 u 模块唯一标示 g 事务组Id |
响应数据 | st 开始时间 s 事务状态 0 失败 1 成功 nt 当前时间 g 事务组ID o 是否结束 0 未结束 1 已结束 |
-
- 关闭事务组
{"a":"ctg","k":"oQq276AM","p":{"s":1,"g":"vSqCF5oq"}}
{"p":{"d":"1"},"a":"t","k":"oQq276AM"}
调用关系 | 事务控制方->TxManager |
指令标示 | ctg |
请求参数 | s 事务是否正常 0 否 1 是 g 事务组Id |
响应数据 | d 是否正常通知执行 0 否 1 是 |
-
- 检查事务组
{"a":"ckg","k":"Ysf2u6I0","p":{"t":"LGk7DYvV","g":"vSqCF5oq"}}
{"p":{"d":"1"},"a":"t","k":"Ysf2u6I0"}
调用关系 | 事务控制方->TxManager |
指令标示 | ckg |
请求参数 | t 事务TaskId g 事务组Id |
响应数据 | d 事务状态 0 事务不存在 -1 事务尚未结束 1 事务尚未通知 |
-
- 通知事务单元
{"a":"t","c":1,"t":"LGk7DYvV","k":"Ysf2u6I0"}
{"p":{"d":"1"},"a":"t","k":"Ysf2u6I0"}
调用关系 | TxManager-> 事务控制方 |
指令标示 | t |
请求参数 | c 事务状态 1 提交 其他回归 t 事务TaskId |
响应数据 | d 是否结束 0 未结束 1 已结束 |
该接口与其他的接口存在差异。请求时没有p而是c/t参数。这里c/t就相当于p下的c/t参数
-
- 补偿事务
{"a":"c","d":"C5IBLWNvbS5leGFtcGxlLmRlbW8uc2VydmljZS5pbXBsLkRlbW9TZXJ2aWNlSW1wbAwSBHNhdmUbehBqYXZhLmxhbmcuT2JqZWN0GAAQARwjeg9qYXZhLmxhbmcuQ2xhc3MYABABJCo/cHVibGljIGludCBjb20uZXhhbXBsZS5kZW1vLnNlcnZpY2UuaW1wbC5EZW1vU2VydmljZUltcGwuc2F2ZSgp","g":"sBqnT4P1","k":"FEvDoYX1"}
{"p":{"d":"1"},"a":"c","k":"FEvDoYX1"}
调用关系 | TxManager-> 事务控制方 |
指令标示 | c |
请求参数 | d 补偿切面的序列化数据 g 事务组Id |
响应数据 | d 补偿成功 0 不成功 1 成功 |
该接口与其他的接口存在差异。请求时没有p而是c/t参数。这里d/g就相当于p下的d/g参数
-
- 添加负载均衡模块
{"a":"plb","d":"1234567890","g":"sBqnT4P1","k":"FEvDoYX1","d":"FEvDoYX1"}
{"p":{"d":"1"},"a":"c","k":"FEvDoYX1"}
调用关系 | 事务控制方-> TxManager |
指令标示 | plb |
请求参数 | d 负载均衡模块唯一标示值 g 事务组Id k 负载均衡模块唯一标示key |
响应数据 | d 补偿成功 0 不成功 1 成功 |
该接口与其他的接口存在差异。请求时没有p而是c/t参数。这里d/g就相当于p下的d/g参数
-
- 获取负载均衡模块
{"a":"glb","g":"sBqnT4P1","k":"FEvDoYX1","d":"FEvDoYX1"}
{"p":{"d":"1234567890"},"a":"c","k":"FEvDoYX1"}
调用关系 | 事务控制方-> TxManager |
指令标示 | glb |
请求参数 | g 事务组Id k 负载均衡模块唯一标示key |
响应数据 | d 负载均衡模块唯一标示值 |
该接口与其他的接口存在差异。请求时没有p而是c/t参数。这里d/g就相当于p下的d/g参数.
第五章TxClient使用说明
-
- 配置LCN的maven包
<dependency>
<groupId>com.codingapi</groupId>
<artifactId>tx-client</artifactId>
<version>{lcn.last.version}</version>
</dependency>
<dependency>
<groupId>com.codingapi</groupId>
<artifactId>tx-plugins-db</artifactId>
<version>{lcn.last.version}</version>
</dependency>
再根据项目的rpc方式不同选择对应springcloud/dubbo/motan的扩展支持
<dependency>
<groupId>com.codingapi</groupId>
<artifactId>transaction-springcloud</artifactId>
<version>{lcn.last.version}</version>
</dependency>
<dependency>
<groupId>com.codingapi</groupId>
<artifactId>transaction-dubbo</artifactId>
<version>{lcn.last.version}</version>
</dependency>
<dependency>
<groupId>com.codingapi</groupId>
<artifactId>transaction-motan</artifactId>
<version>{lcn.last.version}</version>
</dependency>
-
- 配置tx-manager的url地址。
有两种方式
方式一: 使用默认方式是添加tx.properties文件,内容如下
#txmanager地址
url=http://127.0.0.1:8899/tx/manager/
方式二: 自定义url的配置.
- 编写配置文件到application.properties文件下keytm.manager.url如tm.manager.url=http://127.0.0.1:8899/tx/manager/。
- 复写读取配置文件的类TxManagerTxUrlService。
import com.codingapi.tx.config.service.TxManagerTxUrlService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class TxManagerTxUrlServiceImpl implements TxManagerTxUrlService{
@Value("${tm.manager.url}")
private String url;
@Override
public String getTxUrl() {
System.out.println("load tm.manager.url ");
return url;
}
}
-
- 配置dubbo:consumer
(dubbo与motan框架下)
<dubbo:consumer filter="transactionFilter" />
-
- FQA
- springcloud下在使用feign的hystrix时需要设置策略为信号量模式.
feign.hystrix.enabled=true
# 关于springcloud-hystrix机制 http://www.jianshu.com/p/b8d21248c9b1
hystrix.command.default.execution.isolation.strategy= SEMAPHORE
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
- 如何将tx-manager的访问地址设置为服务发现的方式.
复写TxManagerHttpRequestService自定义方法方式.
import com.codingapi.tx.netty.service.TxManagerHttpRequestService;
import com.lorne.core.framework.utils.http.HttpUtils;
import org.springframework.stereotype.Service;
@Service
public class TxManagerHttpRequestServiceImpl implements TxManagerHttpRequestService{
@Override
public String httpGet(String url) {
System.out.println("httpGet-start");
String res = HttpUtils.get(url);
System.out.println("httpGet-end");
return res;
}
@Override
public String httpPost(String url, String params) {
System.out.println("httpPost-start");
String res = HttpUtils.post(url,params);
System.out.println("httpPost-end");
return res;
}
}
第六章 TxManager启动说明
-
- 使用教程
- 启动redis服务
- 启动eureka服务
- 配置bootstrap.yml文件下的eureka服务地址
eureka:
instance:
hostname: ${hostname:localhost}
preferIpAddress: true
server:
peerEurekaNodesUpdateIntervalMs: 60000
enableSelfPreservation: false
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
healthcheck:
enabled: true
eurekaServiceUrlPollIntervalSeconds: 60
endpoints:
health:
sensitive: false
- 配置application.properties文件
#######################################txmanager-start#################################################
#服务端口
server.port=8899
#tx-manager不得修改
spring.application.name=tx-manager
spring.mvc.static-path-pattern=/**
spring.resources.static-locations=classpath:/static/
#######################################txmanager-end#################################################
#zookeeper地址
#spring.cloud.zookeeper.connect-string=127.0.0.1:2181
#eureka 地址
eureka.client.service-url.defaultZone=http://127.0.0.1:8761/eureka/
#######################################redis-start#################################################
#redis 配置文件,根据情况选择集群或者单机模式
##redis 集群环境配置
##redis cluster
#spring.redis.cluster.nodes=127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003
#spring.redis.cluster.commandTimeout=5000
##redis 单点环境配置
#redis
#redis主机地址
spring.redis.host=127.0.0.1
#redis主机端口
spring.redis.port=6379
#redis链接密码
spring.redis.password=
spring.redis.pool.maxActive=10
spring.redis.pool.maxWait=-1
spring.redis.pool.maxIdle=5
spring.redis.pool.minIdle=0
spring.redis.timeout=0
#####################################redis-end###################################################
#######################################LCN-start#################################################
#业务模块与TxManager之间通讯的最大等待时间(单位:秒)
#通讯时间是指:发起方与响应方之间完成一次的通讯时间。
#该字段代表的是Tx-Client模块与TxManager模块之间的最大通讯时间,超过该时间未响应本次请求失败。
tm.transaction.netty.delaytime = 5
#业务模块与TxManager之间通讯的心跳时间(单位:秒)
tm.transaction.netty.hearttime = 15
#存储到redis下的数据最大保存时间(单位:秒)
#该字段仅代表的事务模块数据的最大保存时间,补偿数据会永久保存。
tm.redis.savemaxtime=30
#socket server Socket对外服务端口
#TxManager的LCN协议的端口
tm.socket.port=9999
#最大socket连接数
#TxManager最大允许的建立连接数量
tm.socket.maxconnection=100
#事务自动补偿 (true:开启,false:关闭)
# 说明:
# 开启自动补偿以后,必须要配置 tm.compensate.notifyUrl 地址,仅当tm.compensate.notifyUrl 在请求补偿确认时返回success或者SUCCESS时,才会执行自动补偿,否则不会自动补偿。
# 关闭自动补偿,当出现数据时也会 tm.compensate.notifyUrl 地址。
# 当tm.compensate.notifyUrl 无效时,不影响TxManager运行,仅会影响自动补偿。
tm.compensate.auto=false
#事务补偿记录回调地址(rest api 地址,post json格式)
#请求补偿是在开启自动补偿时才会请求的地址。请求分为两种:1.补偿决策,2.补偿结果通知,可通过通过action参数区分compensate为补偿请求、notify为补偿通知。
#*注意当请求补偿决策时,需要补偿服务返回"SUCCESS"字符串以后才可以执行自动补偿。
#请求补偿结果通知则只需要接受通知即可。
#请求补偿的样例数据格式:
#{"groupId":"TtQxTwJP","action":"compensate","json":"{\"address\":\"133.133.5.100:8081\",\"className\":\"com.example.demo.service.impl.DemoServiceImpl\",\"currentTime\":1511356150413,\"data\":\"C5IBLWNvbS5leGFtcGxlLmRlbW8uc2VydmljZS5pbXBsLkRlbW9TZXJ2aWNlSW1wbAwSBHNhdmUbehBqYXZhLmxhbmcuT2JqZWN0GAAQARwjeg9qYXZhLmxhbmcuQ2xhc3MYABABJCo/cHVibGljIGludCBjb20uZXhhbXBsZS5kZW1vLnNlcnZpY2UuaW1wbC5EZW1vU2VydmljZUltcGwuc2F2ZSgp\",\"groupId\":\"TtQxTwJP\",\"methodStr\":\"public int com.example.demo.service.impl.DemoServiceImpl.save()\",\"model\":\"demo1\",\"state\":0,\"time\":36,\"txGroup\":{\"groupId\":\"TtQxTwJP\",\"hasOver\":1,\"isCompensate\":0,\"list\":[{\"address\":\"133.133.5.100:8899\",\"isCompensate\":0,\"isGroup\":0,\"kid\":\"wnlEJoSl\",\"methodStr\":\"public int com.example.demo.service.impl.DemoServiceImpl.save()\",\"model\":\"demo2\",\"modelIpAddress\":\"133.133.5.100:8082\",\"channelAddress\":\"/133.133.5.100:64153\",\"notify\":1,\"uniqueKey\":\"bc13881a5d2ab2ace89ae5d34d608447\"}],\"nowTime\":0,\"startTime\":1511356150379,\"state\":1},\"uniqueKey\":\"be6eea31e382f1f0878d07cef319e4d7\"}"}
#请求补偿的返回数据样例数据格式:
#SUCCESS
#请求补偿结果通知的样例数据格式:
#{"resState":true,"groupId":"TtQxTwJP","action":"notify"}
tm.compensate.notifyUrl=http://ip:port/path
#补偿失败,再次尝试间隔(秒),最大尝试次数3次,当超过3次即为补偿失败,失败的数据依旧还会存在TxManager下。
tm.compensate.tryTime=30
#各事务模块自动补偿的时间上限(毫秒)
#指的是模块执行自动超时的最大时间,该最大时间若过段会导致事务机制异常,该时间必须要模块之间通讯的最大超过时间。
#例如,若模块A与模块B,请求超时的最大时间是5秒,则建议改时间至少大于5秒。
tm.compensate.maxWaitTime=5000
#######################################LCN-end#################################################
- 启动tx-manager-4.x.x.jar。若从源码下则需要启动运行TxManagerApplication
然后访问http://127.0.0.1:8899/index.html 正常如下:
-
- FQA 如何修改服务发现依赖
例如替换为zookeeper。
- 修改服务发现为zookeeper
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
- 修改配置文件服务发现连接地址
#zookeeper地址
spring.cloud.zookeeper.connect-string=127.0.0.1:2181
第七章TxManager高可用教程
TxManager是LCN分布式事务框架的事务协调器,框架基于Netty做消息通讯,事务控制数据存储在Redis中。
-
- 高可用配置
可通过两种方式:
方式一:用nginx做负载均衡
原理图:
- 配置Redis集群
- 配置TM服务 部署多分tm,然后修改各个配置文件的eureka.client.serviceUrl.defaultZone,指向各服务的tm地址中间用","分割。
- 配置nginx负载均衡
负载均衡TM服务。
- 修改各事务模块的tx.properties配置文件url地址参数为nginx负载均衡的地址。
方式二:通过服务发现的方式去实现
通过修改tx-manager的url地址改为服务名称,然后在复写TxManagerHttpRequestService的方式实现。
第八章LCN分布式事务示例demo
-
- 项目介绍
demo下分为jdbc/jpa/mybatis 三种版本的demo,可根据自己的项目需求来对应查看相关的demo
其中jdbc版本的demo涉及到了5个业务模块,他们的调用关系图如下:
jpa版本的demo中有是三个模块,其中demo3是没有数据库事务的中间模块:
mybatis版本的demo中使用了hytrix,有两个模块,并且demo1,自定义了TxManager的通讯方式和tx.properties地址的获取方式。调用关系图如下:
-
- 依赖的服务
springcloud-lcn-demo 需要依赖的服务有:
- TxManager
- Mysql
- Redis
- Eureka
TxManager的启动与配置见
- TxManager启动说明中包含Eureka的配置环境
- 数据库配置与设置
demo项目的数据库配置在application.properties配置文件下
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url= jdbc:mysql://localhost:3306/test
spring.datasource.username= root
spring.datasource.password=root
spring.datasource.initialize = true
spring.application.name = demo2
server.port = 8082
#${random.int[9000,9999]}
eureka.client.service-url.defaultZone=http://127.0.0.1:8761/eureka/
数据库为test,账号密码为root/root,使用者可根据自己的环境调整。
数据库的初始化脚本
USE test;
DROP TABLE IF EXISTS `t_test`;
CREATE TABLE `t_test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
关于ribbon.MaxAutoRetriesNextServer=0,由于springcloud默认是开启的重试机制,开启次机制以后会导致当springcloud请求超时时会重复调用业务模块,从而会引发数据混乱,因此建议将其禁用。对于网络模块超时等故障问题建议使用hytrix方式。
-
- springcloud-lcn 配置教程
- 添加maven依赖。请及时关注maven中心库的最新版本,尽量使用最新版本。
<dependency>
<groupId>com.codingapi</groupId>
<artifactId>transaction-springcloud</artifactId>
<version>${lcn.last.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.codingapi</groupId>
<artifactId>tx-plugins-db</artifactId>
<version>${lcn.last.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
- 添加tx.properties配置
#txmanager地址
url=http://127.0.0.1:8899/tx/manager/
-
- 启动说明
所有的demo启动以后的访问地址都是
http://127.0.0.1:8081/demo/save
请求以后会出现 / by zero异常。这是由于在demo1的最后一句代码上写有int v = 100/0;
-
- LCN事务补偿控制流程