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

2018-10-08  本文已影响350人  人世间

事务的概念

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

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

例如银行转账,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数据项的。除此之外,常见的事务还有很多,例如

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

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

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

事务的特性

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

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

原子性

如何理解原子性呢?在化学中,物质都是由原子构造,早期的化学家认为原子是最小的单位,不能再进行分割。其实原子还是可以分割为自由电子和原子核。原子性可以理解为“不可分割”,原子核和自由电子是不可分割的一个整体。

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

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

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

这里的原子性强调的是从事务单元的宏观角度来看,事务成功或者失败。微观上来看就是事务可以恢复,恢复到事务尚未发生的状态。通常在非关系数据库语境,原子性含义如下:

对于一门支持并发的编程语言(比如Java,C++),原子性是指一组指令被执行时,不受其他指令的干扰。

Redis作为NoSQL数据库,它的原子性也是指一组指令执行的时候,不受其他指令的干扰,而不是这些指令执行失败了可以回滚。

一致性

一致性也是一个颇为让人费解的定义。顾名思义,一致性强调的就是一样。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四个特性。

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

参考引用:

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

上一篇下一篇

猜你喜欢

热点阅读