吹雨听风

说透 SQL 事务四大特性

标签: MySQL , 数据库

提到数据库事务,相信大家都不陌生,也一定能不加思索的说出 ACID 四大特性:

  • 原子性 Atomicity
    一个事务中的对数据库的所有操作都是一个不可分割的工作单元,这些操作作为一个整体,要么全部执行,要么什么都不做(要么全部成功,要么全部失败)。
  • 一致性 Consistency
    一个事务独立执行的结果,应该保持数据库的一致性,即数据不会因为事务的执行而遭到破坏。
  • 隔离性 Isolation
    在多个事务并发执行时,数据库系统应保证与这些事务先后单独执行时的结果一样,称为事务达到了隔离性的要求。也就是在多个事务并发执行时,保证执行的结果是正确的,就如同单用户执行单个事务一样。
  • 持久性 Durability
    事个事务一旦完成全部操作后,它对数据库的所有更新应永久地反映在数据库中,不会丢失,即使以后系统发生故障也是如此。

那,除此以外呢?这四大特性是如何保证的、隔离性的是如何分级与实现的等等。如果你也类似的疑问,那么可以往下阅读,本文接下来还将详细探讨事务四大特性背后的原理与细节。

数据、数据库与数据库管理系统

在今天,数据库已经是各系统中不可或缺的基础设施。在日常中,我们常说的数据库具有更宽泛的含义,其通常是指数据库管理系统(DBMS, database management system),如 MySQL、PostgreSQL 等。

what is mysql?

MySQL :: MySQL 8.0 Reference Manual :: 1.2.1 What is MySQL?

那么 数据库管理系统(DBMS)、 数据库系统(DBS)、数据库(DB)这么些个术语有区别吗?是指的同一个东西吗?当然,其实它们之间是有区别的,定义分别如下:

  • 数据(data):指的是保存是数据库软件中的信息(有时也称为“记录”)。

  • 数据库(Database):也称为 DB,是长期存储在计算机内、有组织的、统一管理的相关数据的集合。DB且有如下的特点:

    • 共享性,能在多用户之间进行共享。
    • 低冗余,数据间具有较低的冗余度。
    • 相关性,数据和数据之间联系紧密。
    • 独立性,且有较高的数据独立性。
  • 数据库系统(Database System):也称为 DBS。DBS 是实现有组地、动态地存储大量关联数据的、方便多用户访问的硬件、软件、数据资源共同组成的系统。

  • 数据库管理系统(Database Management System):也称为 DBMS。它是位于用户与操作系统之间的一层数据管理软件,它为用户或应用程序提供访问 DB 的方法,包括 DB 的建立、查询、更新及各种数据据的控制和管理。DBMS 根据管理的数据之间的联系方式不同又分为多种类型:

    • 层次型,按记录来存取数据。
    • 网状型,采用网状原理和方法,以网状数据模型为基础建立的数据库。记录类型为结点的网络结构,即一个结点可以有一个或多个下级结点,也可以有一个或多个上级结点,两个结点之间甚至可以有多种联系。代表:DBTG。
    • 关系型(RDBMS),采用了关系模型来组织数据的数据库,其以行和列的形式存储数据,以便于用户理解。代表:MySQL、 MariaDB、SQL Server 等。
    • 面向对象型(OODBMS)。如:Versant Object Database 等。

    目前比较用的是 RDBMS 与 OODBMS。至于它们二者的详细对比,大家阅读这篇文章:# Difference between RDBMS and OODBMS

我们最为常见的 MySQL 它就是 DBMS,其中包含了“数据库”。这里画了两张图,希望能帮助大家更好的理解前面的几个概念。

DBMS与DB的联系-1

图1:可以看到DBMS为于用户和数据库之间,为用户和程序提供访问与管理数据的途径。

DBMS与DB的联系-db_datum

图2:数据库与数据之间的联系。

DBMS与DB的联系-all

图3:各术语之间的层次图。

到这里,相信大家对于前面的几个概念有了一定的了解。花这么大篇幅介绍这几个概念,那想必和事务有一些联系吧?没错!

*请注意:下文出现的 DBMS 如未特别说明均不特指任一具体的数据库管理系统

事务

从用户的视角来看,一个事务中的对数据库的一组操作应该是一个独立的单元、不能分割的整体。无论发生什么情况,都必须确保要么完整执行,要么完全不执行。

在 SQL 定义中,一个事务对应的一组操作序列,由 BEGIN TRANSACTION 语句开始,以 COMMIT 或 ROLLBACK 语句结束。

COMMIT 语句表示事务成功执行并结束,告诉系统此事务对数据的所有修改都要写入磁盘并开始进入到一个新的正确状态。

ROLLBACK 语句则正好相反,表示事务未成功执行,并应该回退。此时告诉系统,此事务对数据库的所有修改都必须被撤销,让数据库回到事务开始时的初始状态。

事务是数据库系统(DBS)运行的最小逻辑工作单元。前面已提到事务的四大特性,这四大特性其实是由 DBMS 的四大子系统统来分别保证的,它们是:事务管理子系统、完整性子系统、并发控制子系统、恢复管理子系统。

DBMS与DB的联系-Page-4

原子性

原子性可以说是数据库系统本身最基本的职责,它具体是由事务管理子系统结果恢复管理子系统来实现。

在 MySQL 的实现中,InnoDB 引擎对事务过程中的数据变更总是维持了 UNDO LOG,若要回滚事务,能够通过 REDO 追溯最老版本的方式,将数据全部回滚回来。若用户需要提交事务,则将 REDO LOG 的缓冲区刷新到磁盘。

REDO 与 UNDO 日志,除了用来保证原子性的实现,还用于保证持久性。

一致性

DBMS 的“完整性子系统” 执行测试任务以保证数据库中数据是正确的,该检查也被称为“完整性检查”。所谓的完整性,是指数据库中数据的正确性、有效性、相容性,防止错误的数据进入。

完整性子系统主要有2大功能:

  • 监督事务的执行,并测试其是否违反完整性规则
  • 或有违反,则采取操作

而 完整性规则 由 DBA 或程序开发人员配置。主要有域约束、表约束、断言三大类。

在 MySQL InnoDB 引擎中,可以通过在创建数据表时定义 Primary Key 和 Unique Key 来定义基本表约束。此外还可以通过编写触发器(断言)的方式来进一步完善约束。通常可选的定义约束的途径有:

  • 选择合适的字段类型(类型约束)
  • 定义 Primary Key
  • 定义 Foreign Key
  • 定就 Unique Key
  • 编写触发器
  • 定义 Default
  • 定义 Not Null

上面说的 Primary Key、Foreign Key、Unique Key 不是在创建索引么。没错,确实是索引。但是在创建上述索引的时候会同时创建对应的约束,索引是一个数据结束,而约束是用来保证数据的完整性。

可以通过 select constraint_name, constraint_type from table_name 语句来查看数据表约束。

对于 MySQL 约束的文档可以查看: MySQL :: MySQL 8.0 Reference Manual :: 13.1.20.6 CHECK Constraints

隔离性

所谓“隔离”,就是让同时执行的多个事务保持各自的独立性,避免相互影响。

同时执行(并发)多个事务会带来三个问题:更新丢失、读到脏数据、不可重复读。如果系统不加以控制,那么数据库的完整性可能会遭到破坏。DBMS 的并发控制子系统正是用来调度多事务的执行并保证隔离性。

更新丢失

先看一个并发事务模拟:

时间 事务T1 数据库中 A 的值 事务T2
t0 100
t1 Read A
t2 Read A
t3 A = A - 30
t4 A = A * 2
t5 Update A
t6 70
t7 Update A
t8 200

在上面的事务中,如果按次序执行,最后 A 的值是 200。这肯定不是正确的, T1 对 A 的更新操作丢失了。

读到脏数据

什么样的数据是脏数据呢?指的是事务中未提交的随后被撤销的数据。我们再看一个并发事务模拟:

时间 事务T1 数据库中 A 的值 事务T2
t0 100
t1 Read A
t2 A = A - 30
t3 Update A
t4 70 Read A
t5 ROLLBACK
t6 100

在上面的事务中,在 t4 时该,T1 事务把 A 的值修改为了 70,但尚未提交(COMMIT),此时并行的 T2 事务读取到了 A 的值(70),但 T1 事务随后执行了 ROLLBACK 操作,而 T2 事务还在使用 70 这个值。这也显然是不正确的(脏数据)。

不可重复读

不可重复读指的是,在一个事务中,对同一个数据项的 2次(或多次)读操作,读取到的值不一致的现象。还是先看一个并发事务模拟:

时间 事务T1 数据库中 A 的值 事务T2
t0 100
t1 Read A
t2 Read A
t3 A = A * 2
t4 UPDATE A
t5 200 COMMIT
t6 Read A

在上面的表中,T1 事务中两次读 A 数据,读到的值却是不一样的,这也显然是不正确的。

那么并发控制子系统到底是如何来解决上述的几个问题的呢?– 锁技术。

锁是一个与数据有关的变量,对应用于该数据的操作页言,锁描述的是该数据的状态。

在数据库中,锁主要有两种:排他锁与共享锁。

排他锁

排他锁也称为独占锁、写锁,是数据库中最为常用的一种锁,也叫 X 锁。

如果一个事务对一个数据项成功的实现了 X 锁,那么在该 X锁释放之前,数据库不允许其他事务对数据项加任务类型的锁。

X锁的操作有两个:

  1. 申请 X 锁。若事务对数据项申请加X锁成功,则可以读、写该数据项。若失败,事务会进行等待队列,直到成功申请到X锁事务才能继续。
  2. 释放 X 锁。数据库系统中并没有提供解除X锁的操作,而是在 ROLLBACK、COMMIT 操作时,自动的释放 X 锁。原因是,如果过早的解除了X锁,那么其他事务依然会读取到未提交的数据。

共享锁

由于 X 锁的并发度太低,一次只能有一个事务获取 X 锁,其他事务即使只需要读数据,也只能在队列等候。为了解决这个问题,就引入了共享锁。也就 S 锁。

事务对数据项成功加上 S 锁后,系统依然允许其他事务对该数据项加S锁,但是该数据项所有的S锁接触之前,不允许任何事务对该数据项加上 X 锁。

两种锁的兼容性:

| | X 锁 | S 锁 |
|: — :|: — :|: — :|
| X 锁 | 不兼容 | 不兼容 |
| S 锁 | 不兼容 | 兼容 |

如果并行事务向同一数据项申请了不相容的锁,那么后提出的事务必须在队列中等待。

使用 S 锁有三个操作:

  1. 申请 S 锁操作。事务如果对数据项成功加上 S 锁,则可以读取数据,但不能写数据。如是加上 S 锁不成功,那么这个事务将进入对队排队,一直到成功加上 S 锁为止。
  2. 升级和写操作。事务如果要对数据项进行写操作,那么需要先将 S 锁升级为 X 锁。如果升级成功可以直接写数据,若失败,事务将入进入队列待待。
  3. 释放 S 锁操作。事务解除对数据项的 S 锁。

从上面的操作步骤不难看到,S 锁只能读,不能写。若需要写,则必须先升级为 X 锁。同时由于 S 锁只能读,因此事务可以随时释放 S 锁。

上面 2 种锁,加锁的对象可以很大,也可以很小。拿 MySQL 来说,锁可以加在表上,数据记录行上,也可以加在某一个属性上。锁对象的大小称之为锁的粒度。

加锁虽然能解决事务并行的问题,但也会带来新的问题。如加锁会增加系统的开销,给数据库系统带来带来性能压力,因此在实际使用中,需要结合具体的业务来决定加锁的粒度。如果一个事务需要更新大量的数据,可以考虑把锁加在表上,反之则应该主加锁的粒度尽可能的小。

意向锁

在 MySQL 的 InnoDB 引擎中,还支持另外一种锁,称之为“意向锁”。意向锁是将锁定的对象分为多个层次,借助意向锁,InnoDB 支持可以支持多粒度锁定,这种锁定允许事务在行级锁和表级锁上同时加锁。

意向锁也分为两种:

  • 意向共享锁:事务想要获得一张表中某几行的共享锁,也称为 IS 锁。
  • 意向排他锁:事务想要获得一张表中某几行的排他锁,也称为 IX 锁。

于是,在 MySQL 的 InnoDB 引擎中锁的兼容矩阵变成了下面这样:

| | X 锁 | S 锁 | IX 锁 | IS 锁 |
| : — : | : — : | : — : | : — : | : — : |
| X 锁 | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
| S 锁 | 不兼容 | 兼容 | 不兼容 | 兼容 |
| IX 锁 | 不兼容 | 兼容 | 兼容 | 兼容 |
| IS 锁 | 不兼容 | 兼容 | 兼容 | 兼容 |

通过各种锁,是否能够顺利的解决前面提到的三个问题呢?再来看一个并行的事务模拟:

时间 事务T1 数据库中 A 的值 事务T2
t0 100
t1 XRead A (加 X 锁)
t2 XRead A (加锁失败)
t3 A = A - 30 wait
t4 UPDATE A wait
t5 70 wait
t6 COMMIT (释放 X 锁) wait
t7 XRead A (加 X 锁成功)
t8 70
t9 A = A * 2
t10 UPDATE A
t11 140 COMMIT
t12

在 t1 时该,事务T1 成功的对数据 A 加上了 X 锁,所以在 t2 时该,各务T2对A加上 X 锁时就会失败。在事务 T1 执行操作的过程中,事务 T2 将一直处理等待状态,直接事务 T1 COMMIT 并释放 X 锁才会继续执行,因此,通过锁技术,可以把多个并行的操作改为串行方式执行,能够有效的避免读到脏数据、更新丢失以及不可能重复读的问题。但锁的引入也还了新的问题,那便是死锁

再再再来看一个模拟:

时间 事务T1 数据库中 A 的值 事务T2
t0 100
t1 SRead A (加 S 锁)
t2 SRead A (加 S 锁)
t3 A = A - 30
t4 A = A * 2
t5 UPDATE A (会失败)
t6 UPDATE A (会失败)
t7 wait wait
t8 wait wait
t9

在这个模拟中,t1、t2 时该,事务T1 和事务 T2 都成功的对数据 A 加上了 S 锁,但是事务执行的过程中,他们都需要将 S 锁升级为 X 锁以便更新数据。但根据 X、S 锁的兼容性,在有 S 锁时是无法加上 X 锁的,因此这两个事务都会无限的等待下去,这便是死锁。

出现死锁时,若系统不进行干预,所有的事务都将无法进行下去。要解决死锁方法也很简单,那就是不等待。

MySQL InnoDB 如何解决死锁

InnoDB 死锁的 Demo,请看这里 MySQL :: MySQL 8.0 Reference Manual :: 15.7.5.1 An InnoDB Deadlock Example

要解决死锁,最简单的办法就是超时,即所有的等待都不能某个阈值,超过则进行回滚操作,其他的事务也就能继续执行。在 InnoDB 引擎中,可以通过 innodb_lock_wait_timeout 来配置锁的超时时间。

超时虽然简单,但如果只依赖超时回滚来解决死锁的问题,也存在一些缺陷。例如要回滚的事务是一个非常大的事务,或者是权重比较高的事务,此时回滚则会占用较多的 undo log,且会引影数据库整体的性能。

因此在 InnoDB 中,除了使用被动的超时机制外,还采了一种叫 等待图(wait-for graph )的技术来主动的检测死锁。关于死锁检的更多测细节,大家可进一步查看 MySQL 官方文档中的说明 MySQL :: MySQL 8.0 Reference Manual :: 15.7.5.2 Deadlock Detection

事务的隔离级别

说动隔离性,不得不提隔离级别。隔离级别用于约束事务的调度,因此先来看看什么是事务的调度。

事并的调度

“调度”指的是多个事务操作的执行次序。如果多个事务依次执行,则称为事务的串行调度。如果种用分时的方法,同时处理多个事务,旵称为事务的并发调度。

串行调度下,事务和事务之间不存在时间上的重叠。但在并行调度时,势必会有部份事务的调度是不正确的,那并发控制子系统又是如何来判断一个并发的调度是正确的呢?

可串行

对于多个事务,每个事务中语句的先后顺序在串行调度或并行调度中始终保持一致。并且在并发度调和串行调度的执行的结果一致,那么这个并发调度可称为“可串行化调度”,反之就不是。

只有可串行化的调度,才能真正在并发调度下获得正确的结果。

隔离级别

SQL 标准提供了 4 种隔离级别。分别是:

  • READ UNCOMMITTED:读未提交。这是最低的级别,允许事务读到其他事务已提交或未提交的数据(可能读到脏数据)。
  • READ COMMITTED:读已提交。允许事务读已提交的数据,但不要求两次读取的值须保持一致,即,不可重复读。
  • REPEATABLE READ:可重复读。只允许事务读已提交的数据,并且在两次读同一数据时不允许其他事务修改此数据。
  • SERIALIZABLE:可串行化。允许事务与其他事务并发执行,但系统必须保证并发调度是可串行化的。此级别下性能最低。

上面不同级别的隔离性依次提高。隔离级别越高意味着事务请求锁就越多,或保持锁的时间就会越长。

隔离级别由标准化组织制定,但实际上各数据库厂商在实现时各有不同,这里同样以 MySQL InnoDB 引擎为例,看看 MySQL 事务隔离性的实现。

MySQL InnoDB 的事务隔离性

InnoDB 的默认隔离级别是 REPEATABLE READ,但我们可能听说过,InnoDB 在此隔离级别下能达到 SERIALIZABLE 的隔离性。

InnoDB 会使用 “Next-Key lock” 来进行搜索和索引扫描,避免了幻读的产生,通过基于行锁的 MVCC 版本控制原理来达到重复读和已提交读,最后使配合表锁来实现串行化。 关于相关技术的细节可通过相关文章进行深入的了解,如:

持久性

DBMS 的恢复管理子系统会采取一系列措施保证在任何情况下何持事务的原子性和执久性,确保数据不丢失、不破坏。主要策略就是备份与恢复。

要在将来进行数据的恢复,那么在平时需要做好备份,否则也没有数据可用于恢复。而要做好备份,其是就是需要做好转储和日志。备份的方式有多种,暂时先不谈了,此处就只说说和事务关系密切的日志:REDO 日志和 UNDO 日志。

REDO 日志和 UNDO 日志其实是使用一种称作检查点的方法来实现的。DBMS 定时设置检查点,在检查检点时该,将事务修改的数据写入磁盘,并在日志文件中写入一条检查点记录。当 DB 需要时行恢复操作时,则只恢复那此在检查点之后的事务就行了,可以极大的减少 DB 恢复的时间。

总结

我们盘点了 SQL 中的事务的四大特及实现的基本原理。其中的隔离性特性,又涉及到事务的4层隔离级别,相关细节技术点本文未作详细的展开,大家可再行深入。

技术点

事务关联的技术点比较多,这里做了个简单的罗列,有兴趣也可深入的去了解

  • MVCC:Multi-Version Concurrency Control
  • Next-Key lock
  • 脏读、重复读、幻读
  • undo log、redo log
  • 串行化
  • 锁、共享锁、排他锁、死锁、死锁避免、死锁检测
  • 约束 与 锁引