分布式事务-从本地事务到分布式事务
这篇文档集中于概念的梳理,不会谈太多实现上的细节。
单机事务
ACID
- Atomicity
Atomicity requires that each transaction be “all or nothing”: if one part of the transaction fails, then the entire transaction fails, and the database state is left unchanged. An atomic system must guarantee atomicity in each and every situation, including power failures, errors and crashes. To the outside world, a committed transaction appears (by its effects on the database) to be indivisible (“atomic”), and an aborted transaction does not happen. - Consistency
The consistency property ensures that any transaction will bring the database from one valid state to another. Any data written to the database must be valid according to all defined rules, including constraints, cascades,triggers, and any combination thereof. This does not guarantee correctness of the transaction in all ways the application programmer might have wanted (that is the responsibility of application-level code), but merely that any programming errors cannot result in the violation of any def ined rules. - Isolation
The isolation property ensures that the concurrent execution of transactions results in a system state that would be obtained if transactions were executed sequentially, i.e., one after the other. Providing isolation is the main goal of concurrency control. Depending on the concurrency control method (i.e., if it uses strict - as opposed to relaxed - serializability), the effects of an incomplete transaction might not even be visible to another transaction. - Durability
The durability property ensures that once a transaction has been committed, it will remain so, even in the event of power loss, crashes, or errors. In a relational database, for instance, once a group of SQL statements execute, the results need to be stored permanently (even if the database crashes immediately thereafter). To defend against power loss, transactions (or their effects) must be recorded in a non-volatile memory.
三个问题
- 脏读
脏读是指在一个事务T1处理过程里读取了另一个 未提交 的事务T2中的数据。(注意T2是可能再次修改该条数据甚至回滚的) - 不可重复读
不可重复读是指在对于数据库中的某个数据,一个事务T1在多次查询时返回了不同的数据值,这是由于在查询间隔,被另一个事务T2修改并提交了。(注意事务T1包含多次查询操作) - 幻读
幻读是指在对数据进行 批量修改 T1时,由其他事务T2插入了一条数据,于是这条数据没有受到修改,这时T1的用户再查询数据库会发现有一条数据没有被修改,就像发生了幻觉。(注意事务T1包含批量写和读这两个过程)
事务传播级别(Spring)
传播级别定义的是事务的控制范围:
- PROPAGATION_REQUIRED ,默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。
- PROPAGATION_SUPPORTS ,从字面意思就知道,supports,支持,该传播级别的特点是,如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。
- PROPAGATION_MANDATORY , 该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。
- PROPAGATION_REQUIRES_NEW ,从字面即可知道,new即每次都要一个新事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
这是一个很有用的传播级别,举一个应用场景:现在有一个发送100个红包的操作,在发送之前,要做一些系统的初始化、验证、数据记录操作,然后发送100封红包,然后再记录发送日志,发送日志要求100%的准确,如果日志不准确,那么整个父事务逻辑需要回滚。
怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW 级别的事务传播控制就可以完成。发送红包的子事务不会直接影响到父事务的提交和回滚。 - PROPAGATION_NOT_SUPPORTED ,这个也可以从字面得知,not supported ,不支持,当前级别的特点就是上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
这个级别有什么好处?可以帮助你将事务极可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况。所以这个事务这个级别的传播级别就派上用场了。用当前级别的事务模板抱起来就可以了。 - PROPAGATION_NEVER ,该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。
- PROPAGATION_NESTED ,字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
嵌套是子事务套在父事务中执行,子事务是父事务的一部分,在进入子事务之前,父事务建立一个回滚点,叫save point,然后执行子事务,这个子事务的执行也算是父事务的一部分,然后子事务执行结束,父事务继续执行。重点就在于那个save point。看几个问题就明了了:- 如果子事务回滚,会发生什么?
父事务会回滚到进入子事务前建立的save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚。 - 如果父事务回滚,会发生什么?
父事务回滚,子事务也会跟着回滚!为什么呢,因为父事务结束之前,子事务是不会提交的,我们说子事务是父事务的一部分,正是这个道理。那么: - 事务的提交,是怎样的顺序?
是父事务先提交,然后子事务提交,还是子事务先提交,父事务再提交?答案是第二种情况,还是那句话,子事务是父事务的一部分,由父事务统一提交。
- 如果子事务回滚,会发生什么?
事务隔离级别(DB)
事务隔离级别定义的是事务在数据库读写方面的控制范围
- Serializable(对事务本身加锁):最严格的级别,事务串行执行,资源消耗最大;
- REPEATABLE READ(乐观锁?):保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。
- READ COMMITTED(互斥锁?):大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。
- Read Uncommitted :保证了读取过程中不会读取到非法数据。
事务隔离级别 | Dirty reads | non-repeatable reads | phantom reads
Serializable | 不会 | 不会 | 不会
REPEATABLE READ | 不会 | 不会 | 会
READ COMMITTED | 不会 | 会 | 会
Read Uncommitted | 会 | 会 | 会
事务常用属性(Spring - Transactional)
- readonly
设置事务为只读以提升性能 - timeout
设置事务的超时时间,一般用于防止大事务的发生(事务应该尽可能小)
实现事务隔离级别
- 数据库层
- JDBC驱动
- MyBatis框架
- Spring框架
MySQL数据库为我们提供四种隔离级别:
- Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
- (默认)Repeatable read (可重复读):可避免脏读、不可重复读的发生。
- Read committed (读已提交):可避免脏读的发生。
- Read uncommitted (读未提交):最低级别,任何情况都无法保证。
修改隔离级别
1 | mysql> select @@tx_isolation; |
JDBC驱动
JDBC的一切行为包括事务是基于一个Connection的,在JDBC中是通过Connection对象进行事务管理。在JDBC中,常用的和事务相关的方法是:setAutoCommit、commit、rollback等。
1 | conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); |
- Connection类提供几个静态成员变量可作为事务的级别。
- 设置事务必须在调用setAutoCommit(false)开启事务之前。设置为 true (默认)则sql命令的提交(commit)由驱动程序负责、数据库将会把每一次数据更新认定为一个事务并自动提交;若为false则sql命令的提交由应用程序负责,程序必须调用commit或者rollback方法
- 一个Connection对象相当于一次session,只在一次数据库访问期间有效。
Spring框架事务隔离级别
PROPAGATION_REQUIRED:
如果存在一个事务,则支持当前事务。如果没有事务则开启。
PROPAGATION_SUPPORTS:
如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。
PROPAGATION_MANDATORY:
如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
PROPAGATION_REQUIRES_NEW:
总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
PROPAGATION_NOT_SUPPORTED:
总是非事务地执行,并挂起任何存在的事务。
PROPAGATION_NEVER:
总是非事务地执行,如果存在一个活动事务,则抛出异常。
PROPAGATION_NESTED:
如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。
Spring 事务的实现原理
源码入口可以看TransactionAspectSupport#invokeWithinTransaction
1 | // 从方法的@Transactional注解中获取事务属性 |
1、createTransactionIfNecessary
准备事务状态,主要任务是开启事务,即调用JDBC的con.setAutoCommit(false)
2、proceedWithInvocation
执行目标方法
3、事务管理器
事务管理器可以提供数据源的接口,包括begin、commit、rollback等。
事务状态也由TransactionManager保存,表示事务当前执行的阶段。
4、事务的传播
Spring事务的传播级别描述的是多个使用了@Transactional注解的方法互相调用时,Spring对事务的处理。
事务传播的实现原理主要看两个角度:一个角度是刚进入事务时,针对不同的传播级别,它们的行为有什么区别,另一个角度是当事务提交或回滚时,传播级别对事务行为的影响。
也就是上边源码中的createTransactionIfNecessary
和completeTransactionAfterThrowing
这两个入口。
这里标记出了源码的入口,但是我并没有再继续深入捋事务传播机制的实现原理了,想必也不会是特别复杂的。
refer: https://blog.csdn.net/weixin_44366439/article/details/89030080
分布式事务
一致性、CAP与BASE
柔性事务和刚性事务
刚性事务是指严格遵循ACID原则的事务, 例如单机环境下的数据库事务.
柔性事务是指遵循BASE理论的事务, 通常用在分布式环境中, 常见的实现方式有: 两阶段提交(2PC), TCC补偿型提交, 基于消息的异步确保型, 最大努力通知型.
通常对本地事务采用刚性事务, 分布式事务使用柔性事务.
最佳实践
如果业务场景需要强一致性, 那么尽量避免将它们放在不同服务中, 也就是尽量使用本地事务, 避免使用强一致性的分布式事务.
如果业务场景能够接受最终一致性, 那么最好是使用基于消息的最终一致性的方案(异步确保型)来解决.
如果业务场景需要强一致性, 并且只能够进行分布式服务部署, 那么最好是使用TCC方案而不是2PC方案来解决.
幂等性
幂等性则是指业务方法调用一次与调用多次的执行返回结果是一样的。分布式事务的设计中一般会需要服务接口提供者再提供一个逆向操作接口,在正向接口失败后被调用,以实现事务补偿机制。正向接口和逆向接口都是有可能被重试调用的,因此将接口设计成幂等的是必须的。
分布式一致性
在分布式系统中,为了保证数据的高可用,通常,我们会将数据保留多个副本(replica),这些副本会放置在不同的物理的机器上。为了对用户提供正确的增\删\改\差等语义,我们需要保证这些放置在不同物理机器上的副本是一致的。
一般来说一致性是必须满足的,不然那个系统就是完全不可靠的了,我们说在某些场景下需要牺牲一致性,指的是牺牲强一致性。
数据一致性
通常情况下,我们所说的分布式一致性问题通常指的是数据一致性问题。
数据一致性其实是数据库系统中的概念。我们可以简单的把一致性理解为正确性或者完整性,那么数据一致性通常指关联数据之间的逻辑关系是否正确和完整。我们知道,在数据库系统中通常用事务(访问并可能更新数据库中各种数据项的一个程序执行单元)来保证数据的一致性和完整性。而在分布式系统中,数据一致性往往指的是由于数据的复制,不同数据节点中的数据内容是否完整并且相同。
为了提高可用性,我们往往会将数据复制到多台服务器上,以消除单点故障问题,然后使用负载均衡来提高整个系统的性能。在分布式系统引入复制机制后,不同的数据节点之间由于网络延时等原因很容易产生数据不一致的情况。
比如两个用户访问一个服务的两个实例,两个实例都知道票只有一张,所以他们都会提示购票成功,但是整个系统也只有这一张票的情况下,必须得有一个用户手上的票作废,或者说购票不成功。
一致性模型
强一致性
当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。但是这种实现对性能影响较大。
弱一致性
系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。但会尽可能保证在某个时间级别(比如秒级别)之后,可以让数据达到一致性状态。
最终一致性
弱一致性的特定形式。系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS是一个典型的最终一致性系统。
最终一致性模型的变种
- 因果一致性:如果A进程在更新之后向B进程通知更新的完成,那么B的访问操作将会返回更新的值。如果没有因果关系的C进程将会遵循最终一致性的规则。
- 读己所写一致性:因果一致性的特定形式。一个进程总可以读到自己更新的数据。
- 会话一致性:读己所写一致性的特定形式。进程在访问存储系统同一个会话内,系统保证该进程读己之所写。
- 单调读一致性:如果一个进程已经读取到一个特定值,那么该进程不会读取到该值以前的任何值。
- 单调写一致性:系统保证对同一个进程的写操作串行化。
- 上述最终一致性的不同方式可以进行组合,例如单调读一致性和读己之所写一致性就可以组合实现。并且从实践的角度来看,这两者的组合,读取自己更新的 数据,和一旦读取到最新的版本不会再读取旧版本,对于此架构上的程序开发来说,会少很多额外的烦恼。
分布式事务
分布式事务是指会涉及到操作多个数据库的事务。其实就是将对同一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性。分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做的所有动作,提交或回滚事务的决定必须产生统一的结果(全部提交或全部回滚)。
常用的几个方案:
- XA两阶段提交
锁定资源时间长,对性能影响大,不适合微服务场景。 - TCC方案
对应用侵入性强,实现难度大。 - 基于消息的最终一致性方案
侵入性强,需要对业务进行大量改造,成本较高。
分布式事务的替代方案
补偿事务
在业务端实施业务逆向操作事务。
包括正向(重试)和逆向(回滚)两个部分,
补偿事务有什么缺点?
- 不同的业务要写不同的补偿事务,不具备通用性;
- 没有考虑补偿事务的失败;
- 如果业务流程很复杂,if/else会嵌套非常多层;
应用场景
支付
最经典的场景就是支付了,一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,要么全部成功,要么全部失败。而对于买家账户属于买家中心,对应的是买家数据库,而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务。
在线下单
买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。
参考
概念理解
- ACID
- 浅谈分布式事务
- 分布式系统常见的事务处理机制 https://waylau.com/distributed-system-transaction/
- 分布式系统的事务处理 https://coolshell.cn/articles/10910.html
- 分布式系统事务一致性解决方案 http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency
- 分布式服务的事务如何处理?比如dubbo,服务与服务之间的事务怎么处理比较好,现在有没有开源的解决方案? https://www.zhihu.com/question/29483490
- 如何理解TCC分布式事务?
方案
- 理解分布式事务的两阶段提交2pc
- 如何用消息系统避免分布式事务? http://blog.jobbole.com/89140/
微服务–分布式事务的实现方法及替代方案 - 微服务架构的分布式事务解决方案
- 收发事务消息
- 支付宝运营架构中柔性事务指的是什么?
- TCC事务机制简介
- Distributed-Transaction-Notes
- 微服务架构下处理分布式事务,你必须知道的事儿
- 支付核心系统设计:Airbnb的分布式事务方案简介
实现
- TCC分布式事务实现原理
- spring-cloud-rest-tcc
- tcc-transaction
- tcc-transaction 执行流程源码分析
- 芋道源码
- TCC分布式事务框架源码解析系列(一)之项目结构
- Transaction management API for REST: TCC