惊!MySQL事务隔离级别原来这么简单
扫码二维码
获取更多精彩
事务概述
MySQL4种事务隔离级别分析
总结
1 事务概述
什么是事务?
数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。事务的使用是数据库管理系统区别文件系统的重要特征之一。
事务拥有四个重要的特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),人们习惯称之为 ACID 特性。
事务
特性
事务具有ACID特性
1. 原子性(Atomicity)。事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。例如,如果一个事务需要新增 100 条记录,但是在新增了 10 条记录之后就失败了,那么数据库将回滚对这 10 条新增的记录。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
2. 一致性(Consistency)。指事务将数据库从一种状态转变为另一种一致的的状态。事务开始前和结束后,数据库的完整性约束没有被破坏。例如工号带有唯一属性,如果经过一个修改工号的事务后,工号变的非唯一了,则表明一致性遭到了破坏。
3. 隔离性(Isolation)。要求每个读写事务的对象对其他事务的操作对象能互相分离,即该事务提交前对其他事务不可见。也可以理解为多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。这指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。例如一个用户在更新自己的个人信息的同时,是不能看到系统管理员也在更新该用户的个人信息(此时更新事务还未提交)。
4. 持续性(Durability)。事务一旦提交,则其结果就是永久性的。即使发生宕机的故障,数据库也能将数据恢复,也就是说事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。这只是从事务本身的角度来保证,排除 RDBMS(关系型数据库管理系统,例如 Oracle、MySQL 等)本身发生的故障。
2 MySQL4种事务隔离级别分析
隔离
级别
事务隔离级别概述
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
1. Read Uncommitted(读取未提交内容)。在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
2. Read Committed(读取提交内容)。
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
3. Repeatable Read(可重读)。
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
4. Serializable(可串行化)。
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
以上这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:
脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新了原有的数据。
幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
MySQL数据库实现了这以上4种隔离级别,每种隔离级别可能会产生的问题如下表所示:
隔离级别
脏读
不可重复读
幻读
Read Uncommitted
Y
Y
Y
Read Committed
N
Y
Y
Repeatable Read
N
N
Y
Serializable
N
N
N
隔离
级别
测试各种隔离级别
创建一张Student表。
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
`age` int(4) NOT NULL,
`gender` varchar(8) NOT NULL,
`createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
分别使用2个客户端A和B操作Student表,不断修改客户端A的隔离级别,在客户端B进行数据修改。
客户端A:将隔离级别调整为Read Uncommitted,然后查询当前隔离级别如下图所示。
客户端A:开启一个事务,此时数据为初始状态。
客户端B:启动一个事务,更新id=1学生,将其name属性修改为wushuang,但不提交事务。
客户端A:再次读取数据,发现数据已经被修改了,这就是所谓的“脏读”。
客户端B:事务回滚。回滚后的id=1的学生name属性仍然为zhangsan。
客户端A:再次读取Student表中的数据,发现id=1的学生name属性仍然为zhangsan。
经过上面的实验可以得出结论,事务B更新了一条记录,但是没有提交,此时事务A可以查询出未提交记录。造成脏读现象。未提交读是最低的隔离级别。
客户端A:将隔离级别调整为Read Committed,然后查询当前隔离级别。
客户端A:开启一个事务,此时数据为初始状态。
客户端B:启动一个事务,更新id=1学生,将其name属性修改为wushuang,但不提交事务。
客户端A:再次读取数据,发现数据未被修改。
客户端B:事务提交。
客户端A:再次读取Student表中的数据,发现id=1的学生name属性为wushuang。
经过上面的实验可以得出结论,已提交读隔离级别解决了脏读的问题,但是出现了不可重复读的问题,即事务A在两次查询的数据不一致,因为在两次查询之间事务B更新了一条数据。已提交读只允许读取已提交的记录,但不要求可重复读。
客户端A:将隔离级别调整为Repeatable,然后查询当前隔离级别。
客户端A:开启一个事务,此时数据为初始状态。
客户端B:启动一个事务,更新id=1学生,将其name属性修改为无双,但不提交事务。
客户端A:再次读取数据,发现数据未被修改。
客户端B:事务提交。
客户端A:再次读取数据,发现数据未被修改。
客户端B:插入一条新的数据,并提交。
客户端A:再次读取Student表中的数据,发现数据未被修改。
客户端A:提交事务。再次读取Student表中的数据,发现多了一条数据wangwu。
由以上的实验可以得出结论,可重复读隔离级别只允许读取已提交记录,而且在一个事务两次读取一个记录期间,其他事务不得更新该记录。但该事务不要求与其他事务可串行化。例如,当一个事务可以找到由一个已提交事务更新的记录,但是可能产生幻读问题(注意是可能,因为数据库对隔离级别的实现有所差别)。像以上的实验,就没有出现数据幻读的问题。
重新执行以上步骤,分别开启客户端A和客户端B,事务隔离级别依旧保持为repeatable read。在客户端B插入一条数据。
客户端A插入一条id=4的学生数据。
客户端A出现主键冲突问题。其实这就是该隔离级别下可能产生的问题,MySQL称之为幻读。
客户端A:将隔离级别调整为Serializable,然后查询当前隔离级别。
客户端B:启动一个事务,插入一个name=xiaoming的同学。
由于客户端A事务并未提交,因此客户端B的insert语句将会阻塞。
客户端A:提交事务。
客户端B:在客户端A提交后,客户端B的insert语句执行结束。
3 总结
事务的机制是通过视图(read-view)来实现的并发版本控制(MVCC),不同的事务隔离级别创建读视图的时间点不同。
可重复读是每个事务重建读视图,整个事务存在期间都用这个视图。
读已提交是每条 SQL 创建读视图,在每个 SQL 语句开始执行的时候创建的。隔离作用域仅限该条 SQL 语句。
读未提交是不创建,直接返回记录上的最新值
串行化隔离级别下直接用加锁的方式来避免并行访问。
这里的视图可以理解为数据副本,每次创建视图时,将当前已持久化的数据创建副本,后续直接从副本读取,从而达到数据隔离效果。
更多有关事务隔离级别实现的方式请参考下一篇文章。
扫码二维码
获取更多精彩