Mysql

数据库事务的ACID特性与事务隔离级别小结

2020-02-04  本文已影响0人  LittleMagic

前言

今天是本博客开博一周年,一年来写了将近30万字,收获颇丰。但是回头翻看,发现之前写的“漂在半空中”的东西居多。所以最近几天打算搞些平时不会经常注意到,但是又非常基础、重要的知识点,权当复习和查漏补缺,以及训练一下自己“说人话”的能力。

本文只总结概念和理论层面的东西,数据库事务的具体实现细节会以MySQL为例另写文章来说(flag立好了_(:з」∠)_

数据库事务与ACID特性

在数据库系统中,一个事务(transaction)是指由一系列数据库操作组成的一个完整的逻辑过程。考虑最经典的“银行转账”情景,从某个客户的储蓄账户转账1000元到理财账户,可以对应以下的事务描述:

start transaction;
select balance from saving_account where customer_id = 3897;
update saving_account set balance = balance - 1000.00 where customer_id = 3897;
update finance_account set balance = balance + 1000.00 where customer_id = 3897;
commit;

早在1983年的论文《Principles of transaction-oriented database recovery》中,就提出了数据库事务必须具备的四大特性是:原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability),合称为ACID。要保证事务的执行是正确而可靠的,就必须满足ACID。下面我们不按A→C→I→D的顺序来,而按更加易于理解的A→D→I→C顺序进行解说。

原子性

一个事务必须被视为一个内部不可分的工作单元,以确保该事务绝对不会被部分执行——即要么完全执行,要么根本不执行。当事务中有操作失败时,所有操作都将回滚,保证数据库回到事务前的状态。原子性可以说是事务最基本的保障,如果没有原子性,假设上文的SQL语句第4行失败,但事务不回滚,仍然提交的话,客户的储蓄账户就会白白损失1000元。

MySQL InnoDB存储引擎使用undo log(回滚日志)机制实现原子性。undo log是逻辑日志,记录了各个行在已提交事务修改前的数据,当事务失败或主动执行rollback语句时,根据undo log的数据恢复事务前的现场。undo log以及下一节提到的redo log统称为MySQL的事务日志,关于它们两个之后再细说。

持久性

一旦事务提交,事务所做的数据改变将是永久的。这意味着数据改变已经被记录,即使系统崩溃,数据也不会因此而丢失(客户的钱都还在)。直白地说,我们总是通过非易失性存储设备(HDD、SSD)来存储数据,只要能够排除存储介质本身的问题,就可以尽量保证物理上的持久性。

但是在实际的DBMS设计中,持久性还要多注意一点,就是缓存的持久化。为了提升性能,数据的更改往往不是实时落盘的,而是先写缓存(如MySQL的buffer pool)再后台同步。如果同步未完成而服务器宕机,数据就丢失了。InnoDB使用redo log(重做日志)机制进一步保障持久性。redo log是物理日志,记录了各个页在已提交事务修改后的数据,系统重启时可根据redo log将那些没来得及落盘的数据写入。

隔离性

某个事务的结果只有在完成之后才能对其他事务可见,也即一个事务不应该影响其它事务运行效果。由于事务是可以并发执行的,如果这些事务查询或修改的是相关联的表和数据,就有可能会相互影响,导致不正确的结果。所以每个事务都有各自的完整的数据空间,本事务所做的修改与其他事务所做的修改要隔离。在查看特定数据的更新时,要么看到另一事务修改之前的状态,要么看到修改之后的状态,不会看到中间状态。

仍然以上文转账事务为例,当DBMS执行完第3条SQL,还未执行第4条时,如果又有一个账户统计事务开始运行,那么它会认为转账还未进行,即那1000元还在储蓄账户里。如果不隔离的话,统计事务在一瞬间就会看到中间状态,即1000元凭空消失了。

上面的原子性和持久性都是针对单个事务的,而隔离性是针对多个事务的,所以更复杂些。在DBMS中实现隔离性实际上就是实现并发控制(concurrency control)机制,MySQL InnoDB使用了锁和MVCC(多版本并发控制)来保证隔离性,这两点之后再说。

容易想到,如果一刀切地保证事务完全隔离,那么DBMS在同一时间就只能执行一个事务,效率大大降低。所以在实际操作中会将隔离性划分级别,本文稍后会总结。

一致性

数据库总是从一种一致性状态转换到另一种一致性状态,也即事务开始前和结束后,数据库的完整性约束没有被破坏。这包含两个层面。

之所以把一致性放在最后,是因为原子性、持久性和隔离性的设计最终都是为了保证一致性而存在的。

小问题:ACID特性中的一致性和CAP理论中的一致性有关联吗?答案是没有,它俩并不是同一个概念。前者是针对(单机)数据库事务的,是内部一致性。后者是针对分布式系统的,是外部一致性。以CAP/BASE理论为代表的分布式基础对我们大数据工作者来说似乎更重要,最近会总结出来。

事务的隔离级别与读现象

在ANSI/ISO SQL 92标准中,定义了4种事务隔离级别,从低到高分别是:未提交读(READ UNCOMMITTED)、提交读(READ COMMITTED)、可重复读(REPEATABLE READ)、可串行化(SERIALIZABLE)。下面分别解释它们,并顺便引出各个隔离级别可能会出现的其他3种读现象,即某事务读取其他事务可能修改的数据的现象。

未提交读

最低的隔离级别,所有事务都可以看到其他未提交事务的执行结果。为了简单,考虑纯粹用锁实现隔离性的DBMS,未提交读级别的事务在读数据时不会加锁,在写数据时只会加行级共享锁。

该级别只比完全不做隔离好一点点,几乎不用在实际应用中。而读取到未提交的数据,就称为脏读(dirty read)现象。为了解释它,假设有以下原始数据表。

然后两个事务并发执行如下图所示的逻辑。

可见,事务2将id = 1的行的age列修改为21,但没有提交,造成事务1查询到的id = 1的行的age也是21,造成了脏读。事务2回滚后,事务1查询到的数据就是错的了。

小问题:脏读现象虽然多数情况下不符合预期,但还是允许存在的,那么脏写(dirty write)允许存在吗?答案是不行的,如果一个事务的修改在未提交时就被另一个事务的修改覆盖,原子性就被打破了。

提交读

这是大多数DBMS(除MySQL外)的默认事务隔离级别。该级别其实就是符合隔离性基本定义的实现:一个事务在开始时只能看到其他已提交的事务所做的改变;一个事务从开始到提交前,所做的数据更改都对其他事务不可见。仍然以锁机制实现隔离性为例,该级别的事务在读数据时会加行级共享锁,读完立即释放;写数据时会加行级排他锁,直到事务结束释放。

在该级别下,脏读现象不会出现,但是需要注意不可重复读(non-repeatable read)现象。如下图示例。

事务1第一次select和第二次select读到的id = 1行的age列的值是不同的(因为事务2中途提交了)。这种同一事务中,对同一行数据获取多次,返回不同结果的现象就是不可重复读。

可重复读

这是MySQL的默认事务隔离级别。既然提交读级别会造成不可重复读的问题,那么在提交读的基础上解决该问题的隔离级别自然就叫可重复读了。仍然以锁机制实现隔离性为例,该级别的事务在读数据时会加行级共享锁,写数据时会加行级排他锁,两个锁都是直到事务结束才释放。这样,在事务1读某行数据到事务1结束的整个过程中,事务2肯定无法修改该行数据,得到的结果总是一样的,problem solved。

但是,该级别仍然无法解决最高级的一种读现象,即幻读(phantom read)。如下图示例。

事务1执行了一个范围查询,第一次执行时返回2条记录。事务2向表插入了一条新记录并直接提交,导致事务1重新执行该范围查询时,查到了事务2插入的那条记录,并返回3条记录。可见,幻读实际上是不可重复读在范围查询时的一种特殊情况。

可串行化

最高的隔离级别。该级别的事务在读数据时会加行级共享锁,对范围查询则会特别加上范围锁,写数据时会加行级排他锁,并且都是直到事务结束才释放,这样就连幻读都不会出现了。如果要保证绝对安全,即事务都是严格顺序执行的,还可以将行级锁改为表级锁。但同样地,这个级别的并发性是最低的,只有在强制要求数据稳定性时,才会选用它。

The End

强烈推荐《高性能MySQL》(High Performance MySQL)一书,嗯嗯。

民那晚安。祝身体健康,百毒不侵。希望疫情快点过去。

上一篇下一篇

猜你喜欢

热点阅读