MySQL事务详解

253次阅读
没有评论

共计 5993 个字符,预计需要花费 15 分钟才能阅读完成。

说起MySQL事务就不得不提起,事务的基本属性原子性、隔离性、一致性和持久性。事务是一个抽象的概念,它其实对应着一个或多个数据库操作

脏写

脏写就是两个事务没提交的状况下,都修改同一条数据,结果一个事务回滚了,把另外一个事务修改的值也给撤销了,所谓脏写就是两个事务没提交状态下修改同一个值。
并发场景下多个事务(线程)同时写一条数据,失败的事务把成功的回滚了。

脏读

脏读就是一个事务修改了一条数据的值,结果还没提交呢,另外一个事务就读到了你修改的值,然后你回滚了,人家事务再次读,就读不到了,也就是说人家事务读到了你修改之后还没提交的值,这就是脏读了。
并发场景下,一个事务(线程)读,一个事务写,但是写事务回滚了,即读事务读到了写事务回滚前的值

不可重复读

针对的是已经提交的事务修改的值,被事务A给读到了,事务A内多次查询,多次读到的是别的已经提交的事务修改过的值,这就导致不可重复读了。
并发场景下 一个读事务,多个写事务(成功)与幻读的前驱条件差不多,但是他是读具体的数据。幻读是读一批数据

幻读

幻读指的就是你一个事务用一样的SQL多次查询,结果每次查询都会发现查到了一些之前没看到过的数据
并发场景下,一个读事务,多个写事务(成功),导致读事务每次读的数据都不同

事务隔离级别

SQL标准中的四个隔离级别包括了:read uncommitted(读未提交),read committed读已提交),repeatable read(可重复读),serializable(串行化)

  1. RU

解决脏写,不允许脏写,也就是不允许两个事务同事更新一条数据。
但是还存在脏读、不可重复读、幻读的问题。

  1. RC

解决脏写和脏读,其他事务未提交的数据,看不到,提交了可以看到。
但是还存在不可重复读、幻读的问题。

  1. RR

解决脏写、脏读、不可重复读,RR隔离级别,只不过保证对同一行数据的多次查询,你不会读到不一样的值,人家已提交事务修改了这行数据的值,对你也没影响!每次读读到的数据都是一样的,但是还可能是幻读的。(MySQL的RR级别解决了幻读)

  1. serializable

事务串行起来一个一个排队执行,一旦串行,数据库的并发可能就只有几十了,一般不会设置。

查询MySQL事务级别

  1. 当前会话隔离级别

select @@tx_isolation;

  1. 系统当前隔离级别

select @@global.tx_isolation;
level的值可以是REPEATABLE READREAD COMMITTEDREAD UNCOMMITTEDSERIALIZABLE几种级别

  1. 设置当前会话隔离级别

set session transaction isolatin level repeatable read;

  1. 设置系统当前隔离级别

set global transaction isolation level repeatable read;
如果在服务器启动时想改变事务的默认隔离级别, 可以修改启动参数transaction-isolation的值, 比方说在启动服务器时指定了--transactionisolation=SERIALIZABLE, 那么事务的默认隔离级别就从原来的REPEATABLEREAD变成了SERIALIZABLE

MVCC机制

版本链

对于使用InnoDB存储引擎的表来说, 它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的, 我们创建的表中有主键或者非NULL的
UNIQUE键时都不会包含row_id列) :
trx_id: 每次一个事务对某条聚簇索引记录进行改动时, 都会把该事务的事务**id**赋值给trx_id隐藏列。
roll_pointer: 每次对某条聚簇索引记录进行改动时, 都会把旧的版本写入到undo日志中, 然后这个隐藏列就相当于一个指针, 可以通过它来找到该记录修改前的信息。

  • 案例说明

现在有两个事务对一张表中的同一条记录做更新操作,每次对记录进行改动, 都会记录一条**undo**日志, 每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性, 因为该记录并没有更早的版本) , 可以将这些undo日志都连起来, 串成一个链表, 所以现在的情况就像下图一样:

时间编号事务编号
100200
1BEGIN;
2BEGIN;
3update hero set name="关羽" where number=1
4update hero set name="张飞" where number=1
5COMMIT;
6update hero set name="赵云" where number=1
7update hero set name="诸葛亮" where number=1
8COMMIT;
MySQL事务详解

MySQL事务详解

对记录每次更新后, 都会将旧值放到一条undo日志中, 就算是该记录的一个旧版本, 随着更新次数的增多, 所有的版本都会被roll_pointer属性连接成一个链表, 这个链表称之为版本链, 版本链的头节点就是当前记录最新的值。 另外, 每个版本中还包含生成该版本时对应的事务id

ReadView

基本概念

使用READ COMMITTEDREPEATABLE READ隔离级别的事务, 都必须保证读到已经提交了的事务修改过的记录, 也就是说假如另一个事务已经修改了记录但是尚未提交, 是不能直接读取最新版本的记录的, 核心问题就是: 需要判断一下版本链中的哪个版本是当前事务可见的
ReadView中主要包含4个比较重要的内容

  1. m_ids: 表示在生成ReadView时,当前系统中活跃的读写事务的事务id列表
  2. min_trx_id: 表示在生成ReadView时,当前系统中活跃的读写事务中最小的事务id, 也就是m_ids中的最小值。
  3. 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。
  4. creator_trx_id: **表示生成该ReadView的事务的事务id **

访问过程

在访问某条记录时, 按照下边的步骤判断记录的某个版本是否可见:

  1. 如果被访问版本的trx_id属性值等于ReadView中的creator_trx_id值, 意味着当前事务在访问它自己修改过的记录(修改者就是当前事务), 所以该版本可以被当前事务访问。
  2. 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值, 表明生成该版本的事务在当前事务生成ReadView已经提交, 所以该版本可以被当前事务访问。
  3. 如果被访问版本的trx_id属性值大于ReadView中的max_trx_id值, 表明生成该版本的事务在当前事务生成ReadView后才开启(新开的事务), 所以该版本不可以被当前事务访问。
  4. 如果被访问版本的trx_id属性值在ReadViewmin_trx_idmax_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,
  5. 如果在, 说明创建ReadView时生成该版本的事务还是活跃的, 该版本不可以被访问;
  6. 如果不在, 说明创建ReadView时生成该版本的事务已经被提交, 该版本可以被访问

如果某个版本的数据对当前事务不可见的话, 那就顺着版本链找到下一个版本的数据, 继续按照上边的步骤判断可见性, 依此类推, 直到版本链中的最后一个版本。 如果最后一个版本也不可见的话, 那么就意味着该条记录对该事务完全不可见, 查询结果就不包含该记录

READ COMMITTED 机制

每次读取数据前都生成一个ReadView,RC级别的隔离,只要数据被提交了,那就可以被访问,所以会发生不可重复读和幻读。

  • 如何避免脏读

假设,数据有一条记录,是事务id为50 的事务插入的,并且现在活跃两个事务trx_id分别为60,70,此时该记录的版本信息如图所示

MySQL事务详解

MySQL事务详解

现在事务B,发起UPDATE操作,更新了数据,同事修改trx_id=70,但还未提交事务,该记录的版本信息发生改变

MySQL事务详解

MySQL事务详解

若此时事务A,发起查询操作,则会创建以个ReadView,该ReadView里的信息为
m_ids=[60,70]
min_trx_id=60
max_trx_id=71
creator_trx_id=60

MySQL事务详解

MySQL事务详解

这个时候事务A发起查询,发现当前这条数据的trx_id是70。这个事务idReadViewm_ids范围内,说明在生成ReadView之前这个事务(trx_id=70)就是活跃的,是这个事务修改了这条数据的值,而且此时这个事务B还没提交,所以此时根据ReadView的机制,此时事务A是无法查到事务B修改的值B的。
接着就顺着undo log版本链条往下查找,就会找到一个原始值,发现他的trx_id是50,小于当前ReadView里的min_trx_id,说明是他生成ReadView之前,就有一个事务提交了,因此可以查到这个原始值,如下图。

MySQL事务详解

MySQL事务详解

注意:

  1. 如果事务B在A发起查询之前就提交了,那么m_ids列表中就不会有事务B的trx_id
  2. 如果事务A在第二次发起查询时,会重新生成一个ReadView
  3. 如果此时事务B已经提交了,那么m_ids中同样不会有B的trx_id,那么事务A就可以查询B修改后的值(不可重复读)
  4. 如果没提交,则还在m_ids列表中
  5. 如果事务B回滚了,那么事务A查询的值和B没有直接关系,避免了脏读

REPEATABLE READ 机制

在第一次读取数据时生成一个ReadView,之后的查询就不会重复生成了

  • 避免可不可重复度

假设,数据有一条记录,是事务id为50 的事务插入的,并且现在活跃两个事务trx_id分别为60,70,此时该记录的版本信息如图所示

MySQL事务详解

MySQL事务详解

现在事务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的,说明他发起查询之前,早就有事务插入这条数据还提交了,所以此时可以查到这条原始值的。

MySQL事务详解

MySQL事务详解

事务B此时更新了这条数据的值为值B,此时会修改trx_id为70,同时生成一个undo log,而且关键是事务B此时他还提交了,也就是说此时事务B已经结束了,如下图所示。

MySQL事务详解

MySQL事务详解

因为RR级别只会在第一次的时候生成一个ReadView,所以即使事务B已经提交,但是事务A的ReadView还是使用之前的,即事务A的ReadViewm_ids仍然包含事务B的trx_id=70(在事务A开启查询的时候,事务B当时是在运行的)
接着此时事务A去查询这条数据的值,他会惊讶的发现此时数据的trx_id是70了,而70是在ReadViewmin_trx_idmax_trx_id的范围内的,同时还在m_ids列表中,说明起事务A开启查询的时候,id为70的这个事务B还是在运行的,然后由这个事务B更新了这条数据,所以此时事务A是不能查询到事务B更新的这个值的,因此这个时候继续顺着指针往历史版本链条上去找
然后事务A顺着指针找到下面一条数据,trx_id50,是小于ReadViewmin_trx_id的,说明在他开启查询之前,就已经提交了这个事务了,所以事务A是可以查询到这个值的,此时事务A查到的是原始值,如下图

MySQL事务详解

MySQL事务详解

  • 如何避免幻读

避免幻读与上述流程一样,事务A在发起查询时会生成一个ReadView,记录此时系统中活跃事务的状态

  1. 如果在事务A查询过程中,有后加入进来的事务增加了记录(即执行了INSERT操作)并且提交了事务,那么事务A再次查询时,发现新增记录的trx_id大于当前ReadViewmax_trx_id,则不会读取该记录,从而避免幻读
  2. 如果在事务A查询过程中,有后加入的事务,修改来了记录值,并且提交了,同样,事务A的ReadViewmax_trx_id小于该记录的trx_id,也不会读取该记录的最新值,而是顺着版本链找一个合适的版本。

总结

所谓的MVCC(Multi-Version Concurrency Control, 多版本并发控制) 指的就是在使用READ COMMITTDREPEATABLE READ这两种隔离级别的
事务在执行普通的SEELCT操作时访问记录的版本链的过程, 这样子可以使不同事务的读-写、 写-读操作并发执行, 从而提升系统性能。 READ COMMITTD、 REPEATABLE READ这两个隔离级别的一个很大不同就是: 生成ReadView的时机不同, READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView, 而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView, 之后的查询操作都重复使用这个ReadView就好了

正文完
 
yangleduo
版权声明:本站原创文章,由 yangleduo 2023-06-14发表,共计5993字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。