从本地事务到分布式事务
本地事务
当一个事务被提交给了数据库,则数据库需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则事务中的所有操作都需要被回滚,回到事务执行前的状态(要么全执行,要么全都不执行);同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。
本地事务的ACID
- 原子性(Atomicity):同生共死(要么全部成功,要么全部失败)
- 一致性(Consistency):相互抵消(卖方+200元和买方-200元)
- 隔离性(Isolation):并行不扰(理论上,并行的两个事物,相互之间是不干扰的)
- 持久性(Durability):落子无悔(不可逆)
Spring代理本地事务
在实际开发中,本地事务往往由Spring代理,让开发更加简洁,这时本地事务体现的方式是执行一个包含数据库操作方法时,该方法中的所有数据库操作要么全部成功,要么全部失败,且两个独立线程执行该方法时互不影响,Spring中提供了@Transaction注解,包含控制事务传播属性,隔离级别和超时时间等属性的入口
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
/**
* 可选的限定描述符,指定使用的事务名称
*/
@AliasFor("transactionManager")
String value() default "";
/**
* 可选的限定描述符,指定使用的事务管理器
*/
@AliasFor("value")
String transactionManager() default "";
/**
* 可选的事务传播行为设置
*/
Propagation propagation() default Propagation.REQUIRED;
/**
* 可选的事务隔离级别设置
*/
Isolation isolation() default Isolation.DEFAULT;
/**
* 事务超时时间设置
*/
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
/**
* 读写或只读事务,默认读写
*/
boolean readOnly() default false;
/**
* 导致事务回滚的异常类数组
*/
Class<? extends Throwable>[] rollbackFor() default {};
/**
* 导致事务回滚的异常类名字数组
*/
String[] rollbackForClassName() default {};
/**
* 不会导致事务回滚的异常类数组
*/
Class<? extends Throwable>[] noRollbackFor() default {};
/**
* 不会导致事务回滚的异常类名字数组
*/
String[] noRollbackForClassName() default {};
}
Spring代理本地事务提供的属性
事务是逻辑处理原子性的保证手段,使用事务控制,可以极大的避免出现逻辑处理失败导致的脏数据等问题。事务最重要的两个特性,是事务的传播级别和数据隔离级别。传播级别定义的是事务的控制范围,事务隔离级别定义的是事务在数据库读写方面的控制范围。
- 传播属性:传播是指不同Service之间调用的传播,同一个Service中的调用,传播属性是无效的;
- 隔离级别:隔离的是当前修饰的事务(方法),数据库默认隔离级别其实是服务端针对客户端默认的一个配置,如果客户端查询时不加事务,服务端设置当前客户端的查询的隔离级别为默认的隔离级别,总之,隔离级别隔离的是当前事务(当前执行sql的行为);
- 超时时间:超时时间是指所有sql语句执行的总时间,不是方法执行时间,所以让事务粒度更小的说法是错误的。
传播属性
- PROPAGATION_REQUIRED:如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务;
- PROPAGATION_SUPPORTS:如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同;
- PROPAGATION_MANDATORY:如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常;
- PROPAGATION_REQUIRES_NEW:总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起;
- PROPAGATION_NOT_SUPPORTED:总是非事务地执行,并挂起任何存在的事务;
- PROPAGATION_NEVER:总是非事务地执行,如果存在一个活动事务,则抛出异常;
- PROPAGATION_NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按PROPAGATION_REQUIRED 属性执行。
隔离级别
- ISOLATION_DEFAULT:这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别;
- ISOLATION_READ_UNCOMMITTED:这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读;
- ISOLATION_READ_COMMITTED:保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读;
- ISOLATION_REPEATABLE_READ:这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读);
- ISOLATION_SERIALIZABLE:这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。
本地事务和Spring代理事务之间的关系
抛出以前困扰我的问题:Sping代理事务是基于数据库事务实现的,如果@Transaction设置的隔离级别比数据库本身隔离级别高,Spring事务是怎么实现隔离控制的?隔离级别以哪个为准?
学习和测试之后发现,导致这种乌龙问题的原因是理解上的误区
- 错误理解:客户端的隔离级别依赖于数据库服务端设置的隔离级别实现,服务端设置统一的隔离级别控制不同的服务端读取数据库的数据范围
- 正确理解:数据库服务器设置隔离级别是赋予客户端连接的属性,如果客户端不设置隔离级别,就用服务端默认的隔离级别,客户端可自行控制读取数据的范围,在Spring代理事务源码里面提供了设置数据库隔离级别的代码,不同客户端的隔离级别是线程私有的
总之,Spring代理事务本质上就是本地事务
Spring代理事务失效
- @Transaction修饰私有方法,私有方法是不能被AOP代理的;
- @Transactional修饰的方法调用必须来自外部,否则事务不生效,原理是Spring中事务是通过AOP代理实现的;
- @Transactional修饰的方法异常被try-catch了;
- @Transaction修饰的方法抛出了Checked Exception。
分布式事务
分布式事务理论
CAP理论
- C:一致性:节点之间数据同步必须锁定节点,待数据同步完成之后,解除锁定,锁定过程中无响应,所以一致性和可用性不相容;
- A:可用性:无论什么时间所有节点必须有响应,哪怕返回旧数据,和一致性相互矛盾;
- P:分区容错性:一个节点故障不影响其他节点使用。
AP CP之能二选一
CA不是分布式系统,传统单体应用和关系型数据库满足CA
BASE理论
- B:基本可用(Basically Available),允许响应时间的损失和功能上的损失,比如说延迟退款
- S:软状态(Soft State),允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可 用性,即允许系统在多个不同节点的数据副本存在数据延时
- E:最终一致性(Eventually Consistent),软状态有个时间期限,最终数据会达到一致性状态
刚性事务
遵循ACID原则,强一致性
2PC方案
两阶段提交协议:准备阶段(Prepare phase)、提交阶段(Commit phase)。
整个事务过程由事务管理器和参与者组成,事务管理器负责决策整个分布式事务的提交和回滚,事务参与者负责自己本地事务的提交和回滚。在计算机中部分关系数据库如 Oracle、MySQL 都支持两阶段提交协议。下面是计算机数据库进行两阶段提交的说明:
- 准备阶段:事务管理器给每个参与者 Prepare 消息,每个数据库参与者在本地执行事务,并写本地的
Undo/Redo 日志,此时事务没有提交。(Undo 日志是记录修改的数据,用于数据回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件) - 提交阶段:如果事务管理器收到了参与者的执行失败或者超时消息,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放资源。
XA实现
XA实现:基于数据库协议
XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。
XA实现分布式事务的原理如下:
总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换回导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。
Seata实现
基于阿里开源分布式框架Seata实现,代码侵入性小,不灵活
git地址:https://github.com/seata/seata
TCC方案
TCC方案是需要开发者手动实现Try、Confirm和Cancel三个方法,实现分布式事务的一致性,代码侵入性大,很灵活,需解决空回滚,幂等,事务悬挂等问题。
开源框架有很多,可自行学习
框架 | github地址 |
---|---|
tcc-transaction | https://github.com/changmingxie/tcc-transaction |
Hmily | https://github.com/yu199195/hmily |
ByteTCC | https://github.com/liuyangming/ByteTCC |
EasyTransaction | https://github.com/QNJR-GROUP/EasyTransaction |
LCN | https://github.com/1991wangliang/tx-lcn |
柔性事务
遵循BASE理论,最终一致性
可靠消息最终一致性
适合执行周期长且实时性要求不高的场景,引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦,典型的使用场景:注册送积分,登录送优惠券
本地消息表
本地事务保存消息至数据库,定时扫描消息表使用MQ同步数据,ACK保证重试至成功
示例:
- 本地事务保存需要同步的消息至数据库;
- 使用MQ同步消息至第三方服务,MQ的ACK机制保证消息的可靠性消费;
- 更新本地消息状态;
- 定时任务查询同步失败的消息,重新同步至MQ。
事务性消息
RocketMQ事务性消息实现:基于监听RocketMQ消息状态和回查本地事务状态实现
- 用户服务执行本地事务,保存用户积分记录
- 用户服务发送事务性消息到RocketMQ服务器,消息不可见
- RocketMQ监听本地事务状态,如果本地事务提交成功,消息可见
- 积分服务消费,ACK机制保证消息的可靠性消费
最大努力通知
适用于一些最终一致性时间敏感度低的业务,允许发起通知方处理业务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后续处理,发起通知方需提供查询执行情况接口,用于接收通知方校对结果,典型的使用场景:银行支付
示例:
- 第三方服务发起支付;
- 支付服务通过MQ发起异步通知;
- 通知服务监听消息同步回调第三方服务,采用通知服务的原因是支付系统MQ是内网,不可能被外部监听;
- 第三方服务更新支付状态;
- 对于异常支付状态的数据,第三方服务应该主动积极的反查数据,支付服务提供反查接口。
四种方案比较
刚性事务和柔性事务各有优劣,适用于不同的场景
2PC | TCC | 可靠消息 | 最大努力通知 | |
---|---|---|---|---|
一致性 | 强一致性 | 最终一致性 | 最终一致性 | 最终一致性 |
吞吐量 | 低 | 中 | 高 | 高 |
实现复杂度 | 易 | 难 | 中 | 易 |