一、事务概念
1. 什么是事务?
事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。
2. 事务的四个特征
- 原子性:事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做
- 一致性:事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。
- 隔离性:一个事务的执行不能对其它事务形成干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持续性:一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。
3. 事务隔离
事务隔离是指一个事务所做的修改,对另一个事务的可见性。如果事务与事务之间不进行隔离,那么就会导致数据读取中的一些问题:脏读、不可重复读、幻读。
一个事务之间,这并不会产生任何问题,但是两个事务之间根据隔离级别的不同,也就存在了各种各样的问题。如下:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读取未提交内容 (Read Uncommitted) | √ | √ | √ |
读取提交内容 (Read Committed) | × | √ | √ |
可重复读 (Repeatable Read) | × | × | √ |
可串行化 (Serializable) | × | × | × |
接下来让我们详细解读下,四种隔离级别下,分别产生脏读、不可重复读、幻读的原因
二、Mysql的四种隔离级别
为了方便理解,这里统一做这样一个假设:我们有这样两个事务:事务A和事务B,它们在下面中进行着各种乱七八糟的事务操作(偏偏在高并发下这种乱七八糟就是会发生的)。
我们有这样一个表:test。表数据为
id | value |
---|---|
1 | a |
2 | b |
3 | c |
1. 读取未提交内容 (Read Uncommitted)
脏读是怎么产生的?假设事务A、B进行这样的操作:
- 事务A开始,把id为1的value修改为了’aaa’
- 事务B开始,获取id为1的数据,这个时候它能够得到’aaa’
- 事务A回滚,id为1的value从’aaa’被回滚为了’a’
- 事务B使用的是’aaa’,这跟真实数据库中的数据不一致,读的是脏数据
让我们总结一下,脏读产生的关键点有哪些
- 事务B可以获取事务A尚未提交的数据(脏数据)
- 事务A不承认自己的修改,脏数据没有转化为正式数据
在“读取未提交内容”这一隔离级别下,执行中的事务更改过程中可以被其他事务获取到数据,从而引发杯具。
2. 读取提交内容 (Read Committed)
在读取提交内容这一级别下,只允许提交事务后的数据才可以被获取到,也就是上面的“事务B可以获取事务A尚未提交的数据(脏数据)”不成立,从而杜绝了脏读的情况。
让我们模拟下上面的操作:
- 事务A开始,把id为1的value修改为了’aaa’
- 事务B开始,获取id为1的数据,这个时候它获取的是还未被修改的数据’a’
- 事务A回滚,id为1的value从’aaa’被回滚为了’a’
- 事务B使用的是’a’,这跟真实数据库中的数据一致,没有产生脏读
这一隔离级别也是大多数据库的默认隔离级别,但并不是MySQL的默认隔离级别。
因为这种隔离级别下,还会产生"不可重复读"、"幻读"这两种情况。
下面具体讲下:"不可重复读"是怎么产生的。这里引入一个概念:数据版本
- 事务A开始,读取id为1的value为’a’(当前数据版本是1)
- 事务B开始,将id为1的value更改为’aaa’,并提交(数据版本就变为了2)
- 事务A虽然已经获得了‘a’(数据版本1),但为了保险再读取一下数据,这个时候获得的却是’aaa’(数据版本2)
两次数据的读取结果不一致,这种情况让事务A根本无法确定哪个数据才是正确的。
不可重复读也就是指:读取一次结果没有问题,但是读取两次,在其他事务横插一脚的情况下,结果就可能存在不一致的情况。不可重复读取,不然矛盾就产生了。
让我们总结一下,不可重复读产生的关键点是什么?
- 事务A在操作id为1(数据版本为1)的数据,但在事务A的操作过程中,事务B进来操作,更改了数据版本为2!
- 事务A的最新select操作获取的是最新的版本数据(数据版本2)
在“读取提交内容”这一隔离级别下,因为提交内容即更新版本,且select操作一直都是获取最新版本的数据,导致杯具的发生。
3. 可重复读 (Repeatable Read)
那怎么解决这样一个问题呢?这个时候“快照读”参与了。它保证事务B对ID为1记录进行的修改“视而不见”。隔离级别“可重复读”现世了。它规定:在一个事务操作中,获取的数据只能是自己开始事务时的数据,select获取的也是快照(历史版本),不管其他事务怎么处理数据,只认准自己的数据。
让我们模拟下上面的操作:
- 事务A开始,读取id为1的value为’a’(当前数据版本是1)
- 事务B开始,将id为1的value更改为’aaa’,并提交(数据版本就变为了2)
- 事务A虽然已经获得了‘a’(数据版本1),但为了保险再读取一下数据,这个时候获取的是快照时的数据版本,也就是’a’(数据版本1)
虽然“不可重复读”被消灭了,但是还有一个大boss:“幻读”
"幻读"是怎么产生的?上面说了,在可重复读隔离级别下,一个事务处理的数据将是开始版本的数据,它并不会去考虑到其他事务的增删改操作。所以有了类似下面这样的情况:
- 事务A开始,获取到当前数据版本是1
- 事务B开始,新增一行数据id=4,value='d’到test表,尚未提交
- 事务A想要批量把test表中所有数据的value都改为’aaa’,执行了update操作
- 事务B提交,新增了数据
- 事务A提交,查看了一下数据,然后本来全表更改操作,却还有一条数据没有被改成’aaa’。不禁怀疑人生。
归根结底,幻读为什么产生?正正是因为它进行修改的数据时,事务B还未提交,那么对事务A来书,其他新增删的数据就是不存在的,是透明的,自然也就无法去进行修改,导致怀疑人生。
4. 可串行化 (Serializable)
事情既然到了这一步,那就不能忍了。既然事务A要修改数据是因为在这之后,其他事务(事务B)才进行了增删改操作,那么,就等到它完成操作,那么再去进行增删改,不就万无一失了吗?所以在这里,可串行化就规定了:当一个事务(事务A)进行了增删改操作,另一个正在运行中的事务(事务B)若要进行增删改操作,就必须等待事务A提交,才能再进行操作。
当然,select操作并不需要等待(还是按照上面说的快照版本方式处理)。
同样的,在可串行化隔离级别下,执行上面的操作:
- 事务A开始,获取到当前数据版本是1
- 事务B开始,新增一行数据id=4,value='d’到test表,尚未提交,上了表锁
- 事务A想要批量把test表中所有数据的value都改为’aaa’,执行了update操作,但是已经表锁,进入等待状态
- 事务B提交,新增了数据,解锁
- 事务A在解锁后继续执行update操作,之后提交,全表数据都执行了更改操作。
可串行化完美解决了脏读、不可重复读、幻读等问题,但是在事务没有被提交之前,其他的线程只能等到当前操作完成之后才能进行操作。这非常地且影响数据库的性能。
通常情况下,是不会使用可串行化隔离级别的。
三、设置隔离级别
既然隔离级别是事务的,那么它自然针对的也是支持事务操作的存储引擎,MyISAM是无缘的了,InnoDB成为其最大的受益者。
记录一下设置事务隔离级别相关的mysql语句:
# 查看当前所在数据库的事务隔离级别
select @@tx_isolation;
# 设置事务隔离级别为Read Uncommitted
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
# 设置事务隔离级别为Read Committed
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
# 设置事务隔离级别为Repeatable Read
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
# 设置事务隔离级别为Serializable
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
四、参考文献
扩展: