Skip to main content

事务参考知识

ACID特性#

事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持久性( Durability )。这四个特性简称为 ACID 特性。 如果一个数据库声称支持事务的操作,那么该数据库必须要具备这四个特性。

  • 原子性(atomicity):事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。即事务包含的所有操作要么全部成功,要么全部失败回滚。
  • 一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的。也就是说一个事务执行之前和执行之后都必须处于一致性状态。
  • 隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 隔离性又分为四个级别:读未提交(read uncommitted)、读已提交(read committed,解决脏读)、可重复读(repeatable read,解决虚读)、串行化(serializable,解决幻读)。
  • 持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

事务隔离#

事务存在的问题:

  1. 第一类丢失更新
    撤销一个事务的时候,把其它事务已提交的更新数据覆盖了。这是完全没有事务隔离级别造成的。如果事务1被提交,另一个事务被撤销,那么会连同事务1所做的更新也被撤销。
  2. 脏读(Dirty Read)
    如果一个事务对数据进行了更新,但事务还没有提交,另一个事务就可以“看到”该事务没有提交的更新结果。这样就造成的问题就是,如果第一个事务回滚,那么第二个事务在此之前所“看到”的数据就是一笔脏数据。
  3. 不可重复读取(Non-Repeatable Read)
    不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔期间,被另一个事务修改并提交了。不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。所以Read Uncommitted也无法避免不可重复读取的问题。
  4. 第二类丢失更新
    它和不可重复读本质上是同一类并发问题,通常将它看成不可重复读的特例。当两个或多个事务查询相同的记录,然后各自基于查询的结果更新记录时会造成第二类丢失更新问题。每个事务不知道其它事务的存在,最后一个事务对记录所做的更改将覆盖其它事务之前对该记录所做的更改。
  5. 幻读(Phanton Read)
    幻读是指同样一个查询在整个事务过程中多次执行后,查询所得的结果集是不一样的。幻读和不可重复读都是读取了另一个已经提交的事务(这点就脏读不同)的数据,所不同的是不可重复读查询的都是同一个数据项,而幻读问题针对的是查询到的同一个数据集合。在Read Uncommitted隔离级别下,不管事务2的插入操作是否提交,事务1在插入操作执行之前和之后执行相同的查询,取得的结果集是不同的,所以Read Uncommitted同样无法避免幻读。

事务隔离解决的问题:

  • 无事务隔离级别:存在第一类丢失更新、脏读、不可重复读、第二类丢失更新和幻读问题。
  • 读未提交(Read Uncommitted):避免第一类丢失更新的发生,存在脏读、不可重复读、第二类丢失更新和幻读问题。在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别是最低的隔离级别,虽然拥有超高的并发处理能力及很低的系统开销,但很少用于实际应用。
  • 读已提交(Read committed):避免第一类丢失更新和脏读的发生,存在不可重复读、第二类丢失更新和幻读问题。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。
  • 可重复读(Repeatable Read):避免第一类丢失更新、脏读、不可重复读、第二类丢失更新的发生,存在幻读问题。这是MySQL的默认事务隔离级别,它确保同一事务多次读取同一数据项时,会看到同样的数据行。
  • 串行化(Serializable):避免所有问题,不存在问题。这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突。在这个级别,可以解决上面提到的所有并发问题,但可能导致大量的超时现象和锁竞争,通常数据库不会用这个隔离级别,我们需要其他的机制来解决这些问题: 乐观锁和悲观锁。

#

  • 乐观锁
    乐观锁不是数据库自带的,需要自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。 乐观锁需要最低事务级别为:读已提交(Read committed)。
    通常是这样实现的:先给数据表加一个版本(version)字段,对表中的数据进行更新时,每操作一次,将对应记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行更新,则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行过更新操作,则可以执行更新,并同时将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行了更新操作,则不进行更新操作。请看下面对例子:
    1、数据库表设计
    三个字段,分别是id、value、version
    select id,value,version from TABLE where id=#{id}
    2、每次更新表中的value字段时,为了防止发生冲突,需要这样操作
    update TABLE set value=2,version=version+1 where id=#{id} and version=#{version};
  • 悲观锁
    悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,所以悲观锁需要耗费较多的时间。
  • 共享锁
    共享锁又称读锁 read lock,是读取操作创建的锁,属于悲观锁的一种,共享锁指的是对于多个不同的事务,对同一个资源共享同一个锁,其他事务可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。在MYSQL中,通过查询语句后面加上lock in share mode就对集合资源加上共享锁了。 典型用例是针对父子关系的数据表场景,当对子表记录进行变更时,对父表中的父记录加共享锁,确保更新过程中父表对应记录与子表关系不发生变化,例如,避免向子表添加记录时父表被其他事务删除了。
  • 排它锁
    排它锁属于悲观锁的一种,指的是对于多个不同的事务,对同一个资源只能有一把锁,其他悲观锁(共享锁或排它锁)不能再对锁定的记录加锁。在MYSQL中,通过查询语句后面加上for update就对集合资源加上排它锁了。
  • 行锁
    行锁,就是给某一行或多行加上锁。行锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行锁的,会使用表锁。例如上面的悲观锁,如果查询条件针对的是一条或多条记录且能用到索引,那么就是行锁,另外事务中针对行的update/delete语句如果条件用到了索引就会加上行锁,否则会使用表锁。
  • 表锁
    表锁,就是给整个表加上锁。如果事务中的update/delete语句中的条件子句中具有不确定性或没有用索引,就会加上表锁锁住整个表。另外,MYSQL的MyISAM引擎只支持表锁,没有行锁。
  • 死锁(Deadlock)
    死锁,是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象,死锁。

分布式事务#

互联网经过近10来年的迅猛发展,绝大部分公司技术架构都进行了数据库拆分和服务化(SOA)。在这种情况下,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到了分布式事务,由于需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据的操作要么全部成功,要么全部失败。

分布式事务典型场景#

  1. 跨库事务
    跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据,需要确保对多个库的操作要么都成功,要么都失败。

  2. 分库分表
    通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。分库分表后,要保证对多个库的操作要么都成功,要么都失败,这就出现了分布式事务问题。

  3. 服务化(SOA)
    现如今,微服务(Microservice)已经成为了服务化(SOA)的代名词,是指将单体应用或服务拆分成不同的独立服务,以简化业务逻辑或方便集群化部署以增强负载能力。拆分后,独立服务之间通过RPC框架或http协议来进行远程调用,实现彼此的通信。例如,Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B又同时操作了2个数据库,Service C也操作了一个库。需要保证这些跨服务的对多个数据库的操作要么都成功,要么都失败,这应该是最典型的分布式事务应用场景。

原子性的提交协议#

原子提交协议(Atomic Commit Protocol)希望实现的2个特性:

  1. 安全性(Safety)
  • 如果任意一方 commit,所有人必须都 commit
  • 如果任意一方中断,则没有任何一方进行 commit
  1. 存活性(Liveness)
  • 没有宕机或失败发生时,所有事务参与方都能 commit,则 commit
  • 发生失败时,最终能达成一个一致性结果(成功/失败),予以响应,不能一直等待

XA#

XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。

两阶段提交(2PC)#

两阶段提交(Two-Phase Commit),是将整个事务流程分为2个阶段,准备阶段(Prepare phase)和提交阶段(Commit phase)。 两阶段提交协议是协调所有分布式原子事务参与者,并决定提交或取消(回滚)的分布式算法。所有关于分布式事务的介绍中都必然会讲到两阶段提交,因为它是实现XA分布式事务的关键,确切地说,两阶段提交主要保证了分布式事务的原子性,即所有节点要么全成功要么全失败。

第一阶段,准备阶段(Prepare phase):
协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,则会写redo或者undo日志,然后锁定资源,执行操作,但并不提交;

第二阶段,提交阶段(Commit phase):
如果所有参与者都明确返回准备成功,则协调者向参与者发生提交指令,参与者释放锁定的资源,如果任何一个参与者明确返回准备失败,则协调者会发生终止指令,所有参与者取消已经变更的事务,释放锁定的资源。

缺点:

  1. 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。

  2. 单点故障。由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

  3. 数据不一致。在二阶段提交的提交阶段中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了commit请求。 而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。

三阶段提交(3PC)#

三阶段提交(Three-phase commit),是2PC的改进版,实质是将2PC中提交事务请求拆分为两步,形成了CanCommit、PreCommit、doCommit三个阶段的事务一致性协议。

第一阶段,canCommit阶段:
3PC的canCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回yes响应,否则返回no响应。

第二阶段,preCommit阶段:
协调者根据参与者canCommit阶段的响应来决定是否可以继续事务的preCommit操作。根据响应情况,有下面两种可能:

  • 协调者从所有参与者得到的反馈都是yes:那么进行事务的预执行,协调者向所有参与者发送preCommit请求,并进入prepared阶段。参与者接收到preCommit请求后会执行事务操作,并将undo和redo信息记录到事务日志中。如果一个参与者成功地执行了事务操作,则返回ACK响应,同时开始等待最终指令。

  • 协调者从所有参与者得到的反馈有一个是No或是等待超时之后协调者都没收到响应:那么就要中断事务,协调者向所有的参与者发送abort请求。参与者在收到来自协调者的abort请求,或超时后仍未收到协调者请求,执行事务中断。

第三阶段,doCommit阶段:
协调者根据参与者preCommit阶段的响应来决定是否可以继续事务的doCommit操作。根据响应情况,有下面两种可能:

  • 协调者从参与者得到了ACK的反馈:协调者接收到参与者发送的ACK响应,那么它将从预提交状态进入到提交状态,并向所有参与者发送doCommit请求。参与者接收到doCommit请求后,执行正式的事务提交,并在完成事务提交之后释放所有事务资源,并向协调者发送haveCommitted的ACK响应。那么协调者收到这个ACK响应之后,完成任务。

  • 协调者从参与者没有得到ACK的反馈, 也可能是参与者发送的不是ACK响应,也可能是响应超时:执行事务中断。

  • 参与者迟迟不能收到来自协调者的 doCommit 或 abort 请求,那么参与者将在等待超时后继续 commit。

缺点:
如果进入PreCommit后,协调者发出的是abort请求,假设只有一个参与者收到并进行了abort操作,而其他对于系统状态未知的参与者会根据3PC选择继续Commit,此时系统状态发生不一致性。

2PC与3PC的区别#

相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

了解了2PC和3PC之后,我们可以发现,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。Google Chubby的作者Mike Burrows说过, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意思是世上只有一种一致性算法,那就是Paxos,所有其他一致性算法都是Paxos算法的不完整版。

CAP#

CAP最初由Eric Brewer在2000年PODC会议上提出。CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

  1. 一致性(Consistency)
    一致性,指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,不能存在中间状态。例如对于电商系统用户下单操作,库存减少、用户资金账户扣减、积分增加等操作必须在用户下单操作完成后必须是一致的。不能出现类似于库存已经减少,而用户资金账户尚未扣减,积分也未增加的情况。如果出现了这种情况,那么就认为是不一致的。 关于一致性,如果的确能像上面描述的那样时刻保证客户端看到的数据都是一致的,那么称之为强一致性(strong consistency) (又叫原子性 atomic、线性一致性 linearizable consistency)。如果允许存在中间状态,只要求经过一段时间后,数据最终是一致的,则称之为最终一致性。此外,如果允许存在部分数据不一致,那么就称之为弱一致性。

  2. 可用性(Availability)
    可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。“有限的时间内”是指,对于用户的一个操作请求,系统必须能够在指定的时间内返回对应的处理结果,如果超过了这个时间范围,那么系统就被认为是不可用的。试想,如果一个下单操作,为了保证分布式事务的一致性,需要10分钟才能处理完,那么用户显然是无法忍受的。“返回结果”是可用性的另一个非常重要的指标,它要求系统在完成对用户请求的处理后,返回一个正常的响应结果,不论这个结果是成功还是失败。

  3. 分区容错性(Partition tolerance)
    分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

CAP三个特性只能满足其中两个,那么取舍的策略就共有三种:

  • CA without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但放弃P的同时也就意味着放弃了系统的扩展性,也就是分布式节点受限,没办法部署子节点,这是违背分布式系统设计的初衷的。传统的关系型数据库RDBMS:Oracle、MySQL就是CA。
  • CP without A:如果不要求A(不要求可用性),相当于每个请求都需要在服务器之间保持强一致,而P(分区容错性)会导致同步时间无限延长(也就是等待数据同步完才能正常访问服务),一旦发生网络故障或者消息丢失等情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统。设计成CP的系统其实不少,最典型的就是分布式数据库,如Redis、HBase等。对于这些分布式数据库来说,数据的一致性是最基本的要求,因为如果连这个标准都达不到,那么直接采用关系型数据库就好,没必要再浪费资源来部署分布式数据库。
  • AP wihtout C:要高可用并允许分区,则需放弃C(放弃一致性)。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。典型的应用就如某米的抢购手机场景,可能前几秒你浏览商品的时候页面提示是有库存的,当你选择完商品准备下单的时候,系统提示你下单失败,商品已售完。这其实就是先在 A(可用性)方面保证系统可以正常的服务,然后在数据的一致性方面做了些牺牲,虽然多少会影响一些用户体验,但也不至于造成用户购物流程的严重阻塞。

既然一个分布式系统无法同时满足一致性、可用性、分区容错性三个特点,我们就需要抛弃一个,需要明确的一点是,对于一个分布式系统而言,分区容错性是一个最基本的要求。因为既然是一个分布式系统,那么分布式系统中的组件必然需要被部署到不同的节点,否则也就无所谓分布式系统了。而对于分布式系统而言,网络问题又是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然需要面对和解决的问题。因此系统架构师往往需要把精力花在如何根据业务特点在C(一致性)和A(可用性)之间寻求平衡。而前面我们提到的X/Open XA 两阶段提交协议的分布式事务方案,强调的就是一致性;由于可用性较低,实际应用的并不多。而基于BASE理论的柔性事务,强调的是可用性,目前大行其道,大部分互联网公司采可能会优先采用这种方案。

BASE#

eBay的架构师Dan Pritchett源于对大规模分布式系统的实践总结,在ACM上发表文章提出BASE理论(https://queue.acm.org/detail.cfm?id=1394128)。 BASE理论是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。

  1. 基本可用(Basically Available)指分布式系统在出现不可预知故障的时候,允许损失部分可用性。

  2. 软状态( Soft State)指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性。

  3. 最终一致( Eventual Consistency)强调的是所有的数据更新操作,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

BASE理论面向的是大型高可用可扩展的分布式系统,和传统事务的ACID特性是相反的。它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此,在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。