文章目录
事务
什么是事务
简单来说就是完成一系列由副作用事情,要求这些事情要么都成功,要么都不成功。
以我们买键盘为例,购买一个键盘简化成三个步骤:扣款→发货→收货。这三个事情都得完成了,咱们一个购买键盘的操作才算成功;假设卖家发货失败了,事务就得回滚,淘宝就得给咱们退钱。
要求
从上面的描述来看,一个事务应该满足以下条件:
- 原子性:表示事务不可分割,要么都搞定,有一个搞不定,就得回滚。
- 一致性:状态的一致性,处理前和处理后的数据要能够对的上。不能我花了50块买了键盘,最后支付宝账上少了100块
- 持久性:事务完成之后,会对后续的动作产生持续的影响。50块会被划走,键盘也会到货。
- 隔离性:这是描述事务和事务之间的关系,指的是事务在执行的过程中虽然是并行的,但是从最后的结果来看是串行的。
- 这里隔离性又可以分为四个等级:未提交读,提交读,可重复读,序列化读。跟兴趣的可以参考:浅析数据库事务的隔离性(isolation)
上面的几个部分就是我们在数据库系统里面常说的:ACID
组成部分
- 声明事务
- 开始和结束之间的所有操作
- 结束事务
简单分类
按照事务是否在单点的服务上执行,我们可以把事务分成
- 本地事务:在单个实例上执行的事务
- 分布式事务:多个实例上执行的事务,也就是一般意义上的分布式系统。
CAP理论
- C:一致性,数据在分布式系统的多个副本中始终保持一致
- A:可用性,在合适的时间范围内,返回用户期望的结果
- P:分区容错性,在某一个分区中的数据出现了隔离的时候,能够保证对外提供一致的,可用的服务。
Eric Brewer 教授在 2012 年就曾指出 CAP 理论证明不能同时满足一致性、可用性,以及分区容错性的观点。
分布式事务的问题来源
还是以在淘宝买键盘为例
如果淘宝先不做任何特殊处理,直接库存和扣款分别执行,可能会出现,两种错误的情况:库存不够但是扣款成功了;扣款失败,但是库存操作成功了。
这里就遇到了咱们上面提到的,不满足原子事务的四个原则:
- 原子性:很明显,事务被分成了两个部分,各自成功或失败,并没有保证,一起成功或一起失败的原子特性;
- 一致性:中间状态很明显就造成了数据的不一致
为了解决上面的问题,我们需要设计一些特殊的操作流程保证数据安全。同时为了应对不同的业务需求,也有一些不同的处理思路
- 强一致性:数据更新成功之后,任意时刻所有副本的数据都是一致的,一般采用同步的方式是先,成本较高,实现复杂。
- 弱一致性:数据更新成功之后,不承诺立刻就能读到最新的值,也不承诺多久之后可以读到最新的值。
- 最终一致性:数据更新成功之后,虽然不承诺立刻就能读到最新的值,但在一定时间内会一致的。
分布式事务解决方案
两阶段提交——强一致性
事务在这个设计中被分成两个阶段:1. 准备阶段;2. 提交阶段;
准备阶段
事务管理器给所有的事务参与者发送所需要执行的内容,每个参与者有两种返回结果:成功,可以执行,并在本地执行但不提交;不成功,无法执行(例如:权限校验失败)。
如果在准备阶段,有参与者反馈执行失败或超时,则由管理者出发回滚消息,事务参与者再执行回滚本地事务操作;
在这个阶段,参与者如果反馈成功,表示它已经做好了提交所有操作的准备,只要管理器确认提交,就可以立刻执行,即使突然宕机,只要节点重新启动,收到了commit指令,必须依旧能正确提交。
在这个阶段,参与者需要做一些持久化的操作,以应对各种情况,以扣款为例:
- 冻结50块,以保证这50块不被其他扣款操作划走,导致最后提交的时候因余额不足而失败
- 保存必要的日志,确保即使在收到abort之前宕机,重启之后也能根据日志,处理abort请求
- 保存必要的日志,确保即使在收到commit之前宕机,重启之后也能根据日志,处理commit请求
- 保存必要的日志,确保即使在给管理者发送Ready之前宕机,重启之后也能根据日志,正确的发送Ready消息给管理者。
提交阶段
如果所有的事务参与者都回复了成功消息,则发送提交消息,事务参与者在本地提交操作并执行事务。
事务执行完毕之后,释放使用过程中的锁资源
总结
- 解决的问题
- 解决数据一致性问题,满足一致性原则,事务只有执行成功或失败两种可能
- 无论成功或者失败,事务都会达到一个最终一致的状态
- 缺点
- 管理者单点问题,如果管理者宕机,会严重影响事务的执行
- 两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率极其低下。
- 在提交阶段,如果因为网络问题导致部分commit消息无法发送,就会导致一部分参与者接收到了commit而一部分没有,于是就出现了数据不一致。
三阶段提交——强一致性
由于二阶段提交存在的上述缺点,所以又有人提出了二阶段提交的改良版,三阶段提交。
在这个设计中,事务被分成了三个阶段,分别是:预询盘(can_commit)、预提交(pre_commit)和以及事务提交(do_commit)。
预询盘
在这个阶段,管理者会向参与者确认是否可以执行这个事务,参与者给出一个预估的结果。
这个过程和二阶段的准备阶段比较像,主要是对资源情况进行一个确认,该失败的失败,该预留的预留。
预提交
如果有参与者反馈超时,或者反馈无法执行,则发送abort消息,如果所有参与者都反馈了可以执行,则执行一下步骤:
- 管理者给所有的参与者发送事务执行的消息
- 参与者收到执行消息之后开始执行,但不提交
- 参与者执行完毕之后将执行结果反馈给管理者
提交
如果预提交过程,所有的任务都顺利执行,那么管理者下发commit消息,让参与者提交事务。
在本阶段如果因为管理者网络问题,导致参与者迟迟不能收到 commit 或 rollback 请求,那么参与者将不会如两阶段提交中那样陷入阻塞,而是等待超时后继续 commit,相对于两阶段提交降低了同步阻塞。
总结
- 解决的问题
主要还是二阶段的阻塞问题,毕竟没收到commit或者rollback,参与者自己还是能从阻塞的状态回过神来,自己就commit完,恢复了。
事件队列方案——最终一致性方案
通过事件的方式,将事件中需要完成的操作通过消息提交到消息队列中,在消息队列里面保证消息已定会被消费者正确的消费掉,失败了就重试。
TCC补偿模式——最终一致性方案
依次的调用事务中的不同操作,某一个操作执行失败了就依次回滚之前执行成功的操作。实现的要点是
- 要记录调用链
- 每个操作都得是可以回滚的,同时要保证回滚操作是幂等的
- 必须按失败原因执行不同的回滚策略。
稍微复杂的版本是,执行之前先进行资源锁定,只有锁定成功了再真正的依次执行,否则,对锁定资源进行释放。
缓存和数据库一致
- 定时刷新缓存;或者为缓存中的数据设置过期时间;这样就可以在能够容忍的时间内达到和数据库数据一致了
- 数据更新之后删除缓存或者触发缓存更新;这样就能保证下次拿到的一定是最新的数据了
解决框架
Seate,阿里开源的一套分布式事务框架
总结
虽然两阶段/三阶段是非常规范的协议,但是在互联网中很少使用,有几个原因:
- 性能问题,阻塞性协议,高并发场景下并不适用
- mysql5.7之前存在缺陷,支持并不好
- 运维和实现比较复杂
如果我们不追求二阶段提交要求的强一致性,有一个非常朴素的想法,依次触发每一个事务,如果失败了,把之前所有的事务都回滚掉,这个就被称作Saga。但 Saga 这种方式并不能保证隔离性,于是出现了 TCC。在实际交易逻辑前先做业务检查、对涉及到的业务资源进行“预留”,或者说是一种“中间状态”,如果都预留成功则完成这些预留资源的真正业务处理,典型的如票务座位等场景。
当然还有像 Ebay 提出的基于消息表,即可靠消息最终一致模型,但本质上这也属于 Saga 模式的一种特定实现。
仔细对比这些方案与二阶段,会发现这些方案本质上都是将两阶段提交从资源层提升到了应用层。
- Saga 的核心就是补偿,一阶段就是服务的正常顺序调用(数据库事务正常提交),如果都执行成功,则第二阶段则什么都不做;但如果其中有执行发生异常,则依次调用其补偿服务(一般多逆序调用未已执行服务的反交易)来保证整个交易的一致性。应用实施成本一般。
- TCC 的特点在于业务资源检查与加锁,一阶段进行校验,资源锁定,如果第一阶段都成功,二阶段对锁定资源进行交易逻辑,否则,对锁定资源进行释放。应用实施成本较高。
- 基于可靠消息最终一致,一阶段服务正常调用,同时同事务记录消息表,二阶段则进行消息的投递,消费。应用实施成本较低。