共计 5993 个字符,预计需要花费 15 分钟才能阅读完成。
说起MySQL事务就不得不提起,事务的基本属性原子性、隔离性、一致性和持久性。事务是一个抽象的概念,它其实对应着一个或多个数据库操作
脏写
脏写就是两个事务没提交的状况下,都修改同一条数据,结果一个事务回滚了,把另外一个事务修改的值也给撤销了,所谓脏写就是两个事务没提交状态下修改同一个值。
并发场景下多个事务(线程)同时写一条数据,失败的事务把成功的回滚了。
脏读
脏读就是一个事务修改了一条数据的值,结果还没提交呢,另外一个事务就读到了你修改的值,然后你回滚了,人家事务再次读,就读不到了,也就是说人家事务读到了你修改之后还没提交的值,这就是脏读了。
并发场景下,一个事务(线程)读,一个事务写,但是写事务回滚了,即读事务读到了写事务回滚前的值。
不可重复读
针对的是已经提交的事务修改的值,被事务A给读到了,事务A内多次查询,多次读到的是别的已经提交的事务修改过的值,这就导致不可重复读了。
并发场景下 一个读事务,多个写事务(成功)与幻读的前驱条件差不多,但是他是读具体的数据。幻读是读一批数据
幻读
幻读指的就是你一个事务用一样的SQL多次查询,结果每次查询都会发现查到了一些之前没看到过的数据
并发场景下,一个读事务,多个写事务(成功),导致读事务每次读的数据都不同
事务隔离级别
SQL
标准中的四个隔离级别包括了:read uncommitted
(读未提交),read committed
读已提交),repeatable read
(可重复读),serializable
(串行化)
RU
解决脏写,不允许脏写,也就是不允许两个事务同事更新一条数据。
但是还存在脏读、不可重复读、幻读的问题。
RC
解决脏写和脏读,其他事务未提交的数据,看不到,提交了可以看到。
但是还存在不可重复读、幻读的问题。
RR
解决脏写、脏读、不可重复读,RR隔离级别,只不过保证对同一行数据的多次查询,你不会读到不一样的值,人家已提交事务修改了这行数据的值,对你也没影响!每次读读到的数据都是一样的,但是还可能是幻读的。(MySQL的RR级别解决了幻读)
serializable
事务串行起来一个一个排队执行,一旦串行,数据库的并发可能就只有几十了,一般不会设置。
查询MySQL事务级别
- 当前会话隔离级别
select @@tx_isolation;
- 系统当前隔离级别
select @@global.tx_isolation;
level
的值可以是REPEATABLE READ
,READ COMMITTED
,READ UNCOMMITTED
,SERIALIZABLE
几种级别
- 设置当前会话隔离级别
set session transaction isolatin level repeatable read;
- 设置系统当前隔离级别
set global transaction isolation level repeatable read;
如果在服务器启动时想改变事务的默认隔离级别, 可以修改启动参数transaction-isolation
的值, 比方说在启动服务器时指定了--transactionisolation=SERIALIZABLE
, 那么事务的默认隔离级别就从原来的REPEATABLE
READ变成了SERIALIZABLE
。
MVCC机制
版本链
对于使用InnoDB
存储引擎的表来说, 它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的, 我们创建的表中有主键或者非NULL的
UNIQUE键时都不会包含row_id
列) :trx_id
: 每次一个事务对某条聚簇索引记录进行改动时, 都会把该事务的事务**id**
赋值给trx_id
隐藏列。roll_pointer
: 每次对某条聚簇索引记录进行改动时, 都会把旧的版本写入到undo
日志中, 然后这个隐藏列就相当于一个指针, 可以通过它来找到该记录修改前的信息。
- 案例说明
现在有两个事务对一张表中的同一条记录做更新操作,每次对记录进行改动, 都会记录一条**undo**
日志, 每条undo
日志也都有一个roll_pointer
属性(INSERT操作对应的undo日志没有该属性, 因为该记录并没有更早的版本) , 可以将这些undo
日志都连起来, 串成一个链表, 所以现在的情况就像下图一样:
时间编号 | 事务编号 | |
---|---|---|
100 | 200 | |
1 | BEGIN; | |
2 | BEGIN; | |
3 | update hero set name="关羽" where number=1 | |
4 | update hero set name="张飞" where number=1 | |
5 | COMMIT; | |
6 | update hero set name="赵云" where number=1 | |
7 | update hero set name="诸葛亮" where number=1 | |
8 | COMMIT; |
对记录每次更新后, 都会将旧值放到一条undo日志中, 就算是该记录的一个旧版本, 随着更新次数的增多, 所有的版本都会被roll_pointer
属性连接成一个链表, 这个链表称之为版本链, 版本链的头节点就是当前记录最新的值。 另外, 每个版本中还包含生成该版本时对应的事务id
ReadView
基本概念
使用READ COMMITTED
和REPEATABLE READ
隔离级别的事务, 都必须保证读到已经提交了的事务修改过的记录, 也就是说假如另一个事务已经修改了记录但是尚未提交, 是不能直接读取最新版本的记录的, 核心问题就是: 需要判断一下版本链中的哪个版本是当前事务可见的ReadView
中主要包含4个比较重要的内容
- m_ids: 表示在生成ReadView时,当前系统中活跃的读写事务的事务id列表。
- min_trx_id: 表示在生成ReadView时,当前系统中活跃的读写事务中最小的事务id, 也就是m_ids中的最小值。
- max_trx_id: 表示生成ReadView时,当前系统中应该分配给下一个事务的id值。
注意max_trx_id并不是m_ids中的最大值, 事务id是递增分配的。 比方说现在有id为1, 2, 3这三个事务, 之后id为3的事务提交了。 那么一个新的读事务在生成ReadView时, m_ids就包括1和2, min_trx_id的值就是1, max_trx_id的值就是4。 - creator_trx_id: **表示生成该ReadView的事务的事务id **
访问过程
在访问某条记录时, 按照下边的步骤判断记录的某个版本是否可见:
- 如果被访问版本的
trx_id
属性值等于ReadView
中的creator_trx_id
值, 意味着当前事务在访问它自己修改过的记录(修改者就是当前事务), 所以该版本可以被当前事务访问。 - 如果被访问版本的
trx_id
属性值小于ReadView
中的min_trx_id
值, 表明生成该版本的事务在当前事务生成ReadView
前已经提交, 所以该版本可以被当前事务访问。 - 如果被访问版本的
trx_id
属性值大于ReadView
中的max_trx_id
值, 表明生成该版本的事务在当前事务生成ReadView
后才开启(新开的事务), 所以该版本不可以被当前事务访问。 - 如果被访问版本的
trx_id
属性值在ReadView
的min_trx_id
和max_trx_id
之间,那就需要判断一下trx_id
属性值是不是在m_ids
列表中, - 如果在, 说明创建
ReadView
时生成该版本的事务还是活跃的, 该版本不可以被访问; - 如果不在, 说明创建
ReadView
时生成该版本的事务已经被提交, 该版本可以被访问
如果某个版本的数据对当前事务不可见的话, 那就顺着版本链找到下一个版本的数据, 继续按照上边的步骤判断可见性, 依此类推, 直到版本链中的最后一个版本。 如果最后一个版本也不可见的话, 那么就意味着该条记录对该事务完全不可见, 查询结果就不包含该记录
READ COMMITTED 机制
每次读取数据前都生成一个ReadView
,RC级别的隔离,只要数据被提交了,那就可以被访问,所以会发生不可重复读和幻读。
- 如何避免脏读
假设,数据有一条记录,是事务id为50 的事务插入的,并且现在活跃两个事务trx_id
分别为60,70,此时该记录的版本信息如图所示
现在事务B,发起UPDATE操作,更新了数据,同事修改trx_id=70,但还未提交事务,该记录的版本信息发生改变
若此时事务A,发起查询操作,则会创建以个ReadView
,该ReadView
里的信息为
m_ids=[60,70]
min_trx_id=60
max_trx_id=71
creator_trx_id=60
这个时候事务A发起查询,发现当前这条数据的trx_id
是70。这个事务id
在ReadView
的m_ids
范围内,说明在生成ReadView
之前这个事务(trx_id
=70)就是活跃的,是这个事务修改了这条数据的值,而且此时这个事务B还没提交,所以此时根据ReadView
的机制,此时事务A是无法查到事务B修改的值B的。
接着就顺着undo log
版本链条往下查找,就会找到一个原始值,发现他的trx_id
是50,小于当前ReadView
里的min_trx_id
,说明是他生成ReadView
之前,就有一个事务提交了,因此可以查到这个原始值,如下图。
注意:
- 如果事务B在A发起查询之前就提交了,那么
m_ids
列表中就不会有事务B的trx_id
- 如果事务A在第二次发起查询时,会重新生成一个
ReadView
, - 如果此时事务B已经提交了,那么
m_ids
中同样不会有B的trx_id
,那么事务A就可以查询B修改后的值(不可重复读) - 如果没提交,则还在
m_ids
列表中 - 如果事务B回滚了,那么事务A查询的值和B没有直接关系,避免了脏读
REPEATABLE READ 机制
在第一次读取数据时生成一个ReadView
,之后的查询就不会重复生成了
- 避免可不可重复度
假设,数据有一条记录,是事务id为50 的事务插入的,并且现在活跃两个事务trx_id
分别为60,70,此时该记录的版本信息如图所示
现在事务A,发起查询操作,第一次查询就会生成一个ReadView
,该ReadView
里的信息为
m_ids=[60,70]
min_trx_id=60
max_trx_id=71
creator_trx_id=60
这个时候事务A基于这个ReadView
去查这条数据,会发现这条数据的trx_id
为50,是小于ReadView
里的min_trx_id
的,说明他发起查询之前,早就有事务插入这条数据还提交了,所以此时可以查到这条原始值的。
事务B此时更新了这条数据的值为值B,此时会修改trx_id
为70,同时生成一个undo log
,而且关键是事务B此时他还提交了,也就是说此时事务B已经结束了,如下图所示。
因为RR级别只会在第一次的时候生成一个ReadView
,所以即使事务B已经提交,但是事务A的ReadView
还是使用之前的,即事务A的ReadView
的m_ids
仍然包含事务B的trx_id
=70(在事务A开启查询的时候,事务B当时是在运行的)
接着此时事务A去查询这条数据的值,他会惊讶的发现此时数据的trx_id
是70了,而70是在ReadView
的min_trx_id
和max_trx_id
的范围内的,同时还在m_ids
列表中,说明起事务A开启查询的时候,id为70的这个事务B还是在运行的,然后由这个事务B更新了这条数据,所以此时事务A是不能查询到事务B更新的这个值的,因此这个时候继续顺着指针往历史版本链条上去找
然后事务A顺着指针找到下面一条数据,trx_id
为50
,是小于ReadView
的min_trx_id
的,说明在他开启查询之前,就已经提交了这个事务了,所以事务A是可以查询到这个值的,此时事务A查到的是原始值,如下图
- 如何避免幻读
避免幻读与上述流程一样,事务A在发起查询时会生成一个ReadView
,记录此时系统中活跃事务的状态
- 如果在事务A查询过程中,有后加入进来的事务增加了记录(即执行了INSERT操作)并且提交了事务,那么事务A再次查询时,发现新增记录的
trx_id
大于当前ReadView
的max_trx_id
,则不会读取该记录,从而避免幻读 - 如果在事务A查询过程中,有后加入的事务,修改来了记录值,并且提交了,同样,事务A的
ReadView
的max_trx_id
小于该记录的trx_id
,也不会读取该记录的最新值,而是顺着版本链找一个合适的版本。
总结
所谓的MVCC(Multi-Version Concurrency Control, 多版本并发控制) 指的就是在使用READ COMMITTD
、 REPEATABLE READ
这两种隔离级别的
事务在执行普通的SEELCT操作时访问记录的版本链的过程, 这样子可以使不同事务的读-写、 写-读操作并发执行, 从而提升系统性能。 READ COMMITTD、 REPEATABLE READ这两个隔离级别的一个很大不同就是: 生成ReadView的时机不同, READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView, 而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView, 之后的查询操作都重复使用这个ReadView就好了