数据库事务的理解
事务的四个特性
事务一般包含一系列的操作,放在MySQL中,代表的是一系列的SQL语句。事务具备四个特性,ACID,如下:
A:原子性。事务所包含的一系列操作是一个整体,要么全部正确执行,要么全部不执行。
C:一致性。在事务执行的前后,数据的完整性约束不被破坏。
I:隔离性。事务之间是隔离操作的,就好像在特定时间,只有一个事务在执行。确保多个事务之间的操作不会相互影响。
D:持久性。事务执行完成之后,就会被永久的保留在存储介质中,不会丢失。
原子性很容易理解,我们也会经常用到。我们经常在项目中使用的方式就是在某个方法上添加事务注解,以保证该方法中的所有针对数据库的操作,只要有一条报错,其它所有操作都不会执行,以避免产生脏的数据。
一致性也很简单,但是要理解什么叫数据的完整性约束。数据的完整性约束事实上是数据库对其保留的数据有一个规范性的约束。如果某个事务进行的操作会破坏这个规范,将是不被允许的。
约束包括实体完整性,域完整性,参照完整性和用户定义的完整性。实体完整性规定表中的每一行是在库里的一个唯一的实体。域完整性指的是列的值必须满足该列的相关约束,如取值范围,精度等。参照完整性指的是两个表的主关键字和外关键字对应的数据应该保持一致。用户定义的约束指的是用户自定义的针对某个特定数据库的约束。
隔离性指的是多个事务之间的相互影响,就好像多线程中的锁。MySQL的innodb引擎可配置四个隔离级别,分别是未提交读,提交读,可重复读和序列化,从提交读向后,依次可解决三种读取数据时的问题,分别是脏读,不可重复读,幻读。默认级别是提交读。
持久性很简单,指的是事务执行完成后,事务所做的操作会反映到数据上,并保留下来,我们使用数据库的目的,不就是为了持久化的保存数据吗?
事务的原理
接下来,我们尝试去分析,数据库是怎么做到事务的,换句话说,数据库是怎么做到ACID的?
事务的形成
先来看看事务是如何形成的,在数据库中,事务有三种形成的方式,或者说是事务模型,如下:
隐式事务,每一条语句自动成为一个事务。
显式事务,显式的将一系列语句定义成一个事务。
自动事务,系统会自动默认的把一系列语句合理组合成一个事务,不需要显示添加开始或结束标记。
事务的状态
事务形成后,就像多线程中的线程一样,有了各种状态。事务的状态从宏观上和微观上来看,是有差异的。
宏观的来看,事务只有三种状态,分别是执行中,执行成功,执行失败。
微观的来看,事务的状态变得要更细致,更复杂一些,分别是执行中,最后提交,执行成功,执行失败,回滚取消。
原子性的实现——回滚日志
先来看看原子性的实现。其实说来也很简单,因为原子性的问题,拆开来就是一个问题:如果事务中的某一步操作失败了,该如何恢复已经执行了操作?
就好像你在修改一个文档,改着改着突然不想改了,怎么办?撤销呗。数据库也是这样做的。数据库通过一个回滚日志,记录了事务对数据库的操作,当一个事务半途失败时,会基于回滚日志,对事务已经执行的操作进行回滚。
所以,对于一个事务,当事务的某个操作要执行前,会先记录回滚日志,然后再执行相应的修改,这才能保证,当一个事务的某个操作失败后,即使是由于数据库进程直接被杀等原因,当数据库进行恢复后,仍然能够从回滚日志中未完成的事务,进行相应的回滚。
这里需要注意的一个点是,利用回滚日志回滚时,是基于逻辑驱动的物理回滚,而不是直接物理回滚。可以理解为,我们在事务中使用的每一条 INSERT 都对应了一条 DELETE,每一条 UPDATE 也都对应一条相反的 UPDATE 语句。
持久性的实现——重做日志
持久性的要求是,执行完成的事务,对数据的操作是一定要持久化到磁盘中的。但是,会有一些极端情况,比如说某事务已经提交,但是此时断电了,也就是说持久化还没来得及做,这时候,该怎么办?注意,此时事务是正常提交的,没有报错,也就是说,不能靠回滚来解决。那么,就该重做日志登场了。
重做日志包含两份,一份放在重做日志缓存区中,另一份放在磁盘文件中。当一个事务尝试对某数据进行修改时,就会形成一条重做日志,放在缓存中。当事务在真正提交时,会先将重做日志从缓存区刷到磁盘中,再做对应数据的磁盘更新。
如果在更新还没来得及做完的情况下,发生了某种故障导致数据库进程重启,重启后,数据库就会根据重做日志,去完成故障前未完成的持久化工作。
这里,还有个小tips,就是可能有同学会说,如果系统是在持久化到一半的时候出了问题,该怎么办呢?换句话,有没有可能因为重做日志只做了一半而留下脏数据?别担心,MySQL想到了这一点,所以,在innodb中,重做日志都是以512字节的块的形式存储的,这刚好与磁盘扇区的大小相同,所以,重做日志的写入是原子性的,不会出现只写入了一半重做日志而出现脏数据的情况。
整理一下以上说的内容就是:通过回滚日志,达到了事务回滚,原子性的目的,又通过重做日志,达到了持久化的目的。但是,以上只是在单个事务的前提下进行的,多事务并行的情况下,情况会变得复杂。
级联回滚
多事务在并行的情况下,如果不进行一些特殊的处理,可能会出现一些不合理的问题。
举个例子:假设有事务A和事务B,它们都针对数据d做了一些操作。
事务A对d做 +1 操作,事务B做 -1 操作。
我们假设B在A修改d之后开始,在A之前结束。
假设d原来为10,正常来讲,当A和B都结束后,d还是10。
但是,发生了一个异常是,当B执行完成后,A事务由于某些原因失败了,进行了回滚。于是,根据回滚日志,对d进行了-1操作,于是,d变成了9。这对于A事务和B事务来说,显然都是无法理解的。对于A来说,我读取到的是10,我什么都没做,它怎么变成9了呢?而对于B来说,由于是在A修改d之后开始的,所以,读到了11,但是只减了1,怎么变成9了呢?
上面的情况,就称之为脏读。
那么,如果要以回滚的方式去解决这个问题的话,就需要将A回滚的同时,也将B回滚,就是说,在A的脏数据的基础上进行的事务,都需要进行回滚,称之为级联回滚。
脏读,不可重复读和幻读
从级联回滚的情况,我们已经判断出,如果任由多事务并行,那么,就会现各种问题,解决起来是复杂的。除了脏读,还可能会发生不可重复读和幻读。
我们再来统一说一下这三种可能发生和问题。
脏读:多个事务共用某一数据时,A事务修改了数据,B事务使用了该数据,此后,如果A事务由于某些原因,回滚或再次修改了该数据,那么,B读取到的数据就是脏数据,也就是发生了脏读。依据脏读得到的脏数据进行的后续逻辑基本上也是不正确的。
不可重复读:多个事务共用某一数据时,A事务需要多次读取该数据,在A事务的两次读取之间,B事务修改了该数据,这就导致A事务两次读取到的数据是不同的。所以,称之为不可重复读。
幻读:A事务需要多次读取某一范围数据,在A事务的两次读取之间,B事务在此范围内,添加了一条数据,导致A事务第二次的读取多读到一条数据,发生了错误。称之为幻读。
隔离性的实现
前面介绍过,隔离性有四个级别,分别是未提交读,提交读,可重复读和序列化,从提交读向后,依次可解决三种读取数据时的问题,分别是脏读,不可重复读,幻读。那么,隔离性是如何实现的?
隔离性的实现事实上就是采用某种并发控制机制来对并发的事务进行控制,以限制不同事务对同一资源的访问和更新。我们来介绍三种最重要的并发控制机制。
锁:对目的资源加锁,是一种常见的并发控制机制,MySQL中,锁有两种,共享锁和互斥锁 ,前者又叫读锁,后者又叫写锁。当某事务对某资源加了读锁时,其他事务仍然可以读取该资源,但是当加了写锁时,其他事务将无法读或写,只能等待当前事务提交后释放锁。
读锁保证了读操作可以并发执行,相互不受影响,而写锁则保证了写操作的串行执行。
时间戳:通过对锁的说明,我们发现,锁显然是从悲观锁的思想出发而设计的,那么,从乐观锁的角度出发,就有了时间戳这种并发控制机制。这种机制是在每条记录上添加一个读时间戳和一个写时间戳。读时间戳中记录了最后一次读取的时间,写时间戳则记录了最后一次修改该记录的时间。当一个事务要对某个数据进行修改时,会根据读时间戳和写时间戳来判断在当前事务读取后,是否有其它事务修改过,如果没有,则写入,如果有,则生产读取最新的数据,并重新执行事务的修改操作。
MVCC:MVCC也是一种乐观锁思想的实现,只不过,它是通过版本号和快照来实现的。基本特征如下:
每行数据都额外保存一个版本号,每次数据更新时都更新此版本号。
修改时,会将当前版本copy出一份进行修改。不影响其它事务。
保存时,会比对版本号,如一致,说明此期间没有其他事务修改过,成功保存并更新版本号。如不一致,则说明有其他事务修改过,则当前事务失败并回滚。
以上的MVCC的一个基本操作,MySQL的innodb对MVCC的实现,又有了一些差异。
innodb在每行增加了两个版本号,分别是创建版本号和删除版本号。我们来看下,在增、删、改、查操作下,这两个版本号是如何变化和起作用的。
增:插入数据时,将当前事务版本号插入到创建版本号。
改:修改数据时,先将原记录标记为删除,删除版本号为当前事务版本号,再插入一条新记录,创建版本号为当前事务版本号。
删:删除数据时,将当前事务版本号插入到删除版本号。
查:重点来了,在增、改、删时,针对版本号的操作,都要在查时被用到。也就是说,一个事务在查询时,版本号要满足以下两个条件:创建版本号小于或等于当前事务版本号(说明数据是在当前事务或当前事务之前启动的其他事务中创建的。)
删除版本号大于或等于当前事务版本号(说明数据是在当前事务或当前事务之后启动的其他事务中删除的。)
从innodb的MVCC的实现,可以发现,innodb的MVCC,和本身的MVCC还是存在一定差距的,所以,我们或者可以理解为Innodb的MVCC只是挂了个名,它的实现其实是按照自己的方式去实现的。
另外还要注意的是,innodb的MVCC在未提交读和序列化的事务级别是不适用的。因为在未提交读的情况下,会发生脏读,也就是说,能读到未提交的数据行,所以不适用,而在序列化级别下,所有事务都是串行的,也就不存在事务并发的问题,MVCC也就没有什么意义了。
一致性的实现——AID的保证
聊完了AID,最后,还剩下一个C。那么,数据库是如何保证C的呢?
事实上,要解答这个问题,首先要搞明白,这个C的含义。首先,不要把这个C和CAP中的C相混淆。
ACID的C,指的是数据本身的准确性和合理性,而CAP中的C,指的是在分布式系统中,各个子节点的数据是否相同。
理解了这个,那么,数据库对于C的实现也就好理解了。首先,就是不符合一致性的操作会被阻止,如类型不符的插入或修改,主键重复的插入或修改等,再有,就是对AID的实现。保证了数据更新时的校验,又保证了AID,那么,C也就自然完成了。




