什么是事务?
ACID 特性
事务就是通过它的ACID 特性去保证一系列的操作在任何情况下都可以执行;要么同时成功,要么同时失败。
Java 中用的最多的就是在service 层的方法上添加@Transaction 注解,让spring 去帮我们管理事务。底层会给service 组件生成一个对应的proxy 动态代理,当proxy 在调用对应业务的时候,proxy 就会基于AOP 的思想去调用真正的业务方法前调用setAutoCommit 打开事务。在业务方法执行完后提交事务,如果业务方法执行的过程中发生异常就会回滚事务。
分布式事务:
分布式事务就是要在分布式系统中实现事务,它其实是由多个本地事务组成的。对于分布式系统而言几乎是满足不了ACID 的。单块系统是运行在同一个JVM 进程中的,但是分布式系统中的各个系统是运行在各自的JVM 进程中的,对于这种跨多个JVM 的进程普通发放是无法实事务的。
分布式事务主要只有五种方案:
可靠消息最终一致方案
最大努力通知方案
本地消息表
XA 方案
TCC 方案
分布式事务的实现思路:
可靠消息最终一致方案:
方案的大概流程:
- A 系统发送一个消息到mq,如果这个消息发送失败就直接取消操作。
- 消息发送成功,执行A 系统本地事务,执行失败就通知mq 回滚。
- A 系统本地事务执行成功,通知mq 发送确认消息。
- mq 会自动定期轮询所有消息,然后调用A 系统提供的接口,通过这个接口反查A 系统的上次事务是否执行成功,成功就发送消息给mq,失败则通知mq 回滚。
- A 系统成功后B 系统就会接受到消息,然后执行本地事务,本地事务执行成功则完成事务。
- 如果B 本地事务执行失败了就会基于mq 重试,mq 会自动不断重试直至成功,实在不行可以发送报警由人工来手工回滚和补偿。
这中方案的要点就是可以基于mq 来不断重试。目前国内互联网都是基于这种思路去实现分布式事务的。
最大努力通知方案:
大概流程:
- A 系统本地事务执行完成后发送消息给mq。
- 会有一个专门消费MQ 的最大努力通知服务,这个服务会消费MQ 然后写入数据库,记录下来,或者放入某个内存队列,接着调用系统B 的接口。
- B 成功就完成事务,B 失败时最大不理通知服务就会定时尝试重新调用系统B,反复调用N 次后不行则放弃,进行回滚。
可靠消息最终一致方案可以保证只要系统A 的事务完成了,通过不断重试来保证B 系统一定会完成。但是最大努力方案的B 系统如果执行失败了,在执行N 次后就不再重试,系统B 的事务会不会完成。
这个方案一般用在不太重要的业务操作中。
本地消息表:
本地消息表会有一张存放在本地消息的表,一般都是放在数据库中,然后在执行业务的时候将业务执行的操作和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中的业务肯定是执行成功的。大概思想就是:
- 系统在自己本地一个事务操作的同时,插入一条数据到消息表。
- 接着这个系统把消息发送给MQ。
- B 系统接收到消息之后在一个事务里往自己本地消息表里插入一条数据,同时执行其他业务;如果消息被处理过就会回滚,保证不会重复处理消息。
- B 系统执行成功后更新本地消息表的状态和A 系统的消息表章状态。
- 如果B 系统执行失败了就不会更新消息表状态,此时A 系统会定时扫描自己的消息表,如果有未处理的消息就再次发送给MQ,再次处理。
这个方案保证了最终一致性,即使B 系统执行失败,A 会不断发送消息直到B 执行成功。但是这个方案严重依赖于数据库的消息表来管理事务,对高并发和宽展不友好,所以一般很少用。
两段提交方案/XA 方案:
两段提交有一个事务管理的概念。负责协调多个数据库的事务,事务管理器要在所有数据库都准备好了的情况下才会正式提交事务,在各个数据库上执行操作;如果其中任何一个数据库回复没有准备好,就回滚事务。
这种分布式事务方案比较适合单块应用里、跨多个库的分布式事务,而且因为它严重依赖数据库层面来操作复杂的事务,效率很低,绝对不适合高并发场景。
一般来说,规定和规范是要求每个服务只能操作自己对应的一个数据库,需要操作别的服务对应的库,不允许直接连接的。这样的话太乱没有办法管理,可能出现别人改错数据库等情况。所以这种方案的某个系统内部出现跨多个库的操作是不符合规范的。如果要操作被人的库,必须是通过调用别的服务器的接口来实现的。
TCC 强一致性方案:
TCC:
Try(尝试)
Confirm (确认/提交)
Cancel (回滚)
这个其实是用到了补偿的概念,分三个阶段:
Try 阶段:对各个服务的资源做检测以及对资源进行锁定或者预留。
Confirm 阶段:在各个服务中执行实际操作。
Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是要执行已经执行成功的业务逻辑的回滚。
因为这个事务回滚实际上是严重依赖于自己写代码来回滚和补偿,补偿代码巨大,后期的业务代码很难进行维护。所以这种方案很少用。除非对一致性的要求太高,是系统的核心的场景。最好是各个业务执行的时间都比较短。
消息事务:
有一些第三方的MQ 是支持事务消息的,以RocketMQ 为例,实现事务大概思想就是:
- 先给Broker 发送事务消息,即半消息,半消息对消费者来说不可见;发送成功后发送方在执行本地事务
- 在根据本地事务的结果向Broker 发送提交或者回滚命令。
- RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内没有收到任何操作请求,Broker 就会通过反查接口得知发送事务是否执行成功,然后判断执行提交或回滚操作。
- 如果是提交,那么订阅方就能收到这条消息,然后执行操作,完成后在消费这条消息就可以了。
- 如果是回滚,那么订阅方就不会收到消息,就是没有执行过这个事务。