1、ACID特性
1.1 原子性(atomicity)
事务可以由非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成,原子性要求事务的操作中,要么都做,要么都不做,事务中的任何一条SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库的状态应回到执行事务前的状态。原子性由undo日志来实现。
1.2 一致性(consistency)
事务的一致性指的是数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。一致性通常由undo日志来实现。
1.3 隔离性(isolation)
事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,隔离性通常这使用锁来实现。
1.4 持久性(durability)
事务的持久性要求事务一旦提交,其结果就是永久性的,即使发生宕机等故障,数据库也能将数据恢复。持久性通常使用redo日志来实现。
2、redo日志
redo日志用来实现事务的持久性,其由两部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易失性的;二是重做日志文件(redo log file),其是持久性的。每次事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的commit操作完成才算完成。每次将重做日志缓冲写入重做日志文件后,InnoDB引擎都需要调用一次fsync操作。因为重做日志缓冲写入重做日志文件时,是先写入文件系统缓存的,为了确保重做日志写入磁盘,必须进行一次fsync操作。
2.1 log block
重做日志都是以512B进行存储的,意味这重做日志缓冲和重做日志文件都是以block方式进行保存的,称为重做日志块(redo log block),如果一个页中产生的重做日志数量大于512B,那么需要分割为多个重做日志块进行存储。由于重做日志块和磁盘扇区大小一样,都是512字节,因此重做日志写入可以保证原子性,不需要doublewrite。
log block由三部分所构成,分别是 日志块头(log block header),日志块尾(log block tailer),日志本身。日志头占用12字节,日志尾占用8字节。故每个块实际存储日志的大小为492字节
2.2 LSN
LSN是log sequence number的缩写,其代表的是日志序列号,在InnoDB存储引擎中,LSB占用8字节,并且单调递增,LSN表示的含义有:
- 重做日志写入的总量
- checkpoint的位置
- 页的版本
LSN不仅记录在重做日志中,还存在于每个页中。在每个页的头部,有一个值FIL_PAGE_LSN,记录了该页的LSN,在页中,LSN表示该页最后刷新时LSN的大小。因为重做日志记录的是每个页的日志,因此页中的LSN用来判断页是否需要进行恢复操作。
2.3 恢复
数据库启动时,不管上次数据库运行是否正常关闭,都会尝试进行恢复操作。例如页P1的LSN为1000,而数据库启动时,检测到冲日日志的LSN为1300,并且该事务已经提交,那么数据库需要进行恢复操作,将重做日志应用到该页中。同样的,对于重做日志中LSN小于P1页的LSN,不需要进行重做,因为P1页的LSN表示页已经刷新到该位置。
3、undo日志
undo日志用来帮助事务回滚和MVCC功能。当InnoDB回滚时,它实际上做的是与先前相反的工作,对于每个insert,引擎会完成一个delete,对于每个delete,引擎会执行一个insert,对于每个update,引擎会执行一个相反的update。
3.1 undo存储管理
redo存储在重做日志文件中,而undo则存放在数据库内部的一个特殊的段中,这个段称为undo段,undo段位于表空间内。InnoDB存储引擎有rollback segment,每个回滚段记录了1024个undo log segment。在InnoDB1.0之前,只有一个rollback segment,因此支持的同时在线的事务限制为1024。从1.1开始,InnoDB最大支持128个roll back segment,故其支持同时在线的事务限制提高到了128 * 1024。需要注意的是,undo log segment 写入undo log的过程中,同样需要写重做日志。当InnoDB事务提交时,会做两件事情:
- 将undo log 放入列表中,供之后的purge操作;
- 判断undo log 所在的页是否可以重用,若可以则分配给下个事务使用
事务提交后不能马上删除undo log和undo log所在的页,这时因为可能还有其他事务需要通过undo log得到行记录之前的版本。因此事务提交时将undo log放入一个链表中,是否最终删除由purge线程来判断。
3.2 undo log格式
InnoDB中,undo log分为:
- insert undo log:该undo log在事务提交后直接删除,不需要进行purge操作
- update undo log:记录对delete和update产生的undo log,该undo log可能要提供MVCC机制,因此不能在事务提交时就进行删除。
3.3 purge
purge操作是清理之前的delete和update操作,将上述操作最终完成。
4、事务的4种隔离级别
4.1 不同隔离级别下出现的问题
在介绍事务的4种隔离级别之前,我们先介绍几个概念,分别是脏读、不可重复读、幻读。
- 脏读:
脏数据是指事务对缓冲池中的行记录的修改,并且还没有被提交。脏读是指读到了脏数据,即一个事务读到了另一个事务中未提交的数据,这显然违反了数据库的隔离性。
- 不可重复读:
不可重复读是指同一个事务内多次读取同一个数据集合,在这个事务还没有结束时,另外一个事务也访问了同一数据集合,并且做了一些DML操作。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不同的。这样就发生了一个事务内两次读到的数据是不一样的情况,这种情况称为不可重复读。不可重复读的侧重点在于delete和updata操作。
- 幻读(Phantom Problem):
指的是在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的结果可能会返回之前不存在的行。幻读的侧重点在于insert操作。
4.2 InnoDB的锁的类型
InnoDB的隔离性主要通过锁和MVCC机制来实现的,使用不同的行锁算法和MVCC策略,可以实现不同的隔离级别。InnoDB主要有两种锁,分别是共享锁(S Lock)和排它锁(X Lock),InnoDB有以下几种行锁的算法,分别是:record lock、gap lock和next-key lock
- 共享锁:允许事务读一行数据,select只有显式声明才会加共享锁,默认是不加共享锁的。
- 排它锁:允许事务删除或者更新一行数据,在数据库增删改查四种操作中,insert、delete和update都是会加排它锁(Exclusive Locks)的
- 一致性非锁定读:一致性锁定读指的是InnoDB通过MVCC的方式读取当前时间数据库中行的数据。MVCC即Multi Versison Concurrency Control,即多版本并发控制技术,在这种技术下,一个行记录可能有不止一个快照数据。如果读取的行正在执行delete或者update操作,这时读取操作不会因此等待行上的锁释放,相反,InnoDB引擎会去读取一个快照数据,这也叫一致性的非锁定读。在事务隔离级别read commited 和 repeatable read的隔离级别下,InnoDB使用非锁定的一致性读,但是对于快照数据的定义不同,在提交读的隔离级别下,对于快照数据,总是读取锁定行的最新一份快照数据。而在可重复读的隔离级别下,对于快照数据,读取的是事务开始时的行数据版本。
- 一致性锁定读:默认情况下,InnoDB的select操作使用一致性非锁定读,但是在某些情况下,用户需要显式对数据库的读取操作进行加锁操作以保证数据的逻辑一致性,这要求数据库对select的只读操作支持加锁语句。InnoDB对于select支持两种一致性的锁定读操作:
select ... for update # 对读取的行加一个X锁
select ... lock in share mode # 对读取的行加一个S锁
- record lock:单个行记录上锁。
- gap lock:间隙锁,锁定一个范围,但是不锁定记录本身。
- next-key lock:gap lock + record lock。InnoDB的查询都是采用这种锁的算法,例如一个索引有10、11、13和20这四个值,那么该索引可能被next-key锁定的区间为(-oo, 10),(10, 11],(12,13],(13,20],(20,+oo)。next-key锁的设计是为了解决幻读问题,利用这种锁定技术,锁定的不是单个值,而是一个范围。然而,当查询的索引具有唯一属性时,InnoDB会对索引进行优化,将其降级为record lock,即仅仅锁住记录本身,而不是范围。例如,如果一个表中有以下5行数据,分别是(1,1),(3,1),(5,3),(7,6),(10,8),其中第一列为聚集索引,第二列为辅助索引。在使用SQL语句:
select * from z where b = 3 for update
进行查询时,对于聚集索引,仅对第一列等于5加上record lock,而对于辅助索引,其加上的是next-key lock,锁定的范围是(1, 3),此外,InnoDB还会对辅助索引的下一个键值加上gap lock,即还有一个辅助索引范围为(3,6),因此,如果在新会话B中运行SQL语句,都会被阻塞。
select * from z where a = 5 lock in share mode
insert into z select 4,2
insert into z select 6,5
上面的例子中,gap lock的作用是为了阻止多个事务将记录插入到同一个范围之内,而这会导致幻读问题的产生。 在InnoDB存储引擎中,对于Insert操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许查询。
- 锁升级:InnoDB存储引擎不存在锁升级问题,因为其不是根据每个记录来产生锁的,相反,其根据每个事务访问每个页对锁进行管理的,采用的是位图的方式,因此不管一个事务锁住的是页中一个记录还是多个记录,其开销通常都是一样的。
4.3 InnoDB的4种隔离级别和实现
-
未提交读(read uncommited)
含义:在该隔离级别,select语句不加锁,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
实现:读数据不加锁,写数据加排它锁。所有写操作都会加排它锁,那还怎么读未提交呢?前面我们介绍排它锁的时候,有这种说明: 排他锁会阻止其它事务再对其锁定的数据加读或写的锁,但是对不加锁的读就不起作用了。
-
提交读(read commited)
含义:这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
实现:写数据时,使用排它锁, 读取数据不加锁而是使用了MVCC机制。因此,在读已提交的级别下,都会通过MVCC获取当前数据的最新快照,不加任何锁,也无视任何锁(因为历史数据是构造出来的,身上不可能有锁)。但是,该级别下还是遗留了不可重复读和幻读问题: MVCC版本的生成时机: 是每次select时。这就意味着,如果我们在事务A中执行多次的select,在每次select之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读,即:重复读时,会出现数据不一致问题,后面我们会讲解超支现象,就是这种引起的。
- 可重复读(repeatable read)
含义:这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB存储引擎在repeatable read事务隔离级别下,使用next-key lock的算法,避免了幻读的产生。所以说,InnoDB存储引擎在默认的隔离级别下事务的隔离级别已经完全保证了事务的隔离性要求,即达到SQL的serializable隔离级别。
实现:写数据时使用排它锁,读数据时使用MVCC。该级别与提交读不同的是MVCC版本的生成时机,即:一次事务中只在第一次select时生成版本,后续的查询都是在这个版本上进行,从而实现了可重复读。但是不加读锁的select会导致大量的幻读,可以通过next-key lock解决幻读问题。
- 可串行化(serializable)
含义:这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。可串行化通常只有在分布式事务中才用到。
实现:写数据时加排它锁,读数据时自动加共享锁,即对每个select语句后自动加上lock in share mode。在这种情况时,读占用了锁,对一致性非锁定读不再予以支持,事务基本只能串行执行。