事务(Transaction)是并发控制的单位,是用户定义的一个操作序列。 这些操作要么都做,要么都不做,是一个不可分割的工作单位。
本地事务要求符合ACID的特性:
原子,是指不能分解成更小的部分的东西。在多线程编程同时访问相同的数据会发生什么情况,这种问题是由隔离性ACID的原子性是在描述当用户进行多次写入,并且在一些写操作出现故障的情况,例如进程崩溃、网络连接中断,磁盘变满或者某种完整性约束被违反。多个操作被分到一个原子事务中,要不全部完成,要么全部回滚。如果回滚,可以确定应用程序本次操作没有带来任何改变,所以可以安全地进行重试。在Mysql中原子性的实现是主要依靠其undo log来实现的。Mysql的undo log记录了事务修改操作之前的数据,用于在当前事务发生回滚的时候,使该条数据状态恢复到事务开始前的状态。
一致性的概念是:“对数据的一组特定约束必须始终成立,即不变量(invariants)”。例如,在银行账户的转账操作中,收款方和转账方的总余额是永远不变的。但是一致性的这种概念取决于应用程序对于不变量的观念。应用程序负责正确定义事务,并保持一致性。这并不是数据库可以保证的事情,数据库只负责存储,至于存储的是不是脏数据,需用它的用户来定义。应用程序需要依赖数据库提供的AID来实现C,但C并不属于数据库,所以ACID中的C有点拼凑的嫌疑。
如在原子性的表述。这里的隔离性是数据库用来解决竞态条件(race conditions)问题的。隔离性的意思是:在数据库中同时执行的多个事务是相互隔离的,他们彼此间不会相互影响。
Mysql中实现隔离性主要是通过加锁(排它锁和共享锁等)以及MVCC(全称Multi-Version Concurrency Control,即多版本并发控制)来实现的。
Mysql中的Innodb引擎支持事务,有4个隔离级别:
持久性是指:一旦事务成功完成,即使发生硬件故障或者数据库崩溃,写入的任何数据也不会丢失。在单节点的数据库中,持久性通常意味着数据已经被写入非易失性存储设备,如硬盘或者SSD。在带复制的数据库中,持久性可能意味着数据已成功复制到一些节点,为了提供持久性保证,数据库必须等到这些写入或者复制完成之后,才能报告事务成功提交。最真实的是“完美的持久性是不存在的”:如果所有硬盘都被损坏,那依然没有任何数据库能够救得了你。
事务的一个关键特性是指:如果发生了意外,所有操作被终止,之后可以安全地重试。ACID数据库基于这样的一个理念:如果存在违反原子性、隔离性或者持久性的风险,则完全放弃整个事务,而不是部分放弃。从这一点上说,支持安全的重试机制甚至说支持安全的容错机制,并且尽可能地把这些与使用数据库的客户端隔离开来,才是事务的职责所在。分布式事务也可以说是沿着这个思路,尝试建立可以让分布式应用忽略内部各种问题的抽象机制。
CAP理论又叫Brewer理论,是加州大学伯克利分校的埃里克.布鲁尔(Eric Brewer)教授提出的一个猜想,后来麻省理工学院的赛斯.吉尔伯特(Seth Glibert)和南希.林奇(Nancy Lynch)就以严谨的数学推理证明了这个CAP猜想。在这之后CAP理论在分布式领域盛行一时。
CAP定理是以下三个特性最多只能其中两点,不可能三者兼顾。
CAP这三个特性中分区容错性是不可以放弃的,因为网络可能永远可靠,那么CAP就变成了CP或者AP间的博弈。如果放弃可用性(CP),那么一旦发生分区不一致问题,系统将无限期停止对外提供服务直至一致性达成。这等同于2PC/3PC的解决方案。如果**放弃一致性(AP)**则表示当发生分区问题,节点之间所提供的数据可能不一致。但是一致性是事务的最终目标,这里放弃一致性有点难以接受,所以后边又引入了“最终一致性”,也就是BASE理论。
ACID译为"酸",对应BASE译为"碱"。BASE是由eBay的系统架构师丹 · 普利切特(Dan Pritchett)提出,主要有三个特性:
提到2PC,不得不提XA规范。XA规范是X/Open组织定义的异构环境下实施两阶段提交的一个工业标准,于1991年推出并得到广泛推广。目前,许多传统关系数据库(包括PostgreSQL、MySql、DB2、SQL Server和Oracle)和消息队列(ActiveMQ等)都支持XA。XA规范使用两阶段提交来保证所有资源同时提交或者回滚,并规定了事务管理器(TM)和资源管理器(RM)接口。事务管理器相当于协调者,负责各个本地资源的提交和回滚;而资源管理器就是分布式事务的参与者,通常为数据库。
两阶段提交通过引入事务管理器(也称协调者)将事务的提交分为了投票(canCommit)和提交(DoCommit)两个阶段,这或许也是其名称的由来。
协调者会向事务的资源管理器发起执行操作的CanCommit请求,并阻塞等待参与者的响应。参与者接收到请求后,会执行请求中的事务操作,将操作信息记录到事务日志中但不提交(即不修改数据库中的数据),待参与者执行成功,向协调者发送“YES”消息,表示同意操作,若不成功,则发送“NO”消息,表示终止操作。
当所有参与者都返回了操作结果之后,系统进入第二阶段——提交阶段。在第二阶段中,协调者会对所有返回消息进行判定,如果所有消息均为“YES”,则持久化事务状态为“Commit”,并向参与者发送“DoCommit”消息。参与者收到“DoCommit”消息后,完成剩余操作,释放之前锁定资源,然后向协调者返回“HaveCommited”消息;若协调者从参与者收到的消息包含“NO”,则持久化事务状态为“Abort”,并向所有参与者发送“DoAbort”消息。此时所有之前发送“YES”消息的参与者再收到“DoAbort”消息后进行回滚,然后向协调者发送“HaveCommitted”消息。基于XA的两阶段提交算法尽可能地保证了数据的强一致性,且实现成本较低,但依然有些不足:
3PC是2PC的改进版本。为了解决2PC存在的问题,3PC引入了超时机制和预提交阶段。2PC也有超时机制,只不过是只有协调者才有,改进后的3PC在协调者和参与者均引入了超时机制,这时如果参与者或者协调者在规定的时间内没有接收到其他节点的响应,就会根据当前的状态选择提交或者终止整个事务,从而减少了整个集群的阻塞时间,在一定程度上也减少了2PC中出现的性能问题和单点问题;
三阶段提交协议有CanCommit、PreCommit、DoCommit三个阶段,相当于把投票阶段拆分成了CanCommit和PreCommit两个阶段。
然而在数据一致性的问题上,3PC并没有改善:因为在DoCommit阶段引入了超时预判机制,当协调者发送出的最终指令是“Abort”时并且恰巧碰到了网络问题,这个时候会导致一部分接收到消息的参与者回滚,没收到消息的由于超时机制会提交事务,导致不一致问题。
上边阐述的2PC和3PC是一种追求强一致性的解决方案,而可靠消息队列则是最终一致性的解决方案。借用我之前做过的一个例子来阐述可靠事件队列,上家公司是家门户网站,我所在广告投放部门有这样一个需求:需要对广告主开放支持投放另一媒体(涉密,这里称媒体B)的广告。具体就是说广告主可以在我们的平台录入投放另一媒体的广告信息,主要包括广告组、广告、创意等,然后需要我们通过消息同步的方式将数据同步到对方的投放存储中。这里我们使用的解决方案就是类似于可靠事件队列的方式。
上图是我根据可靠事件队列的模式画出来的序列图,但是当时实际研发有一点不同的是我们并没有使用消息队列,一个是因为我们隶属不同公司,再一个业务初创没有必要。我们是使用mysql任务表,外加一个定时服务不停去扫描任务表中的未同步成功的任务去做同步处理(当然这里有一定上限设定)。
在这个模型实现中,广告主在广告管理平台中向本地的Mysql完成新增/修改操作,会在事务中同步新增一个任务表(或者同步任务内容到消息队列),直到定时任务扫描到任务并且同步成功,最终达到两方的数据一致性。这里需要注意的是任务的同步要求具有幂等性,通常采用携带事务ID或者版本号的方式,以保证一个事务中的操作只会被执行一次。
当同步重试超过一定的上限,可能是由于某些约束条件实在无法满足,需要人工介入,当时我们的项目有另外一个可视化页面可以对失败的任务情况进行监视并且手动重试。
这种靠着持续重试的方式来保证一致性的操作,有个专业的名字叫“最大努力交付(Best-Effort Delivery)”,比如TCP协议中的可靠性保障,就属于最大努力交付。
TCC是除可靠消息队列外的另一种常见的分布式事务机制,由数据库专家帕特 · 赫兰德(Pat Helland)提出。使用可靠消息队列的方式解决了最终一致性的问题,但是对于隔离性的问题却没有保证。对于有些扣费业务,当多个商户同时操作的时候,容易发生超扣的现象。这个时候可以考虑TCC解决方案。
TCC的实现相对较为复杂,业务入侵性也较高,要求业务处理过程必须拆分为“预留业务资源”和“确认/释放消费资源”两个子过程。主要分三个阶段:
TCC类似于上文中的2PC,但又有所不同:TCC位于用户代码层面,而非基础设施层面。它以较强的业务入侵性带来了较高的灵活性,让我们可以根据需要设计资源锁定的粒度。
SAGA历史较为悠久,是普林斯顿大学的赫克托·加西亚·莫利纳(Hector Garcia Molina)和肯尼斯·麦克米伦(Kenneth Salem)发明的。
SAGA提供了一种基于数据补偿来代替回滚的解决思路:把一个大事务分解为可以交错运行的一系列子事务的集合,由2部分组成。
一部分是把大事务拆分为若干个小事务,将整个分布式事务T分解为n个子事务,我们命名T1,T2,...,Ti,...,Tn。每个子事务都应该、或者能被看做是原子行为。如果分布式事务T能够正常提交,那么它对数据的影响(最终一致性)就与连续按顺序成功提交子事务T等价。
另一部分是每一个子事务对应的补偿操作,我们命名为C1,C2,...,Ci,...,Cn。Ti与Ci满足如下条件:
SAGA的思路也可以用在前面广告信息同步上,我们可以把广告组、广告、创意依次同步到对方媒体的数据存储中,如果创意数据在对方的数据库违反了约束条件,则需要依赖对方提供的删除接口删除广告、广告组的数据。
AT事务是对XA 2PC的一个升级版本。
它的大致做法是在业务数据提交时,自动拦截所有SQL,分别保存SQL对数据修改前后结果的快照,生成行锁,通过本地事务一起交到操作的数据源中,这就相当于自动记录了redo日志和undo日志。所以,基于这种补偿方式,分布式事务中所涉及的每一个数据源都可以单独提交,然后立刻释放锁和资源。AT事务这种异步提交的模式,相比2PC极大地提升了系统的吞吐量。而使用的代价就是大幅度牺牲了隔离性,甚至于影响到了原子性。因为在缺乏隔离性的前提下,以补偿代替回滚不一定总是成功。
对于脏写(写覆盖)的处理,需要借助于分布式锁的实现来实现写隔离,甚至如果要限制脏读,也可以这样做,但是会对性能造成很大影响。
本文从本地事务的ACID引申到分布式事务的解决方案:从开始的2PC到3PC的升级点,再到后来CAP理论的引入让我们的目光从强一致性的追求上解脱出来,去考虑C(一致性)与A(可用性)的取舍。C作为事务的目标,舍弃往往是难以接受的。所以BASE引入了最终一致性。本文同样也叙述了使用最终一致性的可靠消息队列、TCC、SAGA以及TA 四种解决方案。如同,计算机内时间和空间的博弈一样,这里没有胜负。有的是我们针对不同业务场景,在不同解决方案中的选择。