分布式数据库事务

分布式数据库事务

概述

对于部署在云上的服务,仅仅由一台服务器来承载流量是不够的,因此数据库会被扩展到多台主机上。主要有两种扩展模式:

  • 分库/分表的扩展模式。这意思就是,在每一个节点(服务器)上,运行一套独立的数据库,然后在上面铺一层中间件,在收到了用户的请求之后,由中间件分发给节点来处理。运用这种模式,数据需要划分得特别干净

  • 很多时候我们无法做到划分出这么干净的数据,因此还有一种拓展方式是:并行/分布式数据库。 就是我们不把数据划分的任务交给中间件,而是直接构建一个分布式数据库系统,比如说 OceanBase。如下:

对于分布式数据库,电脑间的交互肯定是需要的,但是我们尽量要减少这种交互,因为网络交互存在带宽限制。

我们要了解三种 数据库构架设计: Shared Everthting、Shared Nothing、和Shared Disk:

  1. Shared Everthting:一般是针对单个主机,完全透明共享CPU/MEMORY/IO,并行处理能力是最差的,典型的代表SQLServer
  2. Shared Disk:各个处理单元使用自己的私有 CPU和Memory,共享磁盘系统。典型的代表Oracle Rac, 它是数据共享,可通过增加节点来提高并行处理的能力,扩展能力较好。其类似于SMP(对称多处理)模式,但是当存储器接口达到饱和的时候,增加节点并不能获得更高的性能 。
  3. Shared Nothing:各个处理单元都有自己私有的CPU/内存/硬盘等,不存在共享资源,类似于MPP(大规模并行处理)模式,各处理单元之间通过协议通信,并行处理和扩展能力更好。典型代表DB2 DPF和Hadoop ,各节点相互独立,各自处理自己的数据,处理后的结果可能向上层汇总或在节点间流转。

我们常说的 Sharding 其实就是Share Nothing架构,它是把某个表从物理存储上被水平分割,并分配给多台服务器(或多个实例),每台服务器可以独立工作,具备共同的schema,比如MySQL Proxy和Google的各种架构,只需增加服务器数就可以增加处理能力和容量。

但是,一旦网络的带宽和IO带宽达到相同数量级的时候,就相当于电脑访问自己硬盘中的数据和访问其他节点中的数据所花费的开销类似,这时候share nothing的优势就不明显了。

分布式事务的ACID

那么,如果使用Share nothing的话,我们该如何对数据进行切分? 可以使用哈希

在分布式数据库中,我们又该如何维持数据的正确性呢?

说到数据库事务就不得不说,数据库事务中的四大特性,ACID:

  • A:原子性(Atomicity)

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

就像你买东西要么交钱收货一起都执行,要么要是发不出货,就退钱。

  • C:一致性(Consistency)

事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

  • I:隔离性(Isolation)

指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

打个比方,你买东西这个事情,是不影响其他人的。

  • D:持久性(Durability)

指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

打个比方,你买东西的时候需要记录在账本上,即使老板忘记了那也有据可查。

隔离性

首先我们来讲分布式数据库事务的隔离性。

在两个节点分别处理两个子事务的情况下,是否只要保证了单个节点的原子性,就可以保证整体的原子性呢?

显然不是,我们以两个节点为例。

现在有两个事务,交给两个节点去处理,可以有以下两种处理方式

第一种方式,是序列化的,在每个节点上,都是先运行T1,再运行T2,而且T1、T2都符合原子性的标准。假设X=Y=0,那么最后它们都等于200

第二种方式,是非序列化的,在Node X上先执行T1.在Node Y上先执行T2。那那么这时候,由于二者是同步进行的,没有序列化,因此会互相造成干扰。如果X=Y=0, 那么,最后X = 200,Y=100

因此,当在分布式数据库中考虑序列化,我们要注意更多,不能依靠单个节点上的事务机制来保证所有事务的原子性。

在分布式数据库执行事务,我们需要考虑两类schedule:

  • Local schedule
  • Global schedule

因此,对于 Global Transaction,如果我们要将其序列化执行,也需要满足两个条件:

  • 每一个 Local Schedule 都必须是 可序列化的 (基本条件)
  • 对于 Global Transaction 的所有 sub-transactions ,都以相同的顺序出现在所有站点的等效串行时间表中,不能出现在X 节点上顺序是 T1->T2,在Y节点上顺序是 T2->T1的情况

Lock

两阶段锁

在介绍分布式两阶段锁之前,我们先来学习两阶段锁,它用于单机事务中的一致性和隔离性

在一个事务操作中,分为加锁阶段解锁阶段,且所有的加锁操作在解锁操作之前,具体如下图所示:

  • 加锁时机

当对记录进行更新操作或者select for update(X锁)、lock in share mode(S锁)时,会对记录进行加锁,锁的种类很多,不在此赘述。

  • 何时解锁

在一个事务中,只有在commit或者rollback时,才是解锁阶段。

  • 二阶段加锁最佳实践

下面举个具体的例子,来讲述二段锁对应用性能的影响,我们举个库存扣减的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
方案一:
start transaction;
// 锁定用户账户表
select * from t_accout where acount_id=234 for update
//生成订单
insert into t_trans;
// 减库存
update t_inventory set num=num-3 where id=${id} and num>=3;
commit;

方案二:
start transaction;
// 减库存
update t_inventory set num=num-3 where id=${id} and num>=3;
// 锁定用户账户表
select * from t_accout where acount_id=234 for update
//生成订单
insert into t_trans;
commit;

我们的应用通过JDBC操作数据库时,底层本质上还是走TCP进行通信,MySQL协议是一种停-等式协议(和http协议类似,每发送完一个分组就停止发送,等待对方的确认,在收到确认后再发送下一个分组),既然通过网络进行通信,就必然会有延迟,两种方案的网络通信时序图如下:

由于商品库存往往是最致命的热点,是整个服务的热点。如果采用第一种方案的话,TPS理论上可以提升3rt/rt=3倍。而这是在一个事务中只有3条SQL的情况,理论上多一条SQL就多一个rt时间。

另外,当更新操作到达数据库的那个点,才算加锁成功。commit到达数据库的时候才算解锁成功。所以,更新操作的前半个rtcommit操作的后半个rt都不计算在整个锁库存的时间内。

  • 性能优化

从上面的例子可以看出,在一个事务操作中,将对最热点记录的操作放到事务的最后面,这样可以显著地提高服务的吞吐量

  • select for update 和 update where的最优选择

我们可以将一些简单的判断逻辑写到update操作的谓词里面,这样可以减少加锁的时间,如下:

1
2
3
4
5
6
7
8
方案一:
start transaction
num = select count from t_inventory where id=234 for update
if count >= 3:
update t_inventory set num=num-3 where id=234
commit
else:
rollback

方案二:

1
2
3
4
5
6
start transaction:
int affectedRows = update t_inventory set num=num-3 where id=234 and num>=3
if affectedRows > 0:
commit
else:
rollback

延时图如下:

从上图可以看出,加了update谓词以后,一个事务少了1rt的锁记录时间(update谓词和select for update对记录加的都是X锁,所以效果是一样的)。

Centralized 2PL

Centralized 2PL 是一种将锁的管理责任只委托给一个节点的方法, 也叫做 Primary 2PL 算法.

首先我们要了解一下 Centralized 2PL中的一些名词:

  • Coordinating TM (Transaction Manager)
  • Participating sites : 那些要进行数据库操作的地方
  • LM (Lock Manager)
流程示意图

  • 首先,Coordinating TM 代理站点向 Centralized LM发送请求加个锁
  • LM 同意TM的请求,帮其加锁
  • TM告诉目标站点:帮你请求到锁了,你可以进行数据库操作了
  • 站点结束数据操作之后,告诉TM
  • TM向LM汇报:操作完成了,现在你可以释放锁了

Centrailized 2PL 主要有以下特点:

  • 有一个维护所有锁信息的单一节点
  • 一个锁管理器(LM)适用于整个DDBMS。
  • 参与全局事务的Local transaction managers(TM) 从 LM中请求和释放锁。或者Transaction Coordinator(可以理解为总代理) 可以代表Participating Sites提出所有的锁请求。
  • 优点:容易发现死锁
  • 劣势:可扩展性差

Distributed 2PL

Distributed 2PL 和 Centralized 2PL 不同,它在每个站点都有一个LM,同时,只有一个TM。它们的通讯逻辑也发生了变化

由于现在每个站点都有一个 LM,因此站点中的LM不再和数据操作隔离,可以直接对本站点的数据库进行操作。

它们之间的交互逻辑如下:

  • TM 告诉 某一个站点的LM: 哥,你们数据库中有个操作,请你加个锁呗
  • LM 回答:得嘞,没问题,这就给他加,就不麻烦你了哈
  • 当前站点的数据库操作完成后,比较害羞,对TM说:哥哥,我好了,你和LM说让他把锁松开
  • TM然后再对 LM说,差不多得了,可以释放锁了

分布式二阶段锁的特点是:

  • 每个站点都有一个LM。因此每一个站点都有一个 时序表(scheduler)
  • 每个LM处理该站点的数据的锁请求。
  • 并发控制是通过 Participating sites 的 LMs 的合作 来完成的
  • 优点:更好的扩展性
  • 缺点:难以探测到死锁的发生

原子性和持久性

分布式数据库事务的原子性和持久性是通过多阶段提交来实现的。

https://blog.csdn.net/skyie53101517/article/details/80741868

2PC

二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol)。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者(coordinator)的组件来统一掌控所有节点(称作参与者(participant))的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

准备阶段 voting phase

事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。

可以进一步将准备阶段分为以下三个步骤:

1)协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。

2)参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)

3)各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。

提交阶段 decision phase

如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)

接下来分两种情况分别讨论提交阶段的过程。

  • 当协调者节点从所有参与者节点获得的相应消息都为”同意”时:

1)协调者节点向所有参与者节点发出”正式提交(commit)”的请求。

2)参与者节点正式完成操作,并释放在整个事务期间内占用的资源。

3)参与者节点向协调者节点发送”完成”消息。

4)协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。

如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

1)协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。

2)参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。

3)参与者节点向协调者节点发送”回滚完成”消息。

4)协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。

不管最后结果如何,第二阶段都会结束当前事务。

二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点的:

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

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

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

4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

由于二阶段提交存在着诸如同步阻塞、单点问题、脑裂等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。

3PC

三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本。

与两阶段提交不同的是,三阶段提交有两个改动点。

  1. 引入超时机制。同时在协调者和参与者中都引入超时机制。

  2. 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交有 CanCommitPreCommitDoCommit三个阶段。

CanCommit阶段

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

1.事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。

2.响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

优点:不像2pc 第一个阶段就开始锁表,3pc的阶段一是为了先排除个别参与者不具备提交事务能力的前提下,而避免锁表。

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以执行事务的PreCommit操作。根据响应情况,有以下两种可能。

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

1.发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。

2.事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。

3.响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应; 或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

1.发送中断请求 协调者向所有参与者发送abort请求。

2.中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

执行提交

1.发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。

2.事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。

3.响应反馈 事务提交完之后,向协调者发送Ack响应。

4.完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

1.发送中断请求 协调者向所有参与者发送abort请求

2.事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。

3.反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息

4.中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

2PC与3PC的区别

相对于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算法的不完整版

总结

-------------本文结束,感谢您的阅读-------------