事务
事物
事物必须服从ISO/IEC所制定的ACID原则。ACID是原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)的缩写:
atomicity:事物的原子性表示,事物执行过程中的任何失败都将导致事物所做的任何修改失效。
consistency:一致性表示当事物执行失败时,所有被该事物影响的数据都应该恢复到事物执行前的状态。
isolation:隔离性表示在事物的执行过程中对数据的修改,在事物提交之前对其他事物不可见。
durability:持久性表示提交的数据在事物失败时,数据的状态都应该正确。
通俗的讲,事物是一组原子操作,从数据库的角度讲,就是一组SQL指令,要么全部执行成功,如果因某一条指令执行错误,则撤销先前执行过的所有指令(即全部回顾)。
隔离级别
标准的事物隔离级别有4种,分别是读取未提交内容(Read Uncommitted)、读取提交内容(Read Committed)、可重读(Repeatable Read)、可串行化(Serializable)。
Read Uncommitted
该隔离级别表示所有事物都可以看到其他事物未提交的执行结果。由于该级别会造成脏读(Dirty Read)的情况,且性能上也不具备显著优势,所以很少用于实际应用中。
Read committed
该隔离级别表示一个事物只能看到已提交事物所做的修改,这也是大多数据库系统的默认隔离级别(但不是MySQL默认的)。
这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一个事物的其他实例在该实例处理期间可能会有新的commit,所以同一个select可能返回不同结果。
Repeatable Read
这是MySQL的默认事物隔离级别,它确保同一事物的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另外一个束手的问题:幻读(Phantom Read)。
简单的说,幻读指当用户读取某一范围的数据行时,另一个事物又在该范围内插入了新行,当用户再读取该范围的数据时,会发现新的“幻影”行。InnoDB和Falcon存储引擎是通过多版本并发控制机制解决该问题的。
Serializable
这是最高的事物级别,它通常强制事物排序,使之不可能相互冲突,从而解决幻读问题。简言之,就是该隔离级别会在每个读取的数据行上加共享锁。这个级别的性能会很差,可能导致大量的超时现象和锁竞争。
在MySQL的中,实现这四种隔离级别分别有可能产生以下问题:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
Read Uncommitted | ✔ | ✔ | ✔ |
Read committed | ✖ | ✔ | ✔ |
Repeatable Read | ✖ | ✖ | ✔ |
Serializable | ✖ | ✖ | ✖ |
脏读(Dirty Read):某个事物A已经更新一份数据,事物B在此时读取了这份数据,由于事物A执行异常,最终RollBack了所有操作,则事物B读取的数据就是错误的了,该种情况被称为脏读。
不可重复读(NonRepeatable Read):在一个事物的两次查询之中数据不一致,这可能是两次查询过程中插入了一个事物更新的原有数据。即select → update → select,两次select结果不一致的情况被称为不可重复读。
幻读(Phantom Read):在一次事物两次查询中,笔数不一致。例如一个事物A查询了几列(Row)数据,而另一个事物却在此时插入了新的几列数据,先前的事物在接下来的查询中,就有几列数据是未查询出来的。如果此时事物里有插入操作,那么和另一个事物的插入就会冲突,会报错。
分布式事物
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。例如在大型电商系统中,下单接口通常会扣减库存、减去优惠、生成订单 id, 而订单服务与库存、优惠、订单 id 都是不同的服务,下单接口的成功与否,不仅取决于本地的 db 操作,而且依赖第三方系统的结果,这时候分布式事务就保证这些操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
强一致性事务
任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。简言之,在任意时刻,所有节点中的数据是一样的。
2PC
2PC(Two-phase commit protocol),中文叫二阶段提交。 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。
第一阶段(prepare):事务管理器向所有本地资源管理器发起请求,询问是否是 ready 状态,所有参与者都将本事务能否成功的信息反馈发给协调者;
第二阶段 (commit/rollback):事务管理器根据所有本地资源管理器的反馈,通知所有本地资源管理器,步调一致地在所有分支上提交或者回滚。
成功:失败:
2PC 是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。
3PC
3PC 的出现是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态。
第一阶段:只是询问所有参与者是否可可以执行事务操作,并不在本阶段执行事务操作。
第二阶段:当协调者收到所有的参与者都返回YES时,在第二阶段才执行事务操作
第三阶段:然后在第三阶段在执行commit或者rollback。
3PC 的引入是为了解决提交阶段 2PC 协调者和某参与者都挂了之后新选举的协调者不知道当前应该提交还是回滚的问题。3PC也不是完美的,同样存在问题:
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。所以,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
最终一致性事务
不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。简单说,就是在一段时间后,节点间的数据会最终达到一致状态。
传统的分布式事务已经无法满足微服务架构下的事务管理需求。那么,既然无法满足传统的ACID事务,在微服务下的事务管理必然要遵循新的法则--BASE理论。
BASE理论由eBay的架构师Dan Pritchett提出,BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性,应用应该可以采用合适的方式达到最终一致性。BASE是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency)。
基本可用:指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
软状态:允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。
最终一致性:最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
BASE中的最终一致性是对于微服务下的事务管理的根本要求,即虽然基于微服务的事务管理无法达到强一致性,但必须保证最终一致性,这就是所说的柔性事务。实现事务最终一致性的方案主要有事件通知模式、事务补偿模式两种。
事件通知模式
本地消息表
本地消息表其实就是利用了 各系统本地的事务来实现分布式事务。本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。如果调用失败也没事,会有后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。
可靠消息MQ事务
RocketMQ 就很好的支持了消息事务,下图中的半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。
事务发起方首先发送 prepare 消息到 MQ。
在发送 prepare 消息成功后执行本地事务。
执行本地事务
根据本地事务执行结果返回 commit 或者是 rollback。
如果消息是 rollback,MQ 将删除该 prepare 消息不进行下发,如果是 commit 消息,MQ 将会把这个消息发送给 consumer 端。
如果执行本地事务过程中,执行端挂掉,或者超时,MQ 将会不停的询问其同组的其他 producer 来获取状态。
Consumer 端的消费成功机制有 MQ 保证
最大努力通知
最大努力通知是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果 不影响主动方的处理结果。这个方案的大致意思就是:
系统 A 本地事务执行完之后,发送个消息到 MQ;
这里会有个专门消费 MQ 的服务,这个服务会消费 MQ 并调用系统 B 的接口;
要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B, 反复 N 次,最后还是不行就放弃。
事件补偿模式
补偿模式比起事件通知模式最大的不同是,补偿模式的上游服务依赖于下游服务的运行结果,而事件通知模式上游服务不依赖于下游服务的运行结果。
TCC
TCC指的是Try - Confirm - Cancel
。
Try 指的是预留,即资源的预留和锁定,注意是预留。
Confirm 指的是确认操作,这一步其实就是真正的执行了。
Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。
其实从思想上看和 2PC 差不多,都是先试探性的执行,如果都可以那就真正的执行,如果不行就回滚。比如说一个事务要执行A、B、C三个操作,那么先对三个操作执行预留动作。如果都预留成功了那么就执行确认操作,如果有一个预留失败那就都执行撤销动作。我们来看下流程,TCC模型还有个事务管理者的角色,用来记录TCC全局事务状态并提交或者回滚事务。
TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。
Saga模式
Saga是一种纯业务补偿模式,其设计理念为,业务在调用的时候正常提交,当一个服务失败的时候,所有其依赖的上游服务都进行业务补偿操作。业务补偿模式要求每个服务都提供补偿接口,且这种补偿一般来说是不完全补偿,既即使进行了补偿操作,那条取消的火车票记录还是一直存在数据库中可以被追踪(一般是有相信的状态字段“已取消”作为标记),毕竟已经提交的线上数据一般是不能进行物理删除的。业务补偿模式最大的缺点是软状态的时间比较长,既数据一致性的时效性很低,多个服务常常可能处于数据不一致的情况。
总结
类型 | 模式 | 数据一致性的实时性 | 开发成本 | 上游服务是否依赖下游服务结果 | 事件消息发送链路 | 业务耦合事件发送 | 绑定MQ特性 |
事件通知 | 本地异步事件服务模式 | 高 | 高 | 不依赖 | 短 | 高 | 否 |
事件通知 | 外部异步事件服务模式 | 高 | 高 | 不依赖 | 长 | 低 | 否 |
事件通知 | MQ事务消息模式 | 高 | 低 | 不依赖 | 中 | 低 | 是 |
事件通知 | 事件最大努力通知模式 | 低 | 低 | 不依赖 | 短 | 高 | 否 |
事务补偿 | Saga纯业务补偿模式 | 低 | 低 | 依赖 | - | - | - |
事务补偿 | TCC业务补偿模式 | 高 | 高 | 依赖 | - | - | - |