数据库基础知识详解一(事务、并发一致性问题与隔离级别)

写在文章前:本系列文章用于博主自己归纳复习一些基础知识,同时也分享给可能需要的人,因为水平有限,肯定存在诸多不足以及技术性错误,请大佬们及时指正。
1.事务 1.1、事务的定义与特性
事务(Transaction)是一个操作序列,逻辑上不可分割的工作单位,以BEGIN TRANSACTION开始,以ROLLBACK/COMMIT结束。
四大特性(即常说的ACID特性):

  • 原子性(Atomicity):一个事务是一个逻辑上不可分割的工作单位,事务中包括的操作要么都做,要么都不做。即事务的所有操作要么全部提交成功,要么全部失败回滚。
  • 一致性(Consistency):事务的执行必须使数据库保持一致性状态,即使数据库从一个一致性状态变到另一个一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。
  • 隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。也就是说一个事务所做的修改在最终提交以前,对其它事务是不可见的。
  • 持久性(Durability):一旦事务提交成功,它对数据库中数据的修改是永久性的。
1.2、事务特性的实现原理
因为MySql的引擎中只有InnoDB支持事务,所以这里的实现都讲述的是InnoDB实现事务特性的原理。
首先介绍一下Innodb中的两个事务日志:redo log和undo log。redo log是重做日志,提供前滚操作,undo log是回滚日志,提供回滚操作。undo log不是redo log的逆向过程,它们都算是用来恢复的日志。
  • redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。(用于实现持久性)它包括两部分,一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。
  • undo log用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。(用于实现原子性和隔离性)除它之外还有binlog(二进制日志)也记录了很多innodb表的操作,也能实现重做的功能,但是他们之间有较大区别,这个后续再讲。
简单介绍一下对数据库的操作步骤:
因为数据库信息存储在磁盘当中,所以并不能直接操作它们。需要先加载数据库信息的缓存到内存中,再进行操作,再写回磁盘的文件当中。
持久性的实现:事务每次成功提交,代表该次操作已经写入了数据库信息的缓存中,但该缓存并不会马上写到磁盘当中。所以我们人为规定必须还要将此次对缓存的操作写到磁盘中的redo log中去,才算该事务提交成功,那么即使发生了断电/系统崩溃/其它的软硬件错误等情况,导致数据没有被完整写入到磁盘的数据库信息中,也能根据redo log文件将未完成的操作进行重做,以保证持久性。
问题:为什么不直接每次都必须把数据库信息写回磁盘才算事务提交成功,这样不就不用redo log了?
理由:因为写入到redo log只需写入一条记录并且是顺序I/O(追加到文件末尾),而对数据库信息的修改是随机I/O(大概率涉及多个表的修改,而它们并不一定是相邻的),每个事务提交前都要等待一次后者的时间是无法接受的,所以选用速度很快的写入redo log。
原子性的实现:和前面持久性的实现有些类似,每次操作数据库之前也都会把操作记录到undo log中,如果发生了断电/系统崩溃/其它的软硬件错误等情况,导致有一些事务没有提交但是已经写到了缓存当中,就可以通过undo log中的记录进行操作的回滚,以达到原子性。
隔离性的实现:这里是需要分步来说,首先是未提交读(Read Uncommited)级别,该隔离级别不需要其他操作就能达到。然后在MySQL中,使用MVCC来实现提交读(Read Commited)和可重复读(Repeatable Read)这两个隔离级别,使用MVCC+Next-Key Lock来解决幻读的问题。
MVCC是什么?
简单来说就是在数据表每行中设有隐藏的列,一列是上一次更新过该行数据的事务id,一列是一个指针,指向本数据上一个版本的undo log。(记录了值)
ReadView机制
就是当你执行一个事务的时候,会生成几个数值。
trx_ids:一个代表此时哪些事务在MySQL中还未提交;
creator_trx_id:一个代表创建本ReadView事务的id;
low_limit_id:一个代表此时未提交的事务中的最小id;
up_limit_id:一个代表MySQL下个生成的事务的id。
  • 实现提交读:每次读取数据时,使用ReadView机制,如果该数据最近更新的事务id在未提交事务中,那就根据表中隐藏的指针找到上一次提交了的事务对应的数据并读取。
  • 实现可重复读:每次select的时候检查所读数据最近更新的事务id号,如果在本次事务之后,那么不会读取,会一直沿指针追溯到更新事务id号小于本事务id号的数据再读取。
  • 解决幻读:使用MVCC+Next-Key Lock。
InnoDB有三种行锁的算法:
(1)Record Lock:行锁,即单个行记录上的锁。
(2)Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
(3)Next-Key Lock:行锁加两边的间隙锁,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。(前开后闭的区间)
以我个人的理解来看,保证了原子性、持久性与隔离性,就做到了一致性。
2.常出现的并发一致性问题
  • 丢失修改:一个事务对数据进行了修改,在事务提交之前,另一个事务对同一个数据进行了修改,覆盖了之前的修改。
    例如:事务A把变量a的值从5修改到10,还未提交时另一个事务B又把变量a的值修改为15并成功提交,然后事务A提交,最后变量a的值还是10。相当丢失了事务B的这次修改。
  • 脏读:一个事务读取了被另一个事务修改、但未提交(有可能进行了回滚)的数据,造成两个事务得到的数据不一致。
    例如:事务A修改变量a的值为10,但未提交。此时事务B读取变量a的值为10,然后进行操作。但是事务A之后进行了回滚,变量a的值变回了5。相当于事务B读取到变量错误的值,即读取了脏数据。
  • 不可重复读:在同一个事务中,查询操作在某个时间读取某一行数据和之后一个时间读取该行数据,发现数据已经发生修改(针对的update操作)。
    例如:计算变量a+变量a,前面那个a读取的值是50,后面那个读取到的值是100。
  • 幻读:当同一查询多次执行时,由于其它事务在这个数据范围内执行了插入操作,会导致每次返回不同的结果集。(和不可重复读的区别:幻读针对一个数据整体/范围,并且针对的是insert操作)。
    例如:多次查询score>60的学生,第一次查询到三个学生,第二次查询到五个学生。
3.数据库的隔离级别
  • 未提交读(Read Uncommited):在一个事务提交之前,它的执行结果对其它事务也是可见的。会导致脏读、不可重复读、幻读。(比如某程序更新数据,但是并没有提交,别的程序就可以读取到它。)
    该级别下,select语句不加锁,虽然并发性最高,但是隔离性最差,所以该隔离级别一般不会被使用。
  • 提交读(Read Commited):一个事务只能看见已经提交的事务所作的改变。可避免脏读问题。
  • 可重复读(Repeatable Read):可以确保同一个事务在多次读取同样的数据时得到相同的结果。可以理解为事务在读取数据的时候获取了一次当前时刻数据的快照,后续的读取都以快照为参照,所以不受其余事务的影响。但是该隔离级别可能会导致幻读,后续在讲解MVCC的时候会说明不能避免幻读的理由。
    MySql默认隔离级别。可避免脏读 、不可重复读的发生。
  • 可串行化(Serializable):强制事务串行执行,使之不可能相互冲突,从而解决幻读问题。每次读都需要获得表级共享锁,读写相互都会阻塞。
    【数据库基础知识详解一(事务、并发一致性问题与隔离级别)】虽然可以避免脏读、不可重复读、幻读的发生。但是因为可能导致大量的超时现象和锁竞争,实际很少使用。

    推荐阅读