微服务·数据一致-事务与分布式事务
概述
事务是计算机科学和数据库管理中的一个关键概念,用于确保数据的一致性和可靠想。事务管理是大多数应用程序和数据库系统中不可或缺的一部分。分布式事务扩展了事务的概念,用于多个分布式系统和服务的数据一致性管理。本调查报告将深入探讨事务和分布式事务的概念、特性、类型和应用,以及事务处理的最佳时间
事务
什么事事务
事务是一组数据库操作的逻辑单元,要么全部成功执行,要么全部失败回滚,以保证数据的一致性和完整性。事务遵循ACID属性,即原子性(Atomicity)、一致性(Consistency)隔离性(Isolation)和持久性(Durability)。
ACID属性
- 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败。如果发生故障或异常,事务应该会滚到起始状态。
- 一致性(Consistency):事务将数据从一个一致状态转移到另一个一致状态。在事务结束后,所有约束和规则都应得到满足
- 隔离性(Isolation):事务应该在隔离的环境中执行,即使多个事务并发执行,也不相互干扰。数据库管理系统提供不同级别的隔离度。
- 持久性(Durability):一旦事务提交,其结果应该用具保存在数据库中,技术发生故障也不会丢失
如何保证原子性
innodb利用undo log实现原子性。undo log名为回滚日志,是实现原子性的关键,当事务回滚是能够撤销虽有已经执行成功的sql语句,它需要记录你要回滚的相应的日志信息。比如:delete一个数据的时候,就需要记录这条数据的信息,回滚的时候insert这条旧数据。当事务执行功能失败或调用rallback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
如何保证持久性
在修改数据的时候,Mysql先把磁盘上的数据加载到内存中,在内存中对数据修改,再刷到磁盘中。如果在刷盘前宕机,内存中的数据就会丢失。
- 解决思路:事务提交前直接把数据写入磁盘中。
引入问题:性能损耗严重,只修改一个页(16k)里面的一个字节,就需要把整个页面刷入磁盘。另外一个事务的SQL可能涉及到多个数据页的修改,存在随机IO的可能,速度会更慢。 - 解决思路:使用redo log
修改数据的时候,不仅内存中操作,还会在redo log中记录这次操作,当事务提交的时候会将redo log日志进行刷盘。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。
一条数据的更新流程
事务并发带来的问题及隔离级别
当事务并发的时候会带来一下问题:
- 脏读:一个事务访问到另一个事务未提交的数据。
- 不可重复读:一个事务多次读取同一个数据的过程中,数值发生变化。
- 幻读:一个事务多次读取同一个数据的过程中, 全局数据(表的结构)发生了改变。
为了解决并发事务带来的问题,提供了事务的隔离级别:
- 读未提交:允许读取未提交的内容,这种级别下查询不会加锁,存在脏读、幻读、不可重复的问题。
- 读已提交:只允许读取已提交的内容,但是仍然会存在幻读、不可重复度的问题。
- 可重复读:用行级锁来保证一个事务在相同查询条件下两次查询结果一致 , 可以避免脏读, 不可重复读, 但无法避免幻读。(Innodb解决了幻读问题)
- 串行化:用表级锁来保证所有事务串行化, 可以防止所有的异常情况, 但牺牲了数据库的并发性。
事务隔离级别实现的原理
- 读未提交:
- 事务对当前读的数据不加锁,都是当前读。
- 事务在更新数据的瞬间,必须先对其加行级共享锁,直到事务结束才释放。
- 读已提交:
- 事务对当前读的数据不加锁,是快照读。
- 事务在更新数据的瞬间,必须先对其加行级排他锁(record)直到事务结束才释放。
- 可重复读:
- 事务对当前读的数据不加锁,且是快照读。
- 事务在更新数据的瞬间,必须对其加行级排他锁(record、gap、nest-key),直到事务结束才释放
- 串行化:
- 事务在读数据时,必须先对其加表级共享锁,直到数据结束才释放,都是当前读。
- 事务在更新数据是,必须先对其加表级排他锁,直到事务结束才释放。
Spring中的事务管理
事务的传播行为
- PROPAGATION_REQUIRED:默认的事务事务传播行为,如果存在当前事务,则加入当前事务,如果不存在,则创建一个新的。
- 如果外部方法没有开启事务,PROPAGATION_REQUIRED修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。
- 如果外部方法开启了事务,且是PROPAGATION_REQUIRED,则外部和内部方法属于一个事务,只要一个方法回滚,整个事务都需要回滚,即A(外部方法)影响B(内部方法),B影响A。
- PROPAGATION_REQUIRED_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- 不管外部方法是否开启事务,PROPAGATION_REQUIRED_NEW修饰的方法都会开启自己的事务。外部方法的事务与内部方法的事务相互独立,互不干扰。即A不影响B,B不影响A。
- PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务,否则以非事务方式运行。
- PROPAGATION_NOT_SUPPORTS:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- PROPAGATION_NESTED:如果当前存在事务,就在当前事务内执行。否则就执行与PROPAGATION_REQUIRED类似的操作
- A影响B,B不影响A。
事务失效的几种情况
- 抛出事务不支持的异常
Spring事务默认支持RuntimeException,如果抛出的异常为RuntimeException及其子类异常,事务均可生效。如果是抛出的Exception异常,则失效。解决方案:指定Spring事务异常捕获类型@Transactional(rollbackFor = Exception.class)或抛出spring事务支持的异常类型。 - 使用了try-catch
异常被try-catch捕获,导致事务失效。解决方案:在catch中抛出Spring事务支持的异常。 - 事务方法为私有方法
Spring声明式事务是基于动态代理实现的,private方法不能被代理,事务不生效。此外static方法属于类,不属于任何对象,也不会被代理,事务不生效。final方法无法被重写,也不能被代理,事务不生效。 - 未被Spring管理的类
Spring实现对象的动态代理,首先这个对象要交给Spring管理 - 一个方法调用本类的另一个方法
@Transactional基于AOP实现,而AOP又是基于动态代理实现,直接调用本类方法或使用this调用本类方法,均不是Spring的代理对象,无法实现动态代理,事务也就不会生效。 - 数据表不支持事务
- Spring事务传播级别设置为不支持事务
分布式事务
什么是分布式事务
分布式事务是在分布式系统中维护数据一致性的方式。它扩展了ACID事务的概念,以跨越多个分布式服务、数据库和资源的范围来保证数据的一致性。
分布式事务面临的挑战
- 网络延迟:跨网络执行事务可能导致较高的延迟,影响性能。
- 故障处理:分布式系统中的节点故障可能导致事务的不一致状态,需要复杂的故障处理机制。
- 并发控制:多个事务同时访问和修改共享数据需要有效的并发控制机制。
分布式事务的历史与发展
分布式事务的历史和发展经历了多个阶段,从早期的两阶段提交到最近的新兴分布式数据库和NoSQL解决发方案。
- 两阶段提交(2PC):
- 1980年代末到1990年代初,提出了两阶段提交。两阶段提交通过协调者和参与者的协作,确保在多个分布式数据库之间的事务具有原子性和一致性。
- 2PC的问题在于它可能导致性能瓶颈和阻塞,因为在第二阶段需要等待所有参与者的确认。
- 三阶段提交(3PC)
- 为了阶段2PC的一些问题,提出了三阶段提交。3PC引入了一个预提交节点,以减少阻塞问题,然而它仍然存在某些情况下无法解决的问题。
- XA事务
- XA(eXtended Architecture)是一种用于分布式事务的标准,由X/Open组织制定。XA事务使用了分布式管理器(TransactionManager)来协调多个资源管理器(Resource Manager)的事务。
- XA事务提供了分布式事务的一些标准化机制,但它在性能可伸缩性方面仍然有限制
- Sage模式
- Sage是一种分布式事务模型,将长事务拆分成一系列小事务,并使用补偿事务来处理部分失败。Sage模式在微服务架构中得到广泛应用,以提高系统的可伸缩性和可恢复性。
- NoSQL数据库
- 随着分布式计算和大数据的兴起,出现了各种新兴的NoSQL数据库,如Cassandra、MongoDB和Couchbase。这些数据库通过采用了最终一致性的模型,提高性能和可用性。
- NoSQL数据库的出现使得开发人员需要重新考虑分布式事务的模型,并引用了新的解决方案,如CRDTs(Confict-Free Replicated Data Types)。
- 新兴分布式数据库
- 近年来,出现了一些性能的分布式数据库系统,如Google的Spanner和CockroachDB,它们旨在提供全球分布式事务的支持。这些系统使用全球分布式的时间同步和意义性协议来实现强一致性的分布式事务。
- 区块链技术
- 区块链技术引入了一种分布式账本的概念,其中的交易具有强一致性和不可变性。这使得区块称为一种分布式事务的潜在选择,尤其是金融和合同领域。
分布式事务处理方法
2PC
两阶段提交是一种强一致性设计,它引入一个事务协调者的角色协调管理各个参与者的提交和回滚。
- 准备阶段(Phase1 - Prepare Phase)
- 协调者向所有参与者发送事务准备请求
- 参与者收到请求后,会执行事务的准备操作,并准备好事务的状态(提交或回滚)保存到本地
- 参与者在准备完成后向协调者发送准备完成的通知,同时将本地准备状态持久化
- 提交阶段(Phase2 - Commit Phase)
- 协调者在收到所有参与者的准备完成通知后,如果所有参与者都准备就绪,将向所有参与者发送事务提交请求。
- 参与者接受到提交请求后,会执行事务的提交操作,将事务永久性的应用到系统中并释放之前的资源。
- 参与者在完成提交后发送提交完成的通知。
- 回滚阶段(Phase2 - Rollback Phase)
- 如果在准备阶段任何参与者没有准备好,或者有参与者在提交阶段失败,协调者将会向所有参与者发送事务回滚请求。
- 参与者接受到回滚请求后,会执行事务回滚操纵,将之前的操作撤销,并释放资源。
- 参与者在完成回滚后向协调者发送回滚完成的通知。
2PC存在的问题
- 同步阻塞:所有的参数者都是事务同步阻塞型的,当参与者占有公共资源时,其他三方访问公共资源不得不处于阻塞状态。
- 单点故障:一旦协调者发生故障,系统不可用。
- 数据不一致:当协调者发送commit之后,有的参与者收到commit消息,事务执行成功,有的没有收到,处于阻塞状态,这段时间会产生数据不一致情况。
- 不确定性:当协调者发送commit后,并且此时只有一个参与者收到commit,那么当该参与者与协调器同时宕机后,重新选举的协调者无法确定该消失是否提交成功。
2PC的优势在于对业务没有侵入,可以利用数据库自身机制急性事务的提交和回滚。常见的机遇2PC的具体落地方案有JTA(XA规范)和Seata(AT模式)
3PC
三阶段提交是二阶段提交的改进版本,在协调者和参与者都引入了超时机制,在2PC中的准备阶段和提交阶段增加了一个预提交阶段。
- 准备阶段(CanCommit):协调者向各个参与者发送请求,询问是否可以执行事务,但并不是执行事务。
- 预提交阶段(PreCommit):如果从协调者得到反馈是满足执行条件,那么就发送预提交请求,并开始执行事务;如果从协调者得到返回时不满足执行条件或者超时,则发送事务中断请求。
- 提交阶段(DoCommit):如果预提交阶段发送的是预提交请求,那么正常提交事务;如果预提交阶段发送的是事务中断请求,那么直接中断事务。
相对于 2PC,3PC 主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行 commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的中断响应没有及时被参与者接收到,那么参与者在等待超时之后执行了 commit 操作。这样就和其他接到中断命令并执行回滚的参与者之间存在数据不一致的情况。而且 3PC 整体的交互过程更长,性能也会有所下降。
3PC 目前似乎只存在于理论,还没有具体落地方案。
TCC
2PC和3PC都是依赖于数据库的事务提交和回滚,但是有时候很多业务并不知涉及到数据库,可能会发送短信、消息等,而TCC就是属于业务层面的分布式应用。
TCC方案分为Try-Confirm-Cancel三个阶段,属于补偿性分布式事务。
- Try阶段:完成所有业务检查(一致性),预留业务资源(隔离性)
- Confirm阶段:确认执行业务操作,不再做任何业务检查,只使用Try阶段预留的业务资源。
- Cancel阶段:取消Try阶段预留的业务资源。
Try阶段失败了会执行Cancel,Confirm阶段失败会不断进行重试,或者进行人工干预。
TCC需要根据每个场景和业务逻辑来设计相应的操作,所以很大程度增加了业务代码的复杂度,对业务有很大的侵入。但是没有资源阻塞,每一个方法都是直接提交事务的,如果出错是通过业务层面的Cancel来进行补偿,所以也称补偿事务方法。
TCC要注意的几个问题
- 幂等问题:因为网络调用无法保证请求一定能到达,所以都会有重试机制,因此对于Try、Confirm、Cancel三个方法都需要幂等实现,避免重复执行产生错误。
- 空回滚问题:指的是Try方法由于网络问题没有收到超时了,此时事务管理器就会发出Cancel命令,那么需要支持Cancel在没有执行Try的情况下能正常Cancel。
- 悬挂问题:这个问题也是指Try方法由于网络阻塞超时出发了事务管理器发出了Cannel命令,但是执行了Cancel命令之后,Try请求到了。所以空回滚之后还得记录一下,防止Try的再调用。
可靠消息最终一致性方案
RocketMQ4.3之后的版本正式支持事务消息。
- 服务A(生产者)向Broker(消息中间件)发送一个HalfMessage(半消息:消息发送到Broker端,但是消息的状态被标记为"不能投递",即消费者看不到)
- 半消息发送成功后,服务A执行本地事务。
- 事务执行成功,向Broker发送Commit命令,此时半消息就变成了可以被消费的消息;如果失败,则发送一个RollBack命令,该消息会被删除。
- 服务B(消费者)收到消息后消费该消息即可。
- 在RocketMQ没有收到服务A确认状态的消息,那么半消息会自宋定时轮询毁掉接口,询问这个处理的处理情况。
- 服务B在执行的过程中也可能会失败,这时需要重试,一直执行不成功也需要人工介入,同时也需要保证服务B方法的幂等性。
应用场景
分布式事务适用于需要数据一致性的多个应用场景,包括:
- 电子商务:确保订单处理、支付和库存管理等操作的一致性。
- 金融服务:跨银行交易、交易清算和资金阶段的一致性管理。
- 物流和供应链:跨多个仓库、配送中心和供应商的库存和订单管理。
- 在线游戏:多玩家游戏中的虚拟经济和资源管理。
结论
事务和分布式事务是分布式系统和数据库管理中的关键概念,了解了它们的原则、属性和实现方式对于构建高可用、高性能和数据一致性的应用程序至关重要。在选择分布式事务方案是,需要根据应用的的要求权衡性能、复杂度和一致性。