事务并发
面试老被拷打事务,下决心搞懂它。
事务并发导致的问题详解
首先,我们需要知道事务并发带来什么问题。
先看有哪些问题,再看解释:
- 脏读: 事务A读取数据,事务B修改数据未提交,事务A去读取,然后事务B回滚, 事务A读到了不存在的数据,这个叫脏读。
- 不可重复读: 事务A读取数据,事务B修改数据并提交, 事务A再读取,前后读取获得的数据不一致
- 幻读: 事务A读取数据,事务B插入数据并提交,事务A再读取,前后数据不一致
为了演示,我创建了一个表book 如下:
下面依次讲解出现的几种问题:
脏读
下面是演示图,画的不好,见谅。
脏读在mysql数据库中的演示:
- 开启事务A(注意设置隔离级别为ru,默认为rr):
运行第一条select语句,查出结果。
- 开启事务B:
修改值
- 事务A再去读取:
读取到修改后的值
- 事务B回滚:
这个时候事务A读到的数据根本不存在。本来是小红书,但是读到了小黄书。
不可重复读
图示:
在数据库中的演示:
- 事务A第一次查询:
- 事务B去进行修改并提交:
- 事务A再去读取发现数据与第一次不一样:
幻读
图示:
在数据库中的演示:
- 事务A进行第一次查询:
- 事务B进行添加并提交事务:
- 事务A进行第二次读取:
我们发现事务A第一次读与第二次读获得的数据并不一致。
上面我演示了并发导致的问题,既然有问题,mysql是如何解决的呢?
事务并发问题及其解决(rr和rc隔离级别基于MVCC来解决)
这是一张基本的隔离级别与其对应的能解决的问题。
当然上面所说的都是宏观层次的设置隔离级别,现在我们去理解理解它的原理MVCC。
官网上有说过,innodb是一个多版本的数据库引擎,它是如何实现的呢?
mysql事务处理事务并发问题
官网有对这里的说明:
innodb是一个多版本存储引擎。它保留有关已更改行的旧版本的信息以支持事务功能,例如并发和回滚。
它会保存事务执行之前的状态,当事务回滚的时候它会执行undo日志,来恢复之前的数据。
它也会使用这个undo日志信息来,为了持久化的read,会构建一个更早版本的行,官网里这种操作的名字叫做: Consistent Nonlocking Reads(非阻塞读)。待会我们来具体讲这个。
当前读与快照读
什么是当前读和快照读? 在学习 MVCC 多版本并发控制之前,我们必须先了解一下,什么是 MySQL InnoDB 下的当前读和快照读?
当前读:
像select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁.
快照读:
像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
数据库演示一下(RR级别的)快照读:
- 事务A第一次读取book表
-
事务B去修改数据
-
事务A第二次读取:
明明已经修改为小黄书,但是读取到的还是小红书。
行添加隐藏字段
innodb对存储在数据库中的行都添加了一些隐藏字段:
DB_TRX_ID:保存最后一个修改row的事务的id,删除在内部被视为 更新行中特殊位设置为将其标记为删除(因为不是真实删除,所以删除回滚只用清除标记)。
DB_ROLL_PTR: 是一个指针,每次对记录进行修改,都会对应undo log,这个指针就会指向之前的undo log通过它可以找到之前的记录。
DB_ROW_ID: 行id, 当新行插入的时候自动生成row_id。
上面所解释的前两个隐藏id是可重复读的关键。
READVIEW
还有一个关键点read view:
具体的处理,取决于他们的隔离级别,能使用read view的隔离级别有 可重复读, 读已提交, 读未提交
ReadView中主要包含4个比较重要的内容,分别如下:
-
creator_trx_id ,创建这个 Read View 的事务 ID。 说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为 事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
-
trx_ids ,表示在生成ReadView时当前系统中活跃的读写事务的 事务id列表 。
-
up_limit_id ,活跃的事务中最小的事务 ID。
-
low_limit_id ,表示生成ReadView时系统中应该分配给下一个事务的 id 值。low_limit_id 是系 统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1, 2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时, trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。
MVCC 在 RC 隔离级别下的快照读,解决脏读
在RC隔离级别下,每次select快照读都会创建一个READVIEW对象。
-
如果当前访问事务的id等于creator_trx_id, 表明该事务对其进行了修改,因此该事务可以访问到修改后的数据, 这个过程就是在一个事务中对数据修改又查询,自己的事自己办,所以是可以访问到最新值的。
-
trx_id < min_trx_id, 当前读到的版本trx_id小于存活事务中最小的,表明该版本已经提交,是可以访问的。
-
trx_id > max_trx_id, 当前读到的版本trx_id大于存活事务中最大的,表明该版本是在该ReadView创建之后再提交的,不可访问。
-
max_trx_id >= trx_id >= min_trx_id, 去事务存活列表去寻找,如果不在,表明事务已经提交,(因为事务是依次递增的,在范围里那么它一定是刚提交或者未提交)可以访问,如果在里面,表明该事务还没提交,不可被访问。
上面出现不可访问的情况就去查上一个版本。
上面这几个步骤就实现了读已提交,解决了脏读的问题
MVCC在RR隔离级别下的快照读,解决不可重复读
只有在第一次快照读时才会创建READVIEW对象,即使一个事务里有十个select语句,也只会创建一个READVIEW对象。
因为每次读取都是使用的同一个READVIEW,所以读取永远和第一次读一样,解决了不可重复读问题。
当然,这个级别肯定也是解决了脏读的问题。
RR隔离级别的MVCC来解决部分幻读问题
事务A连续多次快照读,都会复用同一个READVIEW对象,不管事务B是新增还是修改,读取的还是第一次读取的数据,因此解决了幻读。
但是有一个特例: 当事务A第一次快照读之后,使用了当前读,下一次事务A使用快照读就会生成新的READVIEW对象,因此会读到脏数据。
用一个例子来演示一下。
使用当前读:
- 事务A第一次查询表
2. 事务B插入一条数据:
- 事务A进行当前读,修改数据(注意,这里必须要影响到新添加的数据,因为这样才会创建新的READVIEW):
- 事务A进行快照读:
根据上面的操作我们可以发现出现了幻读,表示RR级别的MVCC是解决不了这种情况的幻读的。
必须使用SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE。设置为串行化。 这样在读事务A未提交时添加事务B是执行不了的。
上面内容总结一波:
- mysql使用MVCC来解决了脏读,不可重复读,部分的幻读问题。
- RC其实是最常用的隔离级别,但是mysql默认是使用的RR隔离级别。
- 一般场景下,脏读是无法忍受的,因为会读取到脏数据,不可重复读和幻读一般对业务影响不大,因为他读到的是真实数据。可以根据具体业务来设置隔离级别。