事务,通常指数据库事务,包含了一些列对数据库的读/写操作。事务提供以下保障:1)为数据库操作序列提供了一个从失败中恢复到正常状态的方法,保证数据库异常时的数据一致性。2)当多个应用程序在并发访问数据库时提供一个隔离方法,以防止彼此的操作互相干扰。
随着中间件技术和的多样化,事务的概念延伸到了更广泛的范围, 如redis缓存事务, MQ的事务消息。在微服务架构的情况下,不同服务可能使用不同的技术栈,不同的存储机制等。因此事务可以使用更宽泛的概念: 针对目标对象或者资源提供事务性,而不仅是数据库。
事务并发问题:
脏读:多事务并发时,读取到其他事务未提交的数据,这些数据并不是最终数据,可能会回滚。
不可重复度:在同一交易的不同时刻读取到的同一批数据不一致。
幻读:幻读和不可重复读都是指读取数据的不一致, 不可重复读是表示数据内容的不一致,幻读是指数据量的差异,比如第一次查询只有10条数据, 处理业务后再查询可能是少于或多于10条数据。
隔离级别:
Read Uncommited(读未提交):事务可以看到其他事务未提交的结果。
Read Committed(读取提交内容):事务只能看到已提交的事务的数据。
Repeatable Read(可重复读): 同一事务内对同一数据看到的内容是一致的。
Serializable(串行化): 强制事务串行。
隔离级别 |
脏读 |
不可重复读 |
幻读 |
Read Uncommited |
√ |
√ |
√ |
Read Committed |
X |
√ |
√ |
Repeatable Read |
X |
X |
√ |
Serializable |
X |
X |
X |
MySQL的默认隔离级别是: Repeatable, Mysql通过MVCC(Multi-Version Concurrency Control,多版本并发控制)来解决幻读问题, 原理是记录事务发生时的事务版本, 对于新事务版本产生的数据不进行读取,无锁并发方案。Mysql的Read Committed是通过共享锁实现。
Spring Transaction的默认隔离级别是:Isolation.DEFAULT,代表使用数据库的默认隔离级别。
当前读和快照读
当前读: 读取记录的的最新版本,并对记录加上排他锁保证其他并发事务无法修改当前记录。比如: select lock in share mode, select for update, insert/delete/update等
快照读:隔离级别不是串行级别下的读, 不加锁的非阻塞读。快照读就是基于MVCC实现。如普通Select。
MYSQL MVCC 实现原理
MVCC是解决读-写冲突的无锁并发控制,为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。
MYSQL MVCC实现主要包括:
事务的嵌套是指事务中存在子事务嵌套的场景。嵌套事务相对于平面事务由如下优势:1) 同一事务可以并发运行,提高事务内的并发度。 2)子事务可以独立提交和放弃,而父事务可以决定是否放弃子事务。
Spring 事务传播方式(Propagation):
值 |
说明 |
REQUIRED |
如果当前存在事务,则加入该事务;否则,新建一个事务 |
SUPPORTS |
支持当前事务,如果当前没有事务,则不使用事务 |
MANDATORY |
强制使用当前事务,如果当前没有事务,则抛出异常 |
REQUIRES_NEW |
如果当前存在事务,则挂起该事务,并开启一个新的事务,直到新事务完成;然后恢复之前的事务 |
NOT_SUPPORTED |
以非事务方式运行,如果当前存在事务,则挂起该事务 |
NEVER |
以非事务方式运行,如果当前存在事务,则抛出异常 |
NESTED |
如果当前存在事务,则嵌套事务执行;否则和 REQUIRED 同样处理 |
CAP理论(CAP theorem):指出对于一个分布式计算系统来说,不可能同时满足以下三点:
根据定理,分布式系统只能满足三项中的两项而不可能满足全部三项
分布式事务是指在分布式环境下执行的事务。
分布式事务有现存的管理标准: X/Open Distributed Transaction Processing(DTP) Model( X/Open XA, https://en.wikipedia.org/wiki/X/Open_XA。 XA 使用两阶段提交协议。主要缺点是 两阶段提交协(2PC)是一个阻塞协议:其他服务器需要等待事务管理器发出关于是否提交或中止每个事务的决定。如果事务管理器在事务等待其出现异常,所有服务参与方将在持有数据库锁的状态下被堵塞,直到事务管理器再次恢复并发布命令。长时间持有锁可能会破坏使用相同数据库的其他应用程序的问题。每一个事务参与方的异常都会造成事务的整体回滚,降低了系统的可用性。
两阶段提交协议(2PC):两阶段提交由阶段1-投票阶段和阶段2-执行阶段组成;
投票阶段:协调者向所有参与者发送询问是否能够提交请求的请求。参与者根据自身业逻辑进行投票,如果投yes票,则在执行前保存所有对象,如果投No票, 参与者立即放弃事务。
执行阶段:协调者收集所有投票,如果所有投票是YES,则协调则向所有参与者发送doCommit请求,否则发送doAbort请求;参与者在收到请求后,如果是doCommit请求则提交,并发送haveCommitted消息确认。
分布式事务的协调者:执行分布式事务请求的服务器需要在协调者的协调下执行事务动作。协调者需要为每一个事务生成一个唯一标识,通过唯一标识将事务的参与者关联起来。
XA模式两阶段模式中第一阶段的事务不进行提交,持续占用数据库的直到所有事务整体提交后才释放,性能较差,除了特殊极强要求一致性的场景下适用,其他场景无法满足实际需求。
针对一阶段的事务的性能延伸除了SAGA、TCC、AT等模式。
SAGA:第一本地事务直接提交,如果成功无需任何操作, 失败则进行业务补偿,事务间没有隔离性。
TCC: 真对SAGA的隔离性问题,讲事务拆分为try/confirm/cancel 三个接口;Try负责资源检查和预留; Confirm: 业务执行和提交; Cancel: 预留资源释放,回滚接口。
图3 TCC模式流程
AT:AT是从XA模式演进而来,两者都是非业务感知的分布式事务(TCC和SAGA则需通过业务来实现分布式事务),需要数据库的支持。 MySQL、Oracle、PostgreSQL和 TiDB等数据库都支持AT模式。整体机制:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。第二阶段异步化,通过回滚日记进行补偿。
图4 AT模式流程
图5 分布式事务模式比较
根据CAP理论, 分布式系统必须再AP和CP之间做出选择,XA协议是CP选择下的事务方案, 而AP选择下则是SAGA, 最终一致性。
TC(Transaction Coordinator): 事务协调者。管理全局事务的状态,调度分支事务的提交和回滚、TC指分布式事务框架提供的独立服务。
TM (Transaction Manager):事务管理者。用于开启、提交或回滚事务。
RM(Resource Manager): 资源管理器。分支事务的资源管理, 向TC注册分支事务,上报分支事务的状态,接受TC的命令对分支事务进行提交或回滚。
TM和RM通常是应用服务、一个是分布式事务的发起者、一个是分布式事务的参与者。
Seata: 支持AT、TCC、SAGA、XA模式, 阿里开源, 支持dubbo、springcloud
ByteTCC:支持TCC模式美团开源, 支持dubbo、springcloud,
Apache/Servicecomb-pack:支持TCC、SAGA, 华为开源, 支持dubbo、springcloud
https://github.com/apache/servicecomb-pack
Hmily:支持TCC模式,支持Dubbo、Spring Cloud,个人开源
1987年普林斯顿大学的Hector Garcia-Molina和Kenneth Salem发表了一篇论文Sagas,讲述的是如何处理long lived transaction(长活事务)。
SAGA原理是将一个长事务拆解成多个可以独立运行的子是事务,每个子事务都是一个保持数据库原子事务的特性,在单个服务上进行事务操作。每个子事务包括一个事务Ti和一个补偿Ci。由的协调者编排子事务Ti和Ci的执行。
SAGA对事务的控制由协调者实现,对子事务的异常可以选择重试,或回滚之前成功事务的选择。
隔离性
SAGA子事务在不同服务内都是一个完整的事务, 因此SAGA长事务之间缺乏隔离性,不同的的SAGA长事务对数据的影响都是相互可见的。
SAGA不具备隔离性,隔离性问题的解决职责从数据库转移到了应用服务上,需要根据实际的业务场景选择实施不同的资源隔离策略。如:悲观锁和乐观锁、更新版本(将更新进行版本化、可排序更新。)、交换式更新(将更新设计为可以按任何顺序执行)等策略。
回滚
SAGA相比TCC的缺点是缺少预留动作。以发送邮件为例,在TCC模式下,先保存草稿(Try)再发送(Confirm),撤销的话直接删除草稿(Cancel)就行了。而Saga则就直接发送邮件了(Ti),如果要撤销则得再发送一份邮件说明撤销(Ci)
实施SAGA时,服务的参与方已经在服务内完成了各自的事务, 无法自动回滚,业务程序需要精心设计自己的回滚补偿逻辑。
《微服务架构设计模式》一书中对Saga实现进行了两点阐释:
使用消息中间件是一项最佳实践关键,并不是必须的,但协作者之间,业务事务之间解耦独立进行是需要保证的。ServiceComb Pack Saga使用grpc作为Saga参与者和协调者的通讯机制。
SAGA的协调模式是指如何协调事务参与者各自的工作。
SAGA协调逻辑有两种方式:
ServiceComb Pack Saga的协作方式就是采用协同方式,只是参与方和事务之间的关系统一由Alpha进行管理, 相应的回滚补偿消息的推送也由Alpha负责发送到需要进行补偿的业务参与者。
图6 SAGA通讯示例
SAGA消息的一个流程示例:
1. Order服务在接收到订单时,设置APPROVAL_PENDING状态,创建一个Order并发布OrderCreated事件。
2. Consumer服务消费OrderCreated事件,验证消费者是否可以下订单,并发布ConsumerVerified事件。
3. Kitchen服务消费OrderCreated事件,验证订单,在CREATE_PENDING状态下创建出菜单,并发布TicketCreated事件。
4. Accounting服务消费TicketCreated和ConsumerVerified事件,向消费者的信用卡收费,并发布信用卡授权失败事件。
5. Kitchen服务使用信用卡授权失败事件并将出菜单的状态更改为REJECTED。
6. 订单服务消费信用卡授权失败事件,并将订单状态更改为已拒绝。
Apache ServiceComb Pack 是一个微服务应用的数据最终一致性解决方案。ServiceComb Pack支持Saga 以及TCC两种分布式事务协调协议实现。 本文只介绍Saga实现。
ServiceComb-pack github地址:https://github.com/apache/servicecomb-pack。
图7 ServiceComb Pack架构
ServiceComb Pack Saga 主要组件包括:Omega(TM,TR)、Trasport和Alpha(TC)
Omega
Omega 是一个内嵌到服务类的Saga代理, 主要包含服务参与Saga的相关组件。
Transport
Omega与Alpha之间的通讯协议实现, 使用GRPC。
Alpha
Alpha是Saga 中的协调者角色,负责工作包括:
图8 Alpha、Omega、和Service的关系。
图9 Omega工作原理
当服务收到请求时,omega拦截请求并提取请求信息中的全局事务ID作为其自身的全局事务ID,并生成本地的事务ID,本地事务预处理阶段,Omega向Alpha发送事务开始事件,后处理阶段Omega向Alpha记录事务结束事件。 每个成功的子事务都有一对对应的开始和结束事件。
图10 ServiceComb Saga事务内服务之间通讯示例
在服务提供方,Omega会拦截请求头中事务上下文信息, 在消费方,Omega会往请求头中注入事务上下文信息。事务全局ID如同链路追踪的trace_id将事务的所有参与方串联起来,而事务本地ID将事务事件和参与者建立关系。
图11 异常补偿用例图
异常场景下omega会向alpha上报异常事件,alpha向该全局事务内其它已完成的子事务发送补偿指令。
图12 超时补偿用例图
Alpha定时扫描检测超时,中断超时的全局事务,alpha向该全局事务内其它已完成的子事务发送补偿指令。
Alpha 是一个SpringBoot应用,作为服务可以独立启动直接使用,可以无缝接入微服务服务注册中心作为一个独立服务注册供Omega客户端使用。
本节只分析Omega的实现细节。
ServiceComb Pack Saga的使用非常简单,具体示例见官方github的demo。
服务在一个方法上添加@SagaStart开启Saga事务。Omega通过AOP机制实现对@SagaStart注解方法的拦截。切面定义类SagaStartAspect处理逻辑包括: 1. 初始化事务上下文:生成全局事务ID和本地事务ID。2. 调用sagaStartAnnotationProcessor进行前置处理。3. 执行本地方法。 4. 调用sagaStartAnnotationProcessor进行后置处理。
代码片段1: sagaStartAnnotationProcessor前置和后置处理
AlphaResponse preIntercept(int timeout) { try { return this.sender.send(new SagaStartedEvent(this.omegaContext.globalTxId(), this.omegaContext.localTxId(), timeout)); } catch (OmegaException var3) { throw new TransactionalException(var3.getMessage(), var3.getCause()); } } void postIntercept(String parentTxId) { AlphaResponse response = this.sender.send(new SagaEndedEvent(this.omegaContext.globalTxId(), this.omegaContext.localTxId())); if (response.aborted()) { throw new OmegaException("transaction " + parentTxId + " is aborted"); } } |
服务方法上添加@Compensable注解的方法被调用时将加入到长事务中。
@Compensable(compensationMethod = "xxxxRollback"): 该注解可以指定重试次数、超时时间、重试间隔和补偿方法。实现方式同样是使用AOP机制, 切面类为TransactionAspect。切面主要工作内容:1. 生成本地事务ID。2. 根据Compensable配置选择不同的策略来调用业务方法。策略包括:1)DefaultRecovery:调用CompensableInterceptor在执行业务方法前进行前置和后续处理,发送TxStartedEvent、TxEndedEvent或者TxAbortedEvent事件到Alpha. 2) ForwardRecovery:在DefaultRecovery基础上, 增加重试机制。
Cmopensable和CompensationMethod方法需保持入参一直。
Omega代理和Alpha之间通过grpc接口调用, org.apache.servicecomb.pack:pack-contract-grpc 中定义了grpc接口描述和调用代理类。
Omega需要在服务间传递事务上下文:事务的Global事务ID和本地事务ID,实现方式:
如果是Feign调用,则通过FeignClientRequestInterceptor 拦截器注入。
代码所在模块:org.apache.servicecomb.pack:omega-transport-feign
代码片段2 feign请求注入Omega上下文信息:
public class FeignClientRequestInterceptor implements RequestInterceptor { …… public void apply(RequestTemplate input) { if (this.omegaContext != null && this.omegaContext.globalTxId() != null) { input.header("X-Pack-Global-Transaction-Id", new String[]{this.omegaContext.globalTxId()}); input.header("X-Pack-Local-Transaction-Id", new String[]{this.omegaContext.localTxId()}); LOG.debug("Added {} {} and {} {} to request header", new Object[]{"X-Pack-Global-Transaction-Id", this.omegaContext.globalTxId(), "X-Pack-Local-Transaction-Id", this.omegaContext.localTxId()}); } else { LOG.debug("Cannot inject transaction ID, as the OmegaContext is null or cannot get the globalTxId."); } } } |
如使用resttemplate调用 ,则通过TransactionClientHttpRequestInterceptor 继承ClientHttpRequestInterceptor 在请求中注入。
代码所在模块:org.apache.servicecomb.pack:omega-transport-resttemplate。
TransactionHandlerInterceptor 拦截器在请求处理前将请求头中的omega上下文信息加载到OmegaContext中。(通过实现org.springframework.web.servlet.HandlerInterceptor接口拦截)
每一个子事务发送事务开始事件中包含补偿方法及子事务参数信息,由Alpha负责在需要进行补偿的时候向长事务内已完成的子事务节点发送补偿事件(包括补偿方法信息和参数)。
GrpcCompensateStreamObserver实现io.grpc.stub.StreamObserver接口,负责处理Alpha发送到Omega的报文, 补偿也是SAGA唯一的报文。执行逻辑在CompensationMessageHandler 中实现:调用注册的所有@compesable注解对象执行指定的补偿方法。
补偿执行上下文:记录所有参与子事务Bean和补偿方法。登记实现类CompensableAnnotationProcessor (实现 BeanPostProcessor接口)通过反射分析所有@Compensable的方法进行登记。
代码片段3 MethodCheckingCallback中进行补偿上下文注册:
Method signature = this.bean.getClass().getDeclaredMethod(each, method.getParameterTypes()); String key = this.getTargetBean(this.bean). getClass().getDeclaredMethod(each, method.getParameterTypes()).toString(); this.callbackContext.addCallbackContext(key, signature, this.bean); |
LoadBalanceContextBuilder负责建立负载均衡连接,sagaLoadBalanceSender在负载均衡上线文中,使用FastestSender来挑选最快的连接往Alpha发送事件, 具体流程如下:
Alpha加入微服务集群后, Omega可以通服务注册中心获取所有的Alpha节点。通过引入不同模块支持不同的服务注册中心产品。包括:
omega-spring-cloud-consul-starter
omega-spring-cloud-eureka-starter
omega-spring-cloud-nacos-starter
omega-spring-cloud-zookeeper-starter
java -Dloader.path=./plugins -Dspring.datasource.url="jdbc:mysql://127.0.0.1:3306/saga?useSSL=false" -Dspring.datasource.username=root -Dspring.datasource.password=123456 -Dspring.cloud.nacos.discovery.enabled=true -Dspring.cloud.nacos.discovery.serverAddr=127.0.0.1:8848 -Dnacos.client.enabled=true -Dspring.profiles.active=mysql -jar alpha-server-0.8.0-SNAPSHOT-exec.jar |
|
alpha: cluster: register: type: nacos omega: spec: names: saga |
交易流程
示例代码截图
txevent: Saga事件记录
txtimeout:Saga事务时间记录
tcc_global_tx_event: tcc模式全局事务事件表
tcc_participate_event: tcc模式参与事务事件表
tcc_tx_event: tcc模式事件记录表
command: alpha 命令处理表, 赔偿事件调用记录
@TccStart
@Participate(confirmMethod = "confirm", cancelMethod = "cancel")
confirm和cancel方法的参数列表应该与@Participate方法一样,需是幂等。
omega:
spec:
names: tcc