从头开始搞懂 MySQL(03)事务隔离性

Posted 一起来搬砖呀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从头开始搞懂 MySQL(03)事务隔离性相关的知识,希望对你有一定的参考价值。

1、什么是事务?

提到事务,我们肯定不陌生,和数据库打交道的时候,我们肯定会用到事物

经典案例就是转账,小孙要给小白转 100 块钱,现在小孙的卡里只有 100 块钱。转账过程具体到程序里会有一系列操作,查询余额、扣除 100、更新余额等,这些操作必须是一体的,不然在查询余额后,还没有扣除之前,借着这个时间差再查一次,然后再给另外一个朋友转账。这个时候就要用到事物这个概念了

事物就是要保证一组数据库操作,要么全部成功,要么全部失败,在 mysql 中,事物支持是在引擎层面实现的,并不是所有的引擎都支持事务,MyISAMy 引擎就不支持事务,我们常用的 InnoDB 支持事务,下面我们也会以 InnoDB 为例来加深我们对 MySQL 事务的理解

2、事务隔离性与隔离级别

提到事务,我们首先会想到事务的 ACID(Atomicty 原子性、 Consistency 一致性、 Isolation 隔离性、 Durability 持久性)

现在让我们来看看其中的 I,也就是隔离性

2.1 为什么要有隔离级别

数据库有多个事务同时执行的时候,就可能出现脏读(dirty read)、幻读(phantom read)、不可重复读(non-repeatable read)的问题,为了解决这些问题,就有了隔离级别的概念

2.2 事务隔离级别

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

  • 读未提交:一个事务还没有提交时,它做的变更就能被别的事务看到
  • 读提交:一个事务提交后,它做的变更才能被其他事务看到
  • 可重复读:一个事务执行过程中看到的数据,总是跟这个事务启动时看到的数据是一致的,未提交变更对其他事务也是不可见的
  • 串行化:对同一行记录,写会加写锁,读会加读锁,如果出现读写锁冲突,后访问的事务需要等待前一个事务执行完成后执行

2.3 不同隔离级别事务示例

首先有一张表 test,其中有一列 ID 的值为 1,按照下面时间顺序执行两个事务:

我们来看看在不同隔离级别下,事务 A 查询得到的值 V1、V2、V3 的返回值分别是什么

在实现上,使用 InnoDB 引擎数据库里面会创建视图,在 MVCC 中用到,在读提交和可重复读隔离级别下,SQL 访问的时候以视图的逻辑结果为准

读未提交

读未提交(read uncommitted)隔离级别下,会直接返回记录上的最新值,不会创建视图

事务 B 还未提交,但是结果已经被事务 A 看到了,所以 V1、V2、V3 的值都是 2

读提交

读提交(read committed)隔离级别下,视图在每个 SQL 执行的时候创建

V1 的值为 1,事务 B 提交之后事务 A 才看到,所以 V2、V3 的值为 2

可重复读

可重复读(repeatable read)隔离级别下,视图在事务启动的时候创建,事务存在期间都使用的同一张视图

事务在执行期间看到的值都是一致的,所以 V1、V2 的值都是 1,V3 的值为 2

串行化

串行化(serializable)隔离级别下,直接采用加锁的方式来防止并行访问

事务 B 在执行将值修改为 2 的时候,会被锁住,直到事务 A 提交后,事务 B 才能继续执行,所以 V1、V2 的值为 1,V3 的值为 2

配置事务隔离级别

查看当前数据库事务隔离级别 SQL 如下:

show variables like ‘transaction_isolation’;

2.4 事务隔离的原理

在 MySQL 中,每条记录在更新的时候都会在 undo log 里生成一条回滚操作,记录上的最新值,通过回滚操作,都可以得到前一个状态的值。

在 InnoDB 引擎中,每个事务都有自己唯一的事务 ID,transaction id,事务开始的时候向系统申请,每次事务更新都会有一个新的版本,每个版本有不同的事务 ID。如下图所示:

这是一条数据的多个版本,最新的版本是 V2,图中的 undo log 1 对应的就是回滚日志,里面的 transaction id 的值都是通过 undo log 计算出来的。

在可重复读的事务隔离级别中,每个事务启动的时候都只能看到已提交的事务,并且在事务执行过程中不能读取其他事务的更新操作。在 InnoDB 中,每个事务都有一组当前事务 ID 的快照,记录下来当前事务开启时,正在执行的事务 ID 的集合。

如果新增加一个事务,可能会出现以下几种情况:

  • 数据版本在事务开始前就已经提交了,当前版本对新增事务是可见的
  • 数据版本在事务开始后变更,当前版本对新增事务不可见
  • 数据版本的 transaction id 在数组中,是正在执行的事务,不可见
  • 数据版本的 transaction id 不在数组中,事务已提交,可见

2.5 MVCC 事务并发

2.5.1 快照读

查询创建的是版本小于等于当前事务的版本号,保证在读取之前记录存在,读取的是快照版本

2.5.2 当前读

  • 插入将当前事务的版本号保存到行的创建版本号,读取的是当前最新版本

  • 修改重新插入一行,用当前事务的版本号作为新的版本号,将原来记录行的删除版本号设置为当前事务版本号,读取当前最新版本

  • 删除将当前事务的版本号保存到行的删除版本号,读取当前最新版本

2.5.3 MVCC 多事务并发操作

数据表 test 中有一个字段 num 值为 100

如果有多个事务并发,事务 A(事务ID = 1)、B(事务 ID = 2)、C(事务 ID = 3)

查询:

  1. 事务 A(事务 ID = 1) 开始前,系统中只有已提交的事务,事务 A 查询字段 num
  2. 系统中总共有三个事务,事务 B 做更新操作 num = num + 1
  3. 事务 A 从可重复读的角度来看,事务 B 和事务 C 的版本 2、3 都不可见,所以查询结果 num = 100

更新:

  1. 事务 B 先开启事务进行更新操作 num = num + 1,事务 ID 集合中不包含 事务 C
  2. 事务 C 也进行更新操作 num = num + 1,并且在事务 B 之前进行提交
  3. 事务 B 的执行过程中,有事务 C 的提交流程,如果事务 B 进行回滚的话不能直接按照 undo log 回滚为 100,因为更新时为当前读,也就是最新的数据,事务 C 先提交后,事务 B 读取的是更新后的最新的数据 101,所以最后获取 num 的值的话应该是 101 + 1 = 102。

3、事务启动方式

显示启动事务

使用 begin 或者 start transaction 启动,使用 commit 进行提交,使用 rollback 进行回滚

关闭自动提交

使用 set autocommit = 0 关闭事务的自动提交,需要主动执行 commit 提交事务或者 rollback 进行回滚

为了避免长事务,最好使用 set autocommit = 1 显示语句的方式来启动事务

长事务查询
查询时间超过 60 秒的事务:

SELECT * FROM information_schema.innodb_trx where TIME_TO_SEC(timediff(now(), trx_started)) > 60;

以上是关于从头开始搞懂 MySQL(03)事务隔离性的主要内容,如果未能解决你的问题,请参考以下文章

Mysql基础篇之事务隔离---03

一文快速搞懂MySQL InnoDB事务ACID实现原理

从头开始搞懂 MySQL(05)行锁表锁全局锁

从头开始搞懂 MySQL(05)行锁表锁全局锁

MySQL 入门:事务隔离

MySQL 入门:事务隔离