关系型数据库事务
大多数人可能和我一样第一次听说事务是在学习关系型数据库mysql、sql server、Oracle的时候在关系型数据库中如果一组操作满足ACID特性那么称之为一个事务。关于关系型数据库的ACID特性不管是教材还是网络上都有大量的资料这里只简单介绍。AAtomic原子性构成事务的所有操作要么都执行完成要么全部不执行不可能出现部分成功部分失败的情况CConsistency一致性在事务执行前后数据库的一致性约束没有被破坏。这里的一致性含义后面会详细解释IIsolation隔离性数据库中的事务一般都是并发的隔离性是指并发的两个事务的执行互不干扰一个事务不能看到其他事务运行过程的中间状态DDurability持久性事务完成之后该事务对数据的更改会被持久化到数据库且不会被回滚。我们举一个简单的转账的例子用户A给玩家B转100块钱那么涉及到两个操作玩家A的账户扣100元玩家B的账户加100元。即UserA.account - 100UserB.account 100原子性很好理解这两个操作要么都成功要么都不执行更准确的是从效果上来看等价于都没有执行。不可能出现用户A的钱减少了而用户B的钱没增加的情况用户是不允许的更不可能出现用户B的钱增加 而 用户A的钱没有减少的情况银行是绝对不干的。一致性说一起来大家都懂但是深究起来也是似懂非懂。ACID中的一致性网络上的介绍都很模糊都是说要处于一致的状态那什么是一致的状态呢比如转账操作中A扣钱B加钱AB的钱的综合是一定的这个是否属于ACID中的Consistency呢我觉得不是的Wiki Transaction_processing和Wiki ACID分别是这么描述的Consistency: A transaction is a correct transformation of the state. The actions taken as a groupdo not violate any of the integrity constraints associatedwith the state.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 defined rules.上面黑色加粗的部分指出ACID中的一致性是指完整性约束不被破坏完整性包含实体完整性主属性不为空、参照完整性外键必须存在原表中、用户自定义的完整性。用户自定义的完整性比如列值非空not null、列值唯一unique、列值是否满足一个bool表达式check语句如性别只能有两个值、岁数是一定范围内的整数等例如age smallint CHECK (age 0 AND age 120).数据库保证age的值在[0, 120]的范围如果不在这个范文那么更新操作失败事务也会失败。另外向mysql中的cascade以及触发器trigger都属于用户自定义的完整性约束。在MongoDB3.2中document validation就是用户自定义的完整性约束在插入或者更新docuemnt的时候检查不过用户可以自行设定validationAction确定当数据不符合约束时的表现默认为error即拒绝数据写操作。因此用户AB在这次事务操作前后账户的总和一定是应用层面的一致性而不是数据库保证的一致性应用层面的一致性事实上是由原子性来保证的。隔离性说起来简单但事实上背后的事情很复杂数据库的隔离性依赖于加锁或者多版本控制。简单来说如果UserA.account初始值为500执行完第一条指令即减去100但事务还没有提交其他的事务是不能读到这个中间结果UserA.account的值为400的。这就是避免了脏读Drity Read对应的隔离级别就是READ_COMMITTED。在SQL标准中定义了四个隔离级别READ_UNCOMMITTEDREAD_COMMITTEDREPEATABLE_READSERIALIZABLE来解决事务并发中带来的一下几个问题脏读Dirty Read)、不可重复读Non-repeatable Read、幻读Phantom Read不同的数据库或者说存储引擎默认支持不同的隔离级别比如InnoDB存储引擎默认支持REPEATABLE_READ而Mongodb只支持READ_UNCOMMITTED持久性需要考虑到一个事务在执行过程中的各种情况的异常。一个事务的流程是这样的开启一个事务执行一组操作如果都执行成功那么提交并结束事务如果任何操作失败那么回滚已经执行的操作结束事务在事务执行过程中如果出现故障比如断电、宕机这个时候就要利用日志redo log或者undo log 加上 checkpoint来保证事务的完整结束。分布式事务回到顶部当数据的规模越来越大超出了单个关系型数据库的处理能力这个时候就出现了关系型数据的垂直分表或者水平分表也出现了天然支持水平扩展sharding的NoSql。另外大型网站的服务化SOA以及这两年非常火的微服务往往将服务进行拆分单独部署自然也使用独立的数据库甚至是异构的数据库。这个时候关系型数据库保证事务的手段比如加锁、日志就行不通了。当然本文讨论的不仅仅是数据库也包含分布式存储、消息队列以及任何要保证原子性、持久性的逻辑。分布式事务的最大挑战在于CAP在《CAP理论与MongoDB一致性、可用性的一些思考》一文中有详细介绍。简而言之由于网络分割P Network Partition的存在用户不得不在一致性C Consistency与可用性A Avaliable之前做权衡。如果要保证强一致性主要是应用层面的强一致性那么在网络分割的时候系统就不可用如果要保证高可用性那么就只能提供弱一致性保证最终一致。下面提到的各种实现分布式事务的方法、协议都需要在一致性与可用性之间权衡。2PC提到分布式事务首先想到的肯定是两阶段提交2pc two-phase commit protocol2pc是非常经典的强一致性、中心化的原子提交协议。中心化是指协议中有两类节点一个中心化协调者节点coordinator和N个参与者节点participant、cohort。顾名思义两阶段提交协议的每一次事务提交分为两个阶段在第一阶段协调者询问所有的参与者是否可以提交事务请参与者投票所有参与者向协调者投票。在第二阶段协调者根据所有参与者的投票结果做出是否事务可以全局提交的决定并通知所有的参与者执行该决定。在一个两阶段提交流程中参与者不能改变自己的投票结果。两阶段提交协议的可以全局提交的前提是所有的参与者都同意提交事务只要有一个参与者投票选择放弃(abort)事务则事务必须被放弃。wiki上给出了简要流程注意上图中最下面一行也表明两阶段提交协议也依赖与日志只要存储介质不出问题两阶段协议就能最终达到一致的状态成功或者回滚而下图来自slideshare详细描述了整个流程在刘杰的《分布式原理介绍中》有非常详细的流程介绍可以配合上图一起看另外还介绍了在各种异常情况下比如Coordinator、Participant宕机网络分割导致的超时两阶段协议的工作情况、工作效率。另外在这篇文章中也有比较清晰的流程介绍。在这里只讨论2PC的优缺点优点强一致性只要节点或者网络最终恢复正常协议就能保证顺利结束部分关系型数据库Oracle、框架直接支持缺点两阶段提交协议的容错能力较差比如在节点宕机或者超时的情况下无法确定流程的状态只能不断重试两阶段提交协议的性能较差 消息交互多且受最慢节点影响这篇文章描述了为什么两阶段提交协议在分布式系统中不适用系统“水平”伸缩的死敌。基于两阶段提交的分布式事务在提交事务时需要在多个节点之间进行协调,最大限度地推后了提交事务的时间点客观上延长了事务的执行时间这会导致事务在访问共享资源时发生冲突和死锁的概率增高随着数据库节点的增多这种趋势会越来越严重从而成为系统在数据库层面上水平伸缩的枷锁 这是很多Sharding系统不采用分布式事务的主要原因。所言甚是3PC三阶段提交协议3pc Three-phase_commit_protocol主要是为了解决两阶段提交协议的阻塞问题从原来的两个阶段扩展为三个阶段并且增加了超时机制。3PC只是解决了在异常情况下2PC的阻塞问题但导致一次提交要传递6条消息延时很大。具体流程描述可参见《关于分布式事务、两阶段提交协议、三阶提交协议 》一文。TCCTCC是Try、Commit、Cancel的缩写在国内由于支付宝的布道而广为人知TCC在保证强一致性的同时最大限度提高系统的可伸缩性与可用性。我们假设一个完整的业务包含一组子业务Try操作完成所有的子业务检查预留必要的业务资源实现与其他事务的隔离Confirm使用Try阶段预留的业务资源真正执行业务而且Confirm操作满足幂等性以遍支持重试Cancel操作释放Try阶段预留的业务资源同样也满足幂等性。“一次完整的交易由一系列微交易的Try 操作组成如果所有的Try 操作都成功最终由微交易框架来统一Confirm否则统一Cancel从而实现了类似经典两阶段提交协议2PC的强一致性。”与2PC协议比较 TCC拥有以下特点位于业务服务层而非资源层 由业务层保证原子性没有单独的准备(Prepare)阶段降低了提交协议的成本Try操作 兼备资源操作与准备能力Try操作可以灵活选择业务资源的锁定粒度而不是锁住整个资源提高了并发度当然TCC需要较高的开发成本每个子业务都需要有响应的comfirm、Cancel操作即实现相应的补偿逻辑。基于消息的分布式事务这类事务机制将分布式事务分成多个本地事务这里称之为主事务与从事务。首先主事务本地先行提交然后通过消息通知从事务从事务从消息中获取信息进行本地提交。可以看出这是一种异步事务机制、只能保证最终一致性但可用性非常高不会因为故障而发生阻塞。另外主事务已经先行提交如果因为从事务无法提交要回滚主事务还是比较麻烦所以这种模式只适用于理论上大概率等成功的业务情况即从事务的提交失败可能是由于故障而不大可能是逻辑错误。基于异步消息的事务机制主要有两种方式本地消息表与事务消息。二者的区别在于怎么保证主事务的提交与消息发送这两个操作的原子性。如果用异步消息实现转账的例子那么操作分为四部用户A扣钱发消息用户B收消息用户B收钱。前两步必须保证原子性如果A扣钱成功但是没有发出消息那么用户A损失了如果发消息成功但是没有扣钱那么用户B就多得了一笔钱银行肯定不干。本地消息表基于本地消息表的方案是指将消息写入本地数据库通过本地事务保证主事务与消息写入的原子性。例如银行转账的例子伪码如下begin transaction:update User set account account - 100 where userId Ainsert into message(userId, amount, status) values(A, 100, 1)commit transaction然后通过pull或者push模式从业务获取消息并执行。如果是push模式那么一般使用具有持久化功能的消息队列从事务务订阅消息。如果是pull模式那么从事务定时去拉取消息然后执行。mongodb的写入就很像本地消息表在WriteConcern为w:1的情况下更新操作只要写到oplog以及primary就可以向客户端返回。secondary异步拉取oplog并本地记录执行。事务消息事务消息依赖于支持“事务消息”的消息队列其基本思想是 利用消息中间间实施两阶段提交将本地事务和发消息放在了一个分布式事务里保证要么本地操作成功成功并且对外发消息成功要么两者都失败。流程如下主事务向消息队列发送预备消息主事务收到ACK之后本地执行主事务根据执行的结果成功或失败向消息队列发送提交或者回滚消息详细的流程如下图图片来源见水印所示