简介
随着业务的快速发展、业务复杂度越来越高,传统单体应用逐渐暴露出了一些问题,例如开发效率低、可维护性差、架构扩展性差、部署不灵活、健壮性差等等。而微服务架构是将单个服务拆分成一系列小服务,且这些小服务都拥有独立的进程,彼此独立,很好地解决了传统单体应用的上述问题,但是在微服务架构下如何保证事务的一致性呢?本文首先从事务的概念出来,带大家先回顾一下ACID、事务隔离级别、CAP、BASE、2PC、3PC等基本理论,然后再详细讲解分布式事务的解决方案:XA、AT、TCC、Saga、本地消息表、消息事务、最大努力通知等。
事务提供一种机制,可以将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。简单地说,事务提供一种“要么什么都不做,要么做全套(All or Nothing)”机制。
事务最经典也经常被拿出来说例子就是转账了。假如A要给B转账1000元,这个转账会涉及到两个关键操作就是:将A的余额减少1000元,将B的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致A余额减少而B的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务,即使不能都很好的满足,也要考虑支持到什么程度。
ACID
ACID 理论是对事务特性的抽象和总结,方便我们实现事务。你可以理解成:如果实现了操作的 ACID 特性,那么就实现了事务。ACID的具体含义详述如下。
原子性(Atomicity):原子性是指单个事务本身涉及到的数据库操作,要么全部成功,要么全部失败,不存在完成事务中一部分操作的可能。以上文说的转账为例,就是要么操作全部成功,A的钱少了,B的钱多了;要么就是全部失败,AB保持和原来一直的数目。
一致性(Consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的。还是以转账为例,原来AB账户的钱加一起是1000,相互转账完成时候彼此还是1000,所以一致性理解起来就是事务执行前后的数据状态是稳定的,对于转账就是金额稳定不变,对于其他的事务操作就是事务执行完成之后,数据库的状态是正确的,没有脏数据。
隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性侧重于多个事务之间的特性,也就是说多个事务之间是没有相互影响的比如A给B转账和B给C转账这两个事务是没有影响的(这里B给C转账如果和A给B转账的事务同时进行的时候,B的金额正确性问题保证就要看隔离级别了)。
持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
事务的隔离级别
在多个事务并发操作数据库(多线程、网络并发等)的时候,如果没有有效的避免机制,就会出现脏读、不可重复读和幻读这3种问题。
脏读(Dirty Read)
A事务读取B事务尚未提交的数据,此时如果B事务由于某些原因执行了回滚操作,那么A事务读取到的数据就是脏数据。
事务A读取到了事务B未提交的记录:
不可重复读(Nonrepeatable Read)
一个事务内前后多次读取,数据内容不一致。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
事务A读取到的name可能为“张三”,也可能为“李四”:
幻读(Phantom Read)
一个事务内前后多次读取,数据总量不一致。参考下图,事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。
不可重复读和幻读有些相似,两者的区别在于:不可重复读的重点在于修改,同样的条件, 你读取过的数据,再次读取出来发现值不一样了;而幻读的重点在于新增或者删除(参考MySQL官网https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html对幻读的定义,记录的减少也应该算是幻读),同样的条件, 第 1 次和第 2 次读出来的记录数不一样。
事务的隔离性是指多个并发的事务同时访问一个数据库时,一个事务不应该被另一个事务所干扰,每个并发的事务间要相互进行隔离。SQL 标准定义了以下四种隔离级别:
- 读未提交(Read Uncommitted):一个事务可以读取到另一个事务未提交的修改。这种隔离级别是最弱的,可能会产生脏读,幻读,不可重复读的问题问题。
- 读已提交(Read Committed):一个事务只能读取另一个事务已经提交的修改。其避免了脏读,仍然存在不可以重复读和幻读的问题。SQL Server和Oracle的默认隔离级别就是这个。
- 可重复读(Repeated Read):同一个事务中多次读取相同的数据返回的结果是一样的。其避免了脏读和不可重复读问题,但是幻读依然存在。MySQL中的默认隔离级别就是这个,不过MySQL通过多版本并发控制(MVCC)、Next-key Lock等技术解决了幻读问题。
- 串行化(Serializable):这是数据库最高的隔离级别,这种级别下,事务“串行化顺序执行”,也就是一个一个排队执行。在这种级别下,脏读、不可重复读、幻读都可以被避免,但是执行效率奇差,性能开销也最大。
事务的隔离级别和脏读、不可重复读、幻读的关系总结如下表所示:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 | 可能 | 可能 | 可能 |
已提交读 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
可串行化 | 不可能 | 不可能 | 不可能 |
https://honeypps.com/architect/introduction-of-distributed-transaction/