数据库事务简介(一)--- 所谓事务

事务的概念

数据库事务简介(一)--- 所谓事务
数据库事务简介(二)--- 故障恢复(未完成)
数据库事务简介(三)--- 并发调度(未完成)

事务是一系列操作的集合,从宏观角度,事务是访问数据库的一个逻辑单元(集合),其微观视角可以抽象为对数据的读和写(一系列操作)。

例如银行转账,Bob给Smith转账100元,转账的操作为

            +-----------------+                   +-------------------+
            |                 |                   |                   |
            |                 |                   |                   |
            |      Bob        |  -------------->  |    Smith          |
            |                 |                   |                   |
            +-----------------+                   +-------------------+

              1. Lock Bob                          2  lock Smith

              3. Read Bob, Bob - 100, Write BOB    4  Read Smith, Smith + 100. Write Smith

              5  unLock Bob                        6  unLock Smith
  1. 锁定Bob账户
  2. 锁定Smith账户
  3. 读取Bob的账户,查看是否有100元,Bob账户减少100元,更新Bob的账户
  4. Smith账户加上100元,更新Smith的账户
  5. 解锁Bob账户
  6. 解锁Smith账户

这一些列操作集合为一个事务单元,而其本质上就是对Bob和Smith数据项的。除此之外,常见的事务还有很多,例如

  • 创建一个数据表
  • 执行一条select查询语句,读取一行记录
  • 创建一个数据库索引
  • 插入一条数据,删除一条数据
  • ......

上面所罗列,都是对数据库的一些基本操作,或基本操作的集合,这些都属于事务,或称之为事务单元。本质上都是对数据库的读写操作。

有人可能不理解,为什么没有 Begin Transaction 声明之后的 SQL 语句也是事务。SQL 的标准规定了一条 SQL 语句被执行,就隐式的开启了一个事务,当 SQL 语句执行完之后也自动的进行 Commit。

如果一个事务要执行多条 SQL 语句,就必须关闭单独的SQL语句的自动提交,显示的使用 Begin Transaction ... Commit/Rollback声明。当执行到 Commit 或者Rollback就结束一个事务。显式使用了Begin Transaction 的一组 sql 语句集合为事务单元。

事务的特性

提到事务,很多书籍或者 Blog 都会搬出事务的 ACID。ACID 是事务的基本特性

  • 原子性(Atomicity)
  • 一致性(Consistency)
  • 隔离性(Isolation)
  • 持久性(Duration)

诚然,ACID 特性是数据库事务的基石,然而到底 ACID 是何意,单从概念上很好背,然后理解却不是一件容易的事情。主要原因是这几个概念的命名的语义是单只在数据库语境中的意义,并且初看几个特性视乎又是相互有交集。我们暂时不必过于纠结他们的定义,而是从数据库的实现和使用角度来慢慢的理解。

原子性

如何理解原子性呢?在化学中,物质都是由原子构造,早期的化学家有一个想法,将物质进行切分,就像切蚯蚓一样。但是不管怎么切,每次都会变得更小。那么切到什么程度就不能切了呢?最后认为原子是最小的单位,即不能再进行分割。因此原原子表示不可分割, 其实原子还是可以分割为自由电子和原子核。原子性可以理解为“不可分割”,原子核和自由电子是不可分割的一个整体。

套用在数据库系统中,原子性就是组成事务的读写操作指令的集合是一个整体,不可分割。这些事务单元的一系列操作命令,要么全部执行,要么全部不执行(回滚)

宏观上来看,事务要么成功,要么就是失败,不能有其他状态。例如 Bob 给 Smith转账的 case 中,只有两种结果,转账成功(Bob 账户少 100,Smith 账户多100),或者转账失败(权当事务没有发生过)。不能存在,Bob 转出去了,而Smith 账户却没有收到钱的中间状态。

因为操作指令不能分割,就不存在有的指令执行成功,有的未执行或执行失败的中间状态。

这里的原子性强调的是从事务单元的宏观角度来看,事务成功或者失败。微观上来看就是事务可以恢复,恢复到事务尚未发生的状态。

一致性

一致性也是一个颇为让人费解的定义。顾名思义,一致性强调的就是一样。CAP理论也有一致性,在分布式系统中一致性只各个节点的数据都一样。数据库的一致性含义却是数据库从一个正确的状态,转变成另外一个正确的状态,这里更多的强调是正确性,更像是业务要求,业务又属于应用层软件所要关系的特性,数据库此时的角色略显尴尬。

之所以会有不一致,原因就是数据库在执行事务单元的时候是并发操作。即事务单元的指令是相互交叉并发执行,如果执行顺序不对,会有可能带来业务属性的错误。导致事务执行结束之后,发现数据库是不一致的(错的)。

如果是串行的执行,那么肯定就不会有一致性问题。因此一致性也可以这么理解,就是并发执行的事务,其结果和串行执行的结果一致(正确),那么就说这个并发事务符合一致性。

例如上面的转账系统,假设 Bob 和 Smith 初始都有100块,系统总共有 200 块,Bob 给 Smith 转账后,系统还是 200 元。如果数据库线程操作事务的时候,未对Bob 和 Smith 账户加锁。



                                T1
                                +
                                |  +-----------+       +------------+
                                |  |           |       |            |
                                |  |  Bob      |       |  Smith     |
                                |  |           |       |            |
                                |  |  100      |       |  100       |
                                |  +-----------+       +------------+
                                |
                                |
                                |  +-----------+       +------------+
                       T2       |  |           |       |            |
                   +----------> |  |  Bob      |       |  Smith     |
             Bob 0              |  |           |       |            |
             Smith 100          |  |  100 - 100|       |  100       |
                                |  +-----------+       +------------+
             lost 100           |
                                |  +-----------+       +------------+
                                |  |           |       |            |
                                |  |  Bob      |       |  Smith     |
                                |  |           |       |            |
                                |  |    0      |       |  100 + 100 |
                                |  +-----------+       +------------+
                                |
                                |
                                |
                                v

T1 事务表示 Bob 给 Smith 转账,当 Bob 账户减少 100 元,尚未给 Smith 账户加上100元的时候,此时其他数据库线程 T2 读取这个状态(其他事务并发执行),那么T2 读取到一个不正确的状态(此时的系统却只有100元),即数据库不一致了。因为在事务执行完毕之后,根本不存在 Bob 没有钱,Smith 仅有 100 元的情况。

这种状态是业务所不能接受的,却是数据库并发调度产生的。因此为了保证正确,数据库需要做到一致性。如何做到呢,不一致是因为中间状态被其他事务看到了,显然禁止读取中间状态就保证了一致性。

因此,数据库一致性可以理解为,在事务开始和结束之间的中间状态不会被其他事务所看到。实现一致性最简单办法就是,让数据库事务的指令像队列一样,有时序的串行执行。那么中间状态就不会被其他事务看到了。

可是这样的实现方案,一眼就能看出数据库的并发性能将会及其低下。并发调度的事务,其结果和串行调度的一致,那么也符合一致性的约束。为了保证一致性,又为了提高并发性能,数据库系统做了取舍,即通过隔离性来平衡。

隔离性

从上面的例子可以看出,串行执行数据库事务读写操作时,一定会保证数据库的一致性。然而很多时候,不同的线程的读写操作的未必是同样的数据。Bob 在给 Smith 转账的时候,Tom 也可能给 Green 转账。他们两个事务完全可以并行执行,相互之间不受影响。因此我们可以并发调度这两个事务进行,宏观上看他们就是同时进行的。隔离性就是指并发调度的事务,相互之间没有影响,事务都任务只有自己在执行。

隔离性的本来要求一组对数据库的并发修改互相不影响,可是实际上,像上面不操作同样数据的时候就不会有影响,而操作相同数据元素的时候,就可能产生冲突,冲突就会导致不一致。因此就需要区分哪个修改优先级更加高,而高优先级的修改应该覆盖掉低优先级的修改。

有的应用场景对性能要求比较高,反而对一些不一致错误业务上可以接受。此时就会出现允许这种不一致。这几种不一致的情况是:

  • 脏读
  • 不可重复读
  • 幻读

针对这几种不一致的”错误“,数据库使用了隔离级别来描述。本质上是对并发调度的处理方式进行的三种实现。所谓隔离就是隔离不一致的错误。隔离级别如下:

隔离级别 脏读 不可重复读 幻读
读未提交(Read Uncommit) × × ×
读已提交(Read Commit) × ×
不可重复读(Repeatable Read) ×
串行序列化(Serializable)

× 表示不能隔离,会出现此种不一致错误
表示可以隔离,不会出现该不一致错误
关于三种不一致问题,将会在数据库并发控制里讨论
注意:在实现了mvcc 的 innodb 和 postgresql 的数据库对应的RR隔离级别,也不会出现幻读错误。

通过上面的表,可以看到,读取 Bob 给 Smith 中间状态的隔离级别为读未提交,假设转账回滚了,就发生了脏读,这种隔离级别不能隔离脏读。对于这三种隔离级别,后面的并发调度将会详细讨论。

由此可见,对于隔离性的理解,可以结合后面的并发调度进行认识,简而言之为:适当的破坏一致性来提升性能和并行读

数据库系统要保证一致性,才能为应用层提供正确的业务逻辑,而为了提升并发读,在业务处理上做到正确性的保证,那么可以通过设置隔离级别提升性能,尽管这样会破坏数据库本身的一致性,而此时的一致正确交给应用层强保证,数据库只做低层次的保证。

持久性

数据库系统最后一个特性是持久性,这也是唯一一个字面意思很好理解的特性。持久性指的就是事务提交之后,就一定是在硬盘永久的存储,而不会丢失。虽然持久性比较好理解,可是实现却不简单,数据库系统通过undo,redo,undo/redo日志实现。即对数据库硬盘存储数据的时候,都是先写日志,再写存储。

通常,软件对数据的操作都在内存,内存数据是易丢失的。存储多数在外存(硬盘)。一个事务的提交操作包括内存和硬盘之间的交互。如下:

                 Memery                                        Disk

        +---------------------------------+                +--------------+
        |                                 |                |              |
        |  +-------+ commit +-----------+ |                |              |
        |  | T1    +------->|           | |                |              |
        |  +-------+        | DB Buffer | |   output       |              |
        |                   |           +-------------------->            |
        |  +-------+rollback|           | |                |              |
        |  | T2    +------->|           |<-----------------+              |
        |  +-------+        +-----------+ |   input        |              |
        +---------------------------------+                +--------------+

事务的处理在内存里进行,当事务进行提交的时候,实际上是把数据写入到内存的DB 缓冲区了,然后再将 DB buffer 的数据强制写入硬盘中。DB buffer 到 disk 之间的交互用 output 指令表示。对于不同的日志类型,Commit 和 output 的先后顺序也不一样。

Undo日志是在写了 commit 之后,再进行 output 操作,redo日志则相反,再进行output 操作,再写入commit 日志。当数数据库发生故障的时候,需要根据日志的规则进行事务的撤销和重做,保证在 commit 之后,数据一定要落盘。具体的原理和过程将会在故障处理的部分进行讨论。

事务状态

介绍了事务的 ACID 特性之后,想必这些概念还是有点模糊。没关系,后面我们会结合后面的数据库的故障恢复和并发处理来理解。事务是一系列操作命令的集合单元,那么在执行这些指令的时候,事务本身这个逻辑单元有一些状态,用于标识事务进行的程度。事务的状态机大致如下:

事务状态

执行了Begin Transition 之后,事务就进行了活动状态,如果刚执行第一个sql语句的时候,就出错了,那么事务就进入失败状态,最后会撤销事务进入终止状态。

当然也可以执行完所有 sql 语句。最后一个 sql 语句执行,就进入到部分提交,对数据的修改都写入到 DBbuffer 区里。此时要是发生故障,那么事务就会变成失败状态。当然如果正常提交,将 DB buffer 的数据写入到磁盘,那么事务就变成提交状态。

事务管理

事务的管理可以用下图简要的概括。

事务管理

当用户提交写的SQL命令的时候,首先将发送给事务管理器,后者进度并发调度处理,将命令发给查询处理器执行,同时也像日志管理器写入日志。

随后是将缓冲区的数据写入到数据库中,或者通过恢复处理器进行事务重做或者恢复。具体的恢复细节我们后面再讨论。

总结

事务是数据库系统的核心,是访问数据库的逻辑单元,这个逻辑单元是一条或者一系列操作读写指令的集合。事务具有ACID四个特性。

  • A 原子性指事务要么成功,要么失败,可以回滚。
  • C 一致性指事务从一个正确的状态转变成另外一个正确的状态,在事务开始和结束之间的中间状 态不会被其他事务所看到。
  • I 隔离性指并发执行的事务,不受别的事务的干扰,通过对一致性的适当破坏来提高并发性能,设置隔离级别隔离可能存在的一致性问题。
  • D 持久性指事务提交时候,一定能够写入磁盘,永久存在。

由此可见 AD 只要针对数据库的故障恢复,CI则是数据库并发调度的保证。接下来将会详细的介绍数据库的故障恢复和并发调度。

参考引用:

如何理解数据库事务中的一致性的概念?
数据库事务、隔离级别和锁
分布式事务原理与实践

推荐阅读更多精彩内容