MySQL 中的事务理解

科技资讯 投稿 8700 0 评论

MySQL 中的事务理解

    MySQL 中的事务
    • 前言
    • 原子性
    • 一致性
    • 持久性
    • 并发事务存在的问题
      • 脏读
      • 幻读
      • 不可重复读
    • 隔离性
        事务的隔离级别
    • 事务隔离是如何实现
        可重复读 和 读提交
      • 串行化
      • 读未提交
  • 可重复读解决了幻读吗
  • 总结
  • 参考

MySQL 中的事务

前言

A(Atomic,原子性:指的是整个数据库事务操作是不可分割的工作单元,要么全部执行,要么都不执行;

    数据的完整性: 实体完整性、列完整性(如字段的类型、大小、长度要符合要求)、外键约束等;

  • 如果事务执行过程中,每个操作失败了,系统可以撤销事务,系统可以撤销事务,返回系统初始化的状态。

D(durability, 持久性: 指的是一旦数据提交,对数据库中数据的改变就是永久的。即使发生宕机,数据库也能恢复。

原子性

在对数据库进行修改的时候,就会记录 undo log ,这样当事务执行失败的时候,就能使用这些 undo log 恢复到修改之前的样子。

undo log 进行事务的回滚,实际上做的是和之前相反的工作,对于每个 INSERT,InnoDB 会生成一个 DELETE ;对于 DELETE 操作,InnoDB 会生成一个 INSERT。通过反向的操作来实现事务数据的回滚操作。实现事务的原子性。

一致性

一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。

持久性

redo log 用来从保证事务的持久性。

redo log 简单点讲就是 MySQL 异常宕机后,将没来得及提交的事物数据重做出来。

redo log 包括两部分:一个是内存中的日志缓冲( redo log buffer,另一个是磁盘上的日志文件( redo log file

redo log buffer,后续某个时间点再一次性将多个操作记录写到 redo log file 。这种 先写日志,再写磁盘 的技术就是 MySQL 里经常说到的 WAL(Write-Ahead Logging 技术。

redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe

并发事务存在的问题

    脏读:读到其他事务未提交的数据;

  • 幻读:前后读取的记录数量不一致。

脏读

幻读

The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row。

简单的讲就是,幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。

不可重复读

在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据并修改数据。那么,在第一个事务的两次读数据之间。由于另一个事务的修改,那么第一个事务两次读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。

幻读和不可重复读的的区别

隔离性

事务的隔离级别

MySQL 中标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。

  • 读提交:一个事务提交之后,它的变更才能被其他的事务看到;

  • 串行化:这是事务的***别,顾名思义就是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读提交 不可能 可能 可能
可重复读 不可能 不可能 可能
串行化 不可能 不可能 不可能

可以看到,只有串行化的隔离级别解决了【脏读,不可重复读,幻读】这 3 个问题。

读提交 和 可重复读

create table user
(
id int auto_increment primary key,
username varchar(64 not null,
age int not null
;
insert into user values(2, "小张", 1;

来分析下下面的栗子

读未提交

读提交

可重复读

虽然事务1提交了,但是 V2 还是在事务2 中没有提交,根据可重复读的要求,一个事务执行的过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,所以 V2 也是 1。

串行化

事务隔离是如何实现

在了解了四种隔离级别,下面来聊聊这几种隔离级别是如何实现的。

Read View。

Read View 是一个数据库的内部快照,用于 InnoDB 中 MVCC 机制。

可重复读 和 读提交

undo log 日志版本链和 Read View

undo log 日志版本链

undo log 是一种逻辑日志,当一个事务对记录做了变更操作就会产生 undo log,里面记录的是数据的逻辑变更。

    trx_id:每次对某条聚簇索引记录进行改动时,都会把对应的事务 id 赋值给 trx_id 隐藏列;

每次事务更新的时候,undo log 就会用 trx_id 记录下当前事务的事务 ID,同时记录下当前更新的数据,通过 roll_pointer 指向上个更新的旧版本数据,这样就形成了一个历史的版本链。

Read View

undo log 版本链会将历史事务进行快照保存,并且根据事务的版本大小,通过指针串联起来,对于 可重复读 和 读提交 这两种事务隔离级别,只需要在 undo log 中选择合适的事务版本进行数据读取,就能实现对应的读取隔离效果。

undo log 版本链中,那个事务版本对当前事务可见,InnoDB 中通过 Read View来解决,作用是事务执行期间用来定义“我能看到什么数据”。

Read View 中有四个重要的字段

    Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。

  • Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。

  • Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;

  • Read View 的事务的事务 ID。

Read View 可以在理解为一个数据的快照,可重复读隔离级别会在每次启动的事务的时候生成一个 Read View 记录下当前事务启动瞬间,当前所有活跃的事务 ID。

Read View 的事务的事务 ID,会在 Read View 中根据事务 ID 大小,判断当前事务落在了那个区域,然后判断当前事务 ID 对应的数据快照是否可读。

  • 未开始事务:Read View 中应该给下一个事务的 ID,这部分的数据是不可见;

1、如果当前事务 ID 在未提交事务集合中,表示这个版本是由还没提交的事务生成的,不可见;

总结下来就是

Read View 时的事务 ID 会判断当前事务落在了 Read View 中那个区域中;

3、如果可读通过 undo log 版本链找到对应事务的快照数据,这就是目前该事物能够读到的数据;

undo log 版本链找到上个事务的版本,持续重复 1~3 的步骤,直到找到版本链中最后一个数据,如果最后一个版本的数据也是不可见,那就表示当前查询找不到记录。

可重复读 和 读提交 事务隔离级别的区别就在于创建 Read View 的时机不同,可重复读事务隔离级别会在每次启动事务的时候创建 Read View读提交 会在每次查询的时候创建 Read View

可重复读事务隔离级别在事务开始创建了 Read View,就能保证事务中的看到的数据一致了,而读提交事务隔离级别在每次查询的时候,创建 Read View,就能在每次查询的时候读到已经提交的事务数据。

可重复读隔离级别的读取过程

其中 V1 的查询结果是 3,V2 的查询结果是 1。

1、事务 1 开始前,系统里面只有一个活跃事务 ID 是 99;

3、三个事务开始前,id = 2 的 age 为 1 这一行数据的 trx_id 是 90。

Read View 分别是

id = 2 的 age 为 2。

id = 2 的 age 为 3。

Read View 中的未提交事务集合中,所以数据不可见,需要根据版本链寻找上一个版本。

trx_id=101,处于当前 Read View 的未开始事务中,所以数据不可见;

trx_id=102,同样处于当前 Read View 的未开始事务中,所以数据不可见;

trx_id=90,处于当前 Read View 的已提交事务中,所以数据可见;

1、版本未提交,不可见;

3、版本已提交,而且是在视图创建前提交的,可见。

数据更新

如果事务2在数据更新之前先去查询一次,那么看到的数据就是id = 2 的 age 为 1,但是更新数据,就不能在老的数据中更新了,否则事务3的更新数据就会丢失了。

这样更新的时候使用当前读,就保证了事务2拿到的最新的数据,所以更新完成之后的查询 id = 2 的 age 就为 3 了。

lock in share mode 或 写锁(X锁,排他锁)for update

select age from user where id=2 lock in share mode;

select age from user where id=2 for update;

如果当前事务3 的事务还没提交,这时候,事务2就开始了写入

什么是两阶段锁协议:在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。

而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

串行化

读写都需要加锁,读的时候加读锁,写的时候加写锁。

读未提交

对写仍需要锁定,策略和读已提交类似,避免脏写。

可重复读解决了幻读吗

针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。

select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

读操作,可重复读隔离级别会使用 MVCC 针对当前事务生成事务快照,通过对比事务版本,就能保证事务中的快照读。

Next-Key,MySQL 把行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key 锁。

Next-Key 算法中,对于索引的扫描,不仅仅是锁住扫描到的索引,而且还锁着这些索引覆盖的范围。因此对于范围内的插入都是不允许的,这样就能避免幻读的发生。

create table user
(
id int auto_increment primary key,
username varchar(64 not null,
age int not null
;
insert into user values(2, "小张", 1;
insert into user values(4, "小明", 1;
insert into user values(6, "小红", 1;
insert into user values(8, "小白", 1;

上面的这两个事务,事务1 中的 select * from where age>4 for updated 会加上一个间隙锁 (4,6] 这样事务 2 中的插入就需要等待事务1中锁释放才能提交修改。

不过可重读真的就完全避免了幻读吗?下面来看两种异常的情况。

场景1

for updated 当前读,这就会出现读取的结果和第一次读取结果的不一致。

场景2

上面两种场景的最终原因,就是后面的查询间接或直接的使用了当前读,造成了数据的不一致,所以只需要在最开始的查询加上 for updated,就能避免幻读的出现了。

总结

    读未提交:一个事务还没提交时,它的变更就能被别的事务看到,读取未提交的数据也叫做脏读;

  • 可重复读:MySQL 中默认的事务隔离级别,一个事务执行的过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,在此隔离级别下,未提交的变更对其它事务也是不可见的,此隔离级别基本上避免了幻读;

2、可重复读 是 MySQL 中默认的事务隔离级别;

undo log 日志版本链和 Read View

5、读未提交,读取最新的数据,读不用加锁,不用遍历版本链,直接读取最新的数据,不管这条记录是不是已提交。不过这种会导致脏读。对写仍需要锁定,策略和读已提交类似,避免脏写;

针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。

select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

参考

【高性能MySQL(第3版】https://book.douban.com/subject/23008813/
【MySQL 实战 45 讲】https://time.geekbang.org/column/100020801
【MySQL技术内幕】https://book.douban.com/subject/24708143/
【MySQL学习笔记】https://github.com/boilingfrog/Go-POINT/tree/master/mysql
【MySQL总结--MVCC(read view和undo log)】https://blog.csdn.net/huangzhilin2015/article/details/115195777
【深入理解 MySQL 事务:隔离级别、ACID 特性及其实现原理】https://blog.csdn.net/qq_35246620/article/details/61200815
【分布式事务】https://mp.weixin.qq.com/s/MbPRpBudXtdfl8o4hlqNlQ

编程笔记 » MySQL 中的事务理解

赞同 (41) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽