本文主要针对MySQL数据库常用的InnoDB存储引擎进行说明。
事务的定义
-
《Datebase System Concepts》: 构成单一逻辑工作单元的操作集合
-
《An Introduction to Database System》: 事务是一个逻辑工作单元
-
《MySQL 技术内幕:InnoDB存储引擎》:事务是访问并更新数据库中各种数据项的一个程序执行单元。
-
ANSI SQL标准:事务是数据库关系系统中的一系列操作的逻辑单位,同时事务”要么成功要么失败“。
事务的特性
-
A(Atomicity)原子性,指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算整个事务成功。
-
C(Consistency)一致性,指事务将数据库从一种状态转变为下一种一致的状态。在事务开始之前和事务结束之后,数据库的完整性约束没有被破坏。
-
I(Isolation)隔离性,隔离性还有其他称呼,如并发控制(concurrency control)、可串行化(serializability)、锁(locking)等。
-
事务的隔离性要求每个读写事务的对象对其他事务的的操作对象能相互分离,即该事物提交前对其他事务都不可见,通常这使用锁来实现。
-
当前数据库系统中都提供了一种粒度锁(granular lock)的策略,允许事务仅锁住一个实体对象的子集,以提高事务之间的并发度。
-
-
D(Durability)持久性,事务一旦提交,其结果就是永久性的。需要注意的是,只能从事务本身的角度来保证结果的永久性。
事务的隔离性
读并发问题
隔离级别的定义,来源于三个读并发问题。
脏读
一个事务读取到了另一个事务未提交的脏数据。
脏数据是指事务对缓冲池中行记录的修改,并且还没有被提交(commit)。
不可重复读
一个事务内多次读取到同一数据集合(范围查询),且两次读到的数据可能不一样(其他事务对部分数据进行修改)。
不可重复读的关注点是,之前读到的某几行记录的字段值被修改了。
幻读
在同一事务下,连续执行两次同样的SQL语句(范围查询)可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行(其他事务有插入、删除操作)。
幻读的关注点是,之前读到的某范围数据,范围内数据总量发生了改变。
隔离级别
四种隔离级别分别解决了上述问题
-
读未提交(Read uncommitted)
-
这种事务隔离级别下,select语句不加锁。
-
读已提交(Read committed)
-
可避免 脏读 的发生。
-
MySQL以外大部分数据库的默认隔离级别。
-
-
可重复读(Repeatable read)
-
MySql默认隔离级别,InnoDB存储引擎能够避免幻读 的发生。
-
可避免 脏读 、不可重复读 的发生。
-
-
串行化(Serializable )
- 可避免 脏读、不可重复读、幻读 的发生。
如何实现隔离级别
读未提交
SELECT语句采用无锁策略,非锁定读,很容易产生脏读。
但若是,主从拷贝过程的slave节点,并且slave节点上的查询不需要特别精确的返回值的情况下,可以采用该隔离级别。
读已提交
SELECT语句支持一致性锁定读策略,该策略下,用户需要显示地对数据库读取操作加锁以保证数据逻辑的一致性。
SELECT……FOR UPDATE对读取的行记录加一个X锁,其他事物不能对已锁定的行加上任何锁。
SELECT……LOCK IN SHARE MODE对读取的行记录加一个S锁,其他事务可以向被锁定的行加S锁,但是如果加X锁,则会被阻塞。
SELECT语句也支持一致性非锁定读策略,具体是指InnoDB存储引擎通过MVCC技术来实现读取当前执行时间数据中行的数据。
在RC隔离级别,SELECT语句是每次读取的都是最新版本的行数据快照,快照本身通过undo段来实现,不占用额外资源。
而对于一致性非锁定读,即使读取的行已被执行了SELECT……FOR UPDATE,也是可以进行读取的。
可重复读
SELECT语句通过一致性非锁定读策略实现,在RR隔离级别,SELECT语句读总是读取事务开始时的行数据版本。
为了防止幻读,RR隔离级别引入了间隙锁(gap lock),它锁住的是一个区间(开区间),当一个区间被加了间隙锁时,是无法执行插入的。
RR隔离级别下,行锁和它之前的间隙锁,共同构成临键锁(next-key lock),是一个前开后闭的区间。
而当SQL语句通过辅助索引进行查询时,由于有两个索引(主键索引、辅助索引),存储引擎会对两个所分别进行加锁。
即对主键索引加record lock,对辅助索引加next-key lock,特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock。
CREATE TABLE z (a INT, b INT, PRIMARY KEY(a), key(b))
INSERT INTO Z SELECT 1, 1;
INSERT INTO Z SELECT 3, 1;
INSERT INTO Z SELECT 5, 3;
INSERT INTO Z SELECT 7, 6;
INSERT INTO Z SELECT 10, 8;
SELECT * FROM z WHERE b=3 for UPDATE;
加锁情况:
record lock,(a=5, b=3)
gap lock,b=(3, 6)
next-key lock,b=(1, 3]
串行化
InnoDB 默默的把所有纯 SELECT 语句都转成了 SELECT … LOCK IN SHARE MODE ,也就默认都加共享锁。
事务的一致性
undo log用来保证事务的一致性,以及MVCC功能。
undo的作用可以视为是一种恢复操作,undo回滚行记录到某个特定版本。
undo是逻辑日志,根据每行记录进行记录。
基本概念
undo存放在数据库内部的一个特殊段(segment)中,这个段称为undo段。undo段位于共享表空间内。
用户通常对undo有这样的误解:undo用于将数据库物理地恢复到执行语句或事务之前的样子——但事实并非如此。undo是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。
所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同。如用户执行了一个INSERT 10W条记录的事务,这个事务会导致分配一个新的段,即表空间会增大。回滚会删除插入的数据,但不会表空间大小不会收缩。
除了回滚操作,undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成的。当用户读取一行记录时,若该行记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。
最后也是最为重要的一点,undo log会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护。
事务的原子性、持久性
redo log称为重做日志,用来保证事务的原子性和持久性。
redo的作用也可以视为是一种恢复操作,redo恢复提交事务修改的页操作。
redo通常是物理日志,记录的是页的物理修改操作。
基本概念
重做日志用来实现事务的持久性,即事务ACID的D。其由两部分组成:一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log file),其是持久的。
InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的提交操作完成才算完成。
redo log是按顺序写的,在数据库运行时不需要对redo log文件进行读取操作。undo log是需要进行随机读写的。