1. 事务机制
事务机制是可以保证一系列写操作要么全部完成,要么全部不会完成,不会发生只完成一系列中一两个写操作,事务机制缺省一般由数据库完成,也可以通过应用程序实现。
事务由四个属性,简称ACID:
- Atomicity原子性:一个事务的所有系列操作步骤被看成是一个操作,也就是一个原子操作,打个比喻:多个SQL语句如同一个SQL执行一样。
- Consistency一致性:如果两个以上数据表有关联,那么更新一个表同时另外一个表也要一起更新,否则两个表的数据记录就发生不一致了。
- Isolation隔离性:主要用于实现并发控制, 隔离能够确保并发执行的事务能够顺序一个接一个执行,通过隔离,一个未完成事务不会影响另外一个未完成事务,隔离是通过用悲观或乐观锁机制实现的。
- Durability耐久性:一个成功的事务将永久性地改变系统的状态,所以在它结束之前,所有导致状态的变化都记录在一个持久的事务日志中。如果我们的系统突然受到系统崩溃或断电,那么所有未完成已提交的事务可能会重演。
关系数据库提供默认不同级别的ACID,完全真正严格的ACID会导致性能下降,因此,事务代表的可靠性和性能扩展性是一对矛盾,如何进行平衡是架构设计的主要考量,特别是在分布式系统中如何保证ACID事务是一项极具有挑战力的工作。
2. 数据库ACID
事务在当今的企业系统无处不在,即使在高并发环境下也可以提供数据的完整性。一个事务是一个只包含所有读/写操作成功的集合。如下图:
尽管一些数据库系统提供多版本并发控制MVCC, 他们的并发控制都是通过锁完成,因此,锁会增加执行的串行性,影响并发性。
SQL标准规定了四个隔离水平:
- READ_UNCOMMITTED
- READ_COMMITTED
- REPETABLE_READ
- SERIALIZABLE
隔离级别 | 脏读 | 非重复读 | 幻读 |
---|---|---|---|
READ_UNCOMMITTED | allowed | allowed | allowed |
READ_COMMITTED | prevented | allowed | allowed |
REPETABLE_READ | prevented | prevented | allowed |
SERIALIZABLE | prevented | prevented | prevented |
2.1 脏读
脏读发生在:当一个事务允许读取一个被其他事务改变但是未提交的状态时,这是因为并没有锁阻止读取,如上图,你看到第二个事务读取了一个并不一致的值,不一致的意思是,这个值是无效的,因为修改这个值的第一个事务已经回滚,也就是说,第一个事务修改了这个值,但是未提交确认,却被第二个事务读取,第一个事务又放弃修改,悔棋了,而第二个事务就得到一个脏数据。
2.2 非重复读
非复读同一个数据却得到不同的结果,这是因为在反复几次读取的过程中,数据被修改了,这就导致我们使用了stale数据,这可以通过一个共享读锁来避免。这是隔离级别READ_COMMITTED会导致可重复读的原因。设置共享读锁也就是隔离级别提高到REPETABLE_READ。
2.3 幻读
当第二个事务插入一行记录,而正好之前第一个事务查询了应该包含这个新纪录的数据,那么这个查询事务的结果里肯定没有包含这个刚刚新插入的数据,这时幻影读发生了。
2.4 几种数据库的隔离级别
Database | Default isolation Level |
---|---|
Oracle | READ_COMMITTED |
MySQL | REPETABLE_READ |
Microsoft SQL Server | READ_COMMITTED |
PostgreSQL | READ_COMMITTED |
READ_COMMITED 是正确的选择,因为SERIALIZABLE虽然能在不同事务发生时避免stale数据,也就是避免上述丢失刚刚修改的数据,但是性能是最低的,因为是一种最大化的串行。
3. 分布式系统的CAP原理
- Consistency(一致性), 数据一致更新,所有数据变动都是同步的;
- Availability(可用性), 好的响应性能;
- Partition tolerance(分区容忍性) 可靠性;
跨数据库两段提交事务:2PC (two-phase commit), 2PC is the anti-scalability pattern (Pat Helland) 是反可伸缩模式的,JavaEE中的JTA事务可以支持2PC。因为2PC是反模式,尽量不要使用2PC,使用BASE来回避。
BASE模型反ACID模型,完全不同ACID模型:
- 牺牲高一致性,获得可用性或可靠性;
- 基本可用,支持分区失败;
- 软状态状态可以有一段时间不同步,异步;
- 最终一致,最终数据是一致的就可以了,而不是时时高一致;
4. ACID中C与CAP定理中C的区别
ACID和CAP定理中都有C,代表Consistent一致性,很多人容易将这两个C混为一谈,其实这两个一致性是有区别的。
事务的定义是一系列操作要么全部成功,要么全部不成功,数据库的事务机制是通过ACID实现的,ACID中的一致性的定义是:一个事务可以封装状态改变(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。
也就是说:如果事务是并发多个,系统也必须如同串行事务一样操作。其主要特征是保护性和不变性(Preserving an Invariant),以转账案例为例,假设有五个账户,每个账户余额是100元,那么五个账户总额是500元,如果在这个5个账户之间同时发生多个转账,无论并发多少个,比如在A与B账户之间转账5元,在C与D账户之间转账10元,在B与E之间转账15元,五个账户总额也应该还是500元,这就是保护性和不变性。
如果说ACID的C是节点服务器的数据完整性,而CAP的一致性是分布式多服务器之间复制数据以取得这些服务器拥有同样的数据,这是一种分布式领域的一致性概念。因此两者是完全不同的概念。
分布式领域中的一致性有的强弱之分,强一致性也就是指一旦有写操作写入任何一个服务器,立即在其他服务器之间同步复制新的数据,这样, 任何服务器上任何读操作总是能看到最近写入的新数据。如果不能立即看到最近写入的新数据,而可能过了一段时间才能看到,则属于弱一致性或最终一致性了。
强一致性分为由写实现一致性Consistency by writes、由读实现一致性Consistency by reads和由冲裁实现一致性Consistency by Quorum。
由写实现一致性:在写入数据同时,将数据复制到其他服务器上,读取任何一台都可以获得新的写入数据,复制数据是在写操作完成,读操作轻量。
由读实现一致性:写入一旦服务器后,不再复制,而是在读取时使用版本来协调复制(如vector clock算法),这样我们简化了写操作,而将负担加在读操作。
由冲裁实现一致性:如果写入时复制到其他2/3大多数服务器,读取时也是从2/3大多数服务器读取,读取这边负责解决哪个更新是最新结果,这在读操作和写操作之间分担了负载。
回到事务话题,如果要在分布式系统中实现像ACID那样的事务机制,只有强一致性还是不够的,如果我们操作步骤顺序很重要,不可以中断或打乱,我们要么一起一次执行它们,如果并发执行这些操作步骤,无论怎么并发,也要如同它们是在独立执行,我们最终得到的结果总是相同的,这是一种更强的一致性:线性一致性linearizable consistency,类似ACID中的隔离层(serial isolation level)。
The CAP FAQ将CAP定理中的一致性定义为这种线性一致性或称为atomic原子一致性。一种比普通一致性更强的一致性,这也是大家又将ACID的C和CAP的C等同在一起的原因。ACID的C与CAP的C的关系类似精确与一致性的关系,如下图:
这种分布式的线性强一致性有两种实现方式:2PC两段提交和Paxos算法是常见两种。
通过2PC写入新数据需要经过两次来回,第一次请求commit,第二次才正式确认commit,在这两者之间过程中,所有服务器都会堵塞等待发起者发出整个事务成功还是失败的结果(只有发起者知道所有服务器的情况),如果失败,所有服务器返回之前状态,相当于写入数据失败,写入数据没有发生过一样。
5. 2PC
2PC(2 phase commit)是一种分布式事务进行两段事务提交的简称,JavaEE的JTA/XA是2PC一种实现。2PC适合有多个数据源情况下统一按照ACID原则完成操作,比如一个操作涉及三个数据库的三个表a b c,如何保证这三个表的数据同时操作完成,保证在同一逻辑下的一致性,这是2PC关注所在,如果没有2PC,有可能a表修改成功,b表和c表没有修改成功,那么就出现不一致性。
下图是传统分布式2PC两段事务提交示意图
两段提交协议是一种分布式资源的原子确认协议,这个协议是通过两段过程完成业务数据的更改:第一段是预准备阶段,事务管理器通知所有分布式资源准备接受提交或退出事务,第二阶段,事务管理器安装每个分布式资源的回应情况决定是真正提交完成事务或者退出事务。在第一阶段,我们通过服务的JTA事务修改的数据库数据并没有真正写入数据库,只有最后阶段完成时才真正写入。
以转账案例为说明,A账户转到B账户100元,A账户余额应该减去100元,而B帐号增加100元,如果A账户在上海机房服务器,B帐号在北京机房,那么通过2PC保证这种加减一致性,如果没有2PC,A帐号已经减去100元,但是B账户却没有增加100元,整个操作也没有报错回滚,所以,2PC是保证业务逻辑正确性,精确性的。
注意:2PC的这种分布式数据一致性与CAP定理中分布式数据一致性是有区别的
5.1 2PC的问题
2PC的问题主要由于并发性能不高,由于一段事务需要经过两个阶段,这两个阶段需要将资源上锁,而分布式环境中网络抖动是不可避免的,因此容易造成事务失败,或者由于事务正在进行中,锁定资源太多,比如锁定数据表记录太多,导致其他事务等待时间较长。区块链等技术由于采取每个区块链接到上一个区块,类似一个LinkedList,虽然保证了分布式数据的可靠性,但是吞吐量和并发性能却不高,两者机制是差不多,2PC性能反而会好一些,因为数据库资源相对是集中的。
传统2PC分布式事务能够保证数据可靠性,但是无法提高吞吐量和并发性能,如何进行权衡?这就需要引入分布式系统的定理:CAP定理
通过降低传统2PC的强一致性,使用弱一致性替代,从而就能在出现网络抖动导致分区的情况下提高可用性,等网络连通正常后,再进行数据同步复制,保证数据的一致性,这种方式也称为柔性事务。其中Saga事务是一种分布式柔性事务的探索。
还有一种TCC(Try-Confirm-Cancel)其实是JTA 2PC的一种补充,TCC中第一个C是提交的意思,TCC中第二个C如果看成是类似2PC的回滚Rollback,那么其实就是两段提交;如果将第二个C理解为补偿有些牵强,补偿的意思是不断新增补充,而不是撤销回退原来的动作,补偿本身概念有始终向前的意思,既然有补偿概念,就需要有过去动作的历史记录,没有历史记录你怎么补偿,你如果撤销删除了以前的动作就不是补偿,如同你把财务账本上一条记录用涂改液擦除了,那是要犯法的,补偿就是你不能回退擦除以前的记录,只能新增一条记录,来冲抵以前的错误记录,所以,谈到补偿肯定需要类似账本的流水记录的。
分布式事务有很多探索,比如使用Paxo/Raft实现分布式环境下的数据高一致性,Google的Spanner和FoundationDB以及微软的Azure Cosmos DB都属于分布式强一致性事务的数据库。
还有一种分布式事务机制,使用事件溯源方式,再引入Kafka这样的原子消息系统,确保消息送达目标,同时保证在目标实现单写方式(Stream流),这种使用事件同步替代状态同步,从而能实现间接实现多个节点之间的数据最终一致性。
总结我们上面讨论的问题:2PC在四个方面使系统中毒:延迟(协议的时间加上冲突事务的停顿时间),吞吐量(因为它需要防止在协议期间运行其他冲突的事务),可扩展性(更大)在系统中,事务变得多分区并且必须支付2PC的吞吐量和延迟成本以及可用性(我们上面讨论的阻塞问题)的可能性越大。
5.2 2PC的替代方案
2PC的替代方案是拥有一个始终向前的补偿事务日志。更新记录在事务日志中,然后将每个更新发送给工作者,他们会看到更新是否应用于他们,并提交与他们自己的数据库相关的更新。没有回滚。可以通过发布补偿事务来否定先前的效果来应用逻辑“回滚”。例如,发行10美元借方的新交易以补偿之前10美元贷方的交易。(注:这是借鉴财务借记冲帐的做法,财务上在发生错误时是不能修改,只能新增一笔相反的交易进行冲账,比特币区块链也是借鉴于此)。
例如,一个工作者维护数据库Inventory表。另一个工作者维护另外一个数据库Order表。当前端应用程序创建订单时,它会在补偿事务日志中记录事务[(减少库存中的部分),(在订单中创建新记录)]。请注意,这两个更新都在一个事务中。
当Inventory工作程序收到事务时,它会应用与其表相关的更新,即Inventory表,并忽略Order更新。Order工作程序接收相同的事务并应用相关的Order更新,同时忽略事务中的Inventory更新。
在订单被取消的情况下,可以在交易日志中创建[(库存中的增量部分),(在订单中取消的该笔记录)]的补偿交易。工作人员可以接收交易并应用与其相关的更新。
两个工作人员数据库都是分离的,可以按照自己的步调进行工作。它们可以崩溃而不会影响另一个。在一天结束时,事情会得到调和并且是一致的。
该方案的缺点是数据的及时性。一个系统可能会停止或缓慢应用更新并落后于另一个系统。
这个方案中:始终向前 + 补偿事务日志 这两个非常重要,“始终向前” 就是没有回滚,确保写入正常,如果不正常,重试,保留事务日志,不断重试,直至人工介入纠正原来的错误保证写入成功,因此在写入方面避免竞争和所,无锁的单写方式可以保证写入事务日志正确,一旦更改事件写入了事务日志,就会持久到一个存储文件或数据库,比如Kafka的日志就是天然的事务日志,kafka的消费者在消费失败重启后会自动重试,消息不会丢。这个模式基本是Saga模式。
还有一种TCC(Try-Confirm-Cancel)其实是JTA 2PC的一种补充,TCC中第二个C如果看成是类似2PC的回滚,那么这还是两段提交,况且TCC中第一个C就是提交的意思;如果将第二个C理解为补偿有些牵强,补偿的意思是不断新增补充,而不是撤销回退原来的动作,补偿本身概念有始终向前的意思,既然有补偿概念,就需要有过去动作的历史记录,没有历史记录你怎么补偿,你如果撤销删除了以前的动作就不是补偿,如同你把财务财务账本上一条记录用涂改液擦除了,那是要犯法的,补偿就是你不能回退擦除以前的记录,只能新增一条记录,来冲抵以前的错误记录。