searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

ACID事务与MVCC简介

2023-05-26 05:51:33
18
0

1  事务基础知识

1.1  ACID事务

事务是访问数据库的一个操作序列。事务的正确执行使得数据库从一种状态转换为另一种状态。这里的“数据库”是广义上的数据服务,包括RDBMS关系型数据库、NoSQL存储(如KV存储),也包含分布式一致性协调组件等数据服务。

事务有4个基本原则,ACID:

  • 原子性(Atomicity):事务要么全部被执行,要么全部不执行。如果事务的所有子事务全部提交成功,则所有的数据操作被提交,数据库状态发生变化;如果有子事务失败,则其他子事务的数据操作被回滚,即数据库回到事务执行前的状态,不会发生状态转换;
  • 一致性(Consistency):事务的执行使得数据库从一种正确状态转换成另外一种正确状态;
  • 隔离性(Isolation):并发执行的多个事务,相互之间互不影响。在事务正确提交之前,不允许把事务对该数据的改变提供给任何其他事务,即在事务正确提交之前,它可能的结果不应该显示给其他事务。
  • 持久性(Durability):事务正确提交之后,其结果将永远保存在数据库之中,即使在事务提交之后有了其他故障,事务的处理结果也会得到保存。

1.2  事务并发问题

事务在并发处理时可能面临以下5类问题,顺便我们也看看事务操作的实际例子:

  • 第一类丢失更新(Lost Update):回滚丢失。即A事务已提交的更新,被B事务的回滚操作给一同重置到了初始版本。

时间

事务A:取款

事务B:转账

T1

开始事务

 

T2

 

开始事务

T3

查询账户余额=1000

 

T4

 

查询账户余额=1000

T5

 

汇入100,余额=1100

T6

 

提交事务

T7

取出100,余额=900

 

T8

撤销事务

 

T9

余额恢复为1000(丢失更新1)

 

 

  • 第二类丢失更新(Second Lost Update):覆盖丢失。即A事务已提交的更新,被B事务的提交覆盖为错误值。

时间

事务A:取款

事务B:转账

T1

开始事务

 

T2

 

开始事务

T3

查询账户余额=1000

 

T4

 

查询账户余额=1000

T5

取出100,余额=900

 

T6

提交事务

 

T7

 

汇入100,余额=1100

T8

 

提交事务

T9

 

余额修改为1100(丢失更新2)

 

  • 脏读(Dirty Read):读到了其他事务还未提交的脏数据,其他事务可能回滚,导致本事务基于无效的值进行操作。

时间

事务A:取款

事务B:转账

T1

 

开始事务

T2

开始事务

 

T3

 

查询账户余额=1000

T4

 

汇入100,余额=1100

T5

查询账户余额=1100(脏读)

 

T6

 

撤销事务

T7

取出100,余额=1000

 

T8

提交事务

 

T9

余额修改为1000

 

 

  • 不可重复读(Nonrepeatable Read):在事务中两次读同一个数据,值不相等。

时间

事务A:取款

事务B:转账

T1

开始事务

 

T2

 

开始事务

T3

查询账户余额=1000

 

T4

账户余额充足,可以取款

查询账户余额=1000

T5

 

转出900,余额=100

T6

尝试取款500,再次查询余额=100,取款失败(不可重复读)

 

T7

撤销事务

 

T8

 

提交事务

T9

 

余额修改为100

 

  • 幻读(Phantom Read):在事务中两次读同一数据集,得到的结果不同,多了或少了数据。

时间

事务A:后台管理

事务B:柜台开户

T1

开始事务

 

T2

 

开始事务

T3

累加所有账户总金额SUM(M)

 

T4

 

新开账户

T5

获取所有账户总数M+1(幻读)

……

T6

计算账户平均存款额SUM(M)/(M+1),幻读造成分母错误。

 

T7

……

 

 

1.3  事务隔离级别

在数据库事务理论中,有以下4个等级的事务隔离级别,强度逐级递增,性能开销也越来越大:

  • 读未提交RU(Read Uncommitted):能够读取到没有被提交的数据,最低级别,相当于没有隔离。所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种。
  • 读已提交RC(Read Committed):能够读到那些已经提交的数据。能避免脏读,但无法解决幻读、不可重复读。
  • 可重复读RR(Repeatable Read):事务内两次读相同数据,保证读到相同的值。可防止脏读、不可重复读,无法防止幻读。
  • 串行化(Serializable):最高级别的事务隔离。完全解决所有事务并发问题。

不同事务隔离级别所能解决的问题见下表:

表 3 不同事务隔离级别解决的并发问题

隔离级别

脏读

不可重复读

幻读

丢失更新1

丢失更新2

读未提交RU

未解决

未解决

未解决

解决

未解决

读已提交RC

解决

未解决

未解决

解决

未解决

可重复读RR

解决

解决

未解决

解决

解决

串行化Serializable

解决

解决

解决

解决

解决

 

1.4  事务隔离的实现

实现事务隔离的一般机制就是加锁。注意这里的锁并不是单纯解决线程安全问题的线程互斥锁——我们认为最基础的线程安全问题,在数据库设计时已经充分考虑了。例如的每个Put/Get操作,必定是线程安全的——本章节中的所有锁,都特指确保事务原子性的数据锁。

  • 悲观锁:认为所有数据都可能被其他事务修改,所有需要读写的地方都主动加锁。
  • 乐观锁:假设数据很少冲突,所以只在更新提交前检查数据是否已被事先修改,类似于CAS机制。
  • MVCC多版本并发控制:引入版本控制机制,每个事务的写都分配一个独立revision,读也只允许读到已提交的revision。MVCC并发的多个读和一个写都不需要加锁,并发性能优秀,也不会有脏读、不可重复读、幻读。但MVCC不能解决第二类丢失更新问题,仍然需要配合锁(悲观锁或乐观锁)解决写-写冲突。

1.5  MVCC多版本并发控制

MVCC引入了版本控制机制,通过并行的多个版本,来实现并发事务隔离。MVCC在MySQL中有成熟的工程化应用实例,网上大多以MySQL的MVCC实现机制为例来介绍。而MySQL作为RDBMS数据库,事务实现细节较为繁琐。这里给出一个KV数据库可用的简化描述版本。

MVCC可通过revision标识每次数据库的修改,每当数据库产生一次修改记录,revision加1。

查询数据库时指定revision,可找到这个revision时刻所修改的数据,以及没修改到的上一版本所有数据,revision所对应的是这两套数据的集合。一个revision所代表的是这次修改后整个数据库的所有数据集合,是数据库某次修改的全量快照。

这样说可能不太好理解,考虑git的某次commit实际只修改了几个文件中的几行代码,但checkout指定的commit id,检出的不仅仅是这几行代码,而是这次commit修改后的全量数据。

因此具备MVCC机制的数据库,每次修改不会覆盖之前的数据,而是产生增量的数据拷贝,形成一份只读快照。已提交的revision,其数据不会再被修改。对已提交的revision做只读操作,可以不用加锁(特指针对事务原子性的数据锁)。

基于MVCC的事务,在开始事务时会得到两个revision:一个是用于只读请求的readRev,必定是已提交的revision;另一个writeRev则专为当前事务分配,所有写入都保存于此revision,并在commit时一并提交。

MVCC机制的示意图如下:

图 7 MVCC多版本并发控制

可以看到在MVCC中,读写是分开的,每个事务的写操作也完全分离,因此不需要借助额外的锁即可达到事务隔离的效果,具备极高的并发性能。但并发的多个写事务,仍然会有第二类丢失更新现象,最简单的做法是使用锁确保同时只有一个写事务。

在MVCC的实现中,事务可以读哪个revision版本的数据,由ReadView控制;事务可以写进哪个revision,由WriteView控制。

0条评论
0 / 1000
王****珺
3文章数
0粉丝数
王****珺
3 文章 | 0 粉丝
王****珺
3文章数
0粉丝数
王****珺
3 文章 | 0 粉丝
原创

ACID事务与MVCC简介

2023-05-26 05:51:33
18
0

1  事务基础知识

1.1  ACID事务

事务是访问数据库的一个操作序列。事务的正确执行使得数据库从一种状态转换为另一种状态。这里的“数据库”是广义上的数据服务,包括RDBMS关系型数据库、NoSQL存储(如KV存储),也包含分布式一致性协调组件等数据服务。

事务有4个基本原则,ACID:

  • 原子性(Atomicity):事务要么全部被执行,要么全部不执行。如果事务的所有子事务全部提交成功,则所有的数据操作被提交,数据库状态发生变化;如果有子事务失败,则其他子事务的数据操作被回滚,即数据库回到事务执行前的状态,不会发生状态转换;
  • 一致性(Consistency):事务的执行使得数据库从一种正确状态转换成另外一种正确状态;
  • 隔离性(Isolation):并发执行的多个事务,相互之间互不影响。在事务正确提交之前,不允许把事务对该数据的改变提供给任何其他事务,即在事务正确提交之前,它可能的结果不应该显示给其他事务。
  • 持久性(Durability):事务正确提交之后,其结果将永远保存在数据库之中,即使在事务提交之后有了其他故障,事务的处理结果也会得到保存。

1.2  事务并发问题

事务在并发处理时可能面临以下5类问题,顺便我们也看看事务操作的实际例子:

  • 第一类丢失更新(Lost Update):回滚丢失。即A事务已提交的更新,被B事务的回滚操作给一同重置到了初始版本。

时间

事务A:取款

事务B:转账

T1

开始事务

 

T2

 

开始事务

T3

查询账户余额=1000

 

T4

 

查询账户余额=1000

T5

 

汇入100,余额=1100

T6

 

提交事务

T7

取出100,余额=900

 

T8

撤销事务

 

T9

余额恢复为1000(丢失更新1)

 

 

  • 第二类丢失更新(Second Lost Update):覆盖丢失。即A事务已提交的更新,被B事务的提交覆盖为错误值。

时间

事务A:取款

事务B:转账

T1

开始事务

 

T2

 

开始事务

T3

查询账户余额=1000

 

T4

 

查询账户余额=1000

T5

取出100,余额=900

 

T6

提交事务

 

T7

 

汇入100,余额=1100

T8

 

提交事务

T9

 

余额修改为1100(丢失更新2)

 

  • 脏读(Dirty Read):读到了其他事务还未提交的脏数据,其他事务可能回滚,导致本事务基于无效的值进行操作。

时间

事务A:取款

事务B:转账

T1

 

开始事务

T2

开始事务

 

T3

 

查询账户余额=1000

T4

 

汇入100,余额=1100

T5

查询账户余额=1100(脏读)

 

T6

 

撤销事务

T7

取出100,余额=1000

 

T8

提交事务

 

T9

余额修改为1000

 

 

  • 不可重复读(Nonrepeatable Read):在事务中两次读同一个数据,值不相等。

时间

事务A:取款

事务B:转账

T1

开始事务

 

T2

 

开始事务

T3

查询账户余额=1000

 

T4

账户余额充足,可以取款

查询账户余额=1000

T5

 

转出900,余额=100

T6

尝试取款500,再次查询余额=100,取款失败(不可重复读)

 

T7

撤销事务

 

T8

 

提交事务

T9

 

余额修改为100

 

  • 幻读(Phantom Read):在事务中两次读同一数据集,得到的结果不同,多了或少了数据。

时间

事务A:后台管理

事务B:柜台开户

T1

开始事务

 

T2

 

开始事务

T3

累加所有账户总金额SUM(M)

 

T4

 

新开账户

T5

获取所有账户总数M+1(幻读)

……

T6

计算账户平均存款额SUM(M)/(M+1),幻读造成分母错误。

 

T7

……

 

 

1.3  事务隔离级别

在数据库事务理论中,有以下4个等级的事务隔离级别,强度逐级递增,性能开销也越来越大:

  • 读未提交RU(Read Uncommitted):能够读取到没有被提交的数据,最低级别,相当于没有隔离。所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种。
  • 读已提交RC(Read Committed):能够读到那些已经提交的数据。能避免脏读,但无法解决幻读、不可重复读。
  • 可重复读RR(Repeatable Read):事务内两次读相同数据,保证读到相同的值。可防止脏读、不可重复读,无法防止幻读。
  • 串行化(Serializable):最高级别的事务隔离。完全解决所有事务并发问题。

不同事务隔离级别所能解决的问题见下表:

表 3 不同事务隔离级别解决的并发问题

隔离级别

脏读

不可重复读

幻读

丢失更新1

丢失更新2

读未提交RU

未解决

未解决

未解决

解决

未解决

读已提交RC

解决

未解决

未解决

解决

未解决

可重复读RR

解决

解决

未解决

解决

解决

串行化Serializable

解决

解决

解决

解决

解决

 

1.4  事务隔离的实现

实现事务隔离的一般机制就是加锁。注意这里的锁并不是单纯解决线程安全问题的线程互斥锁——我们认为最基础的线程安全问题,在数据库设计时已经充分考虑了。例如的每个Put/Get操作,必定是线程安全的——本章节中的所有锁,都特指确保事务原子性的数据锁。

  • 悲观锁:认为所有数据都可能被其他事务修改,所有需要读写的地方都主动加锁。
  • 乐观锁:假设数据很少冲突,所以只在更新提交前检查数据是否已被事先修改,类似于CAS机制。
  • MVCC多版本并发控制:引入版本控制机制,每个事务的写都分配一个独立revision,读也只允许读到已提交的revision。MVCC并发的多个读和一个写都不需要加锁,并发性能优秀,也不会有脏读、不可重复读、幻读。但MVCC不能解决第二类丢失更新问题,仍然需要配合锁(悲观锁或乐观锁)解决写-写冲突。

1.5  MVCC多版本并发控制

MVCC引入了版本控制机制,通过并行的多个版本,来实现并发事务隔离。MVCC在MySQL中有成熟的工程化应用实例,网上大多以MySQL的MVCC实现机制为例来介绍。而MySQL作为RDBMS数据库,事务实现细节较为繁琐。这里给出一个KV数据库可用的简化描述版本。

MVCC可通过revision标识每次数据库的修改,每当数据库产生一次修改记录,revision加1。

查询数据库时指定revision,可找到这个revision时刻所修改的数据,以及没修改到的上一版本所有数据,revision所对应的是这两套数据的集合。一个revision所代表的是这次修改后整个数据库的所有数据集合,是数据库某次修改的全量快照。

这样说可能不太好理解,考虑git的某次commit实际只修改了几个文件中的几行代码,但checkout指定的commit id,检出的不仅仅是这几行代码,而是这次commit修改后的全量数据。

因此具备MVCC机制的数据库,每次修改不会覆盖之前的数据,而是产生增量的数据拷贝,形成一份只读快照。已提交的revision,其数据不会再被修改。对已提交的revision做只读操作,可以不用加锁(特指针对事务原子性的数据锁)。

基于MVCC的事务,在开始事务时会得到两个revision:一个是用于只读请求的readRev,必定是已提交的revision;另一个writeRev则专为当前事务分配,所有写入都保存于此revision,并在commit时一并提交。

MVCC机制的示意图如下:

图 7 MVCC多版本并发控制

可以看到在MVCC中,读写是分开的,每个事务的写操作也完全分离,因此不需要借助额外的锁即可达到事务隔离的效果,具备极高的并发性能。但并发的多个写事务,仍然会有第二类丢失更新现象,最简单的做法是使用锁确保同时只有一个写事务。

在MVCC的实现中,事务可以读哪个revision版本的数据,由ReadView控制;事务可以写进哪个revision,由WriteView控制。

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0