MySQL事务

事务就是由单独单元的一个或多个sql语句组成,在这个单元中,每个sql语句都是相互依赖的。而整个单独单元是作为一个不可分割的整体存在(通俗的讲就是,事务就是一个整体,里面的内容要么都执行成功,要么都不成功。)

事务的四个特性(ACID)

  1. 原子性(Atomicity):指事务是一个不可分割的最小工作单位,事务中的操作只有都发生和都不发生两种情况。

    (通过undo log回滚日志来保证)

  2. 一致性(Consistency):事务必须使数据库从一个一致状态变换到另外一个一致状态,举一个栗子,李二给王五转账50元,其事务就是让李二账户上减去50元,王五账户上加上50元;一致性是指其他事务看到的情况是要么李二还没有给王五转账的状态,要么王五已经成功接收到李二的50元转账。而对于李二少了50元,王五还没加上50元这个中间状态是不可见的。

    (通过其他三个特性共同保证)

  3. 隔离性(Isolation):一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

    (通过MVCC保证)

  4. 持久性(Durability):一个事务一旦提交成功,它对数据库中数据的改变将是永久性的,接下来的其他操作或故障不应对其有任何影响。‘

    (通过redo log重做日志保证)

下面主要谈谈事务隔离性的相关内容。

事务并发问题与事务隔离级别

隔离性(Isolation):一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

事务并发问题

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题

  • 脏读 :A事务读取数据并且对数据进行了修改,这个修改对其他事务来说是可见的,即使当前事务没有提交(如果A事务回滚,导致修改数据没有提交,那其他事务读到的就是脏数据)。 此情况仅会发生在: 读未提交的的隔离级别.
  • 不可重复读 :在一个事务A中多次操作数据,在事务操作过程中(未最终提交), 事务B也才做了处理,并且该值发生了改变,这时候就会导致A在事务操作 的时候,发现数据与第一次不一样了。 就是不可重复读。 此情况仅会发生在:读未提交、读提交的隔离级别.
  • 幻读 :一个事务按相同的查询条件重新读取以前检索过的数据, 却发现其他事务插入了满足其查询条件的新数据,这种现象就称为幻读。(事务a 开启, 查询符合条件的数据 ,发现有10条, 准备将这10条记录修改, 此时事务b开启, 插入了一条符合事务a查询条件的记录. 提交事务, 回到事务a, a也提交事务, 当再次查询到时候, 发现修改了11条..感觉发生了幻觉一样. )此情况会发生在:读未提交、读提交、可重复读的隔离级别.

不可重复读和幻读的区别在于 :

  • 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
  • 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。

为了解决这些问题,就有了“隔离级别”的概念。

隔离级别

隔离级别越高,就隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。以下是SQL标准的隔离级别(由低到高):

  • 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交:一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读(InnoDB默认):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。即只能看到事务启动时的数据
  • 串行化:顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行

不同隔离级别能读到的值不同,如下面这张图 :

事务隔离的实现

如何实现事务隔离?

  • 对于「读未提交」隔离级别的事务来说,直接读取最新的数据就好了;

  • 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;

  • 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同。

    「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View

    「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。

数据库的多版本并发控制(MVCC):是一种并发控制的方法,一般在[数据库管理系统]中,实现对数据库的并发访问.

Read View

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。例如,数据库中的一个值从1被顺序改成了1,2,3,4 ,那么他的当前值就是4 ,在回滚段里就会保存将4改成3将3改成2将2改成1这3个read-view

Read View 有四个重要的字段 :

  • m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务
  • min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。
  • max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;
  • creator_trx_id :指的是创建该 Read View 的事务的事务 id

另外,聚簇索引中还会有两个隐藏列

  • trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里
  • roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。

可重复读的实现

  1. 事务A创建Read_View_A , 假设其creator_trx_id = 100

  2. 创建Read_View_B , 假设其creator_trx_id = 101

  3. 事务A读取这条记录的trx_id,发现 trx_id 为 99,比事务 A 的 Read View 中的 min_trx_id 值(100)还小,(这意味着修改这条记录的事务早就在事务 B 启动前提交过了,所以该版本的记录对事务 A 可见的),也就是事务 B 可以获取到这条记录。

  4. 事务 B 通过 update 语句将这条记录修改了(还未提交事务),将X改为2,这时 MySQL 会记录相应的 undo log,并以链表的方式串联起来,形成版本链

  5. 事务 A 第二次去读取该记录,发现这条记录的 trx_id 值为 101,在事务 A的 Read View 的 min_trx_id(100) 和 max_trx_id(102) 之间,

    则需要判断 trx_id 值是否在 m_ids (提交但未执行的事务id列表 这里是[101]) 范围内,判断的结果是在的,那么说明这条记录是被还未提交的事务修改的,这时事务 A 并不会读取这个版本的记录。

    而是沿着 undo log 链条往下找旧版本的记录,直到找到 trx_id 「小于」事务 A 的 Read View 中的 min_trx_id 值的第一条记录,所以事务 B 能读取到的是 trx_id 为 99 的记录,也就是X = 1的这条记录。

  6. 事物 B 提交事务,

  7. 由于隔离级别时「可重复读」,所以事务 B 再次读取记录时,还是基于启动事务时创建的 Read View 来判断当前版本的记录是否可见。所以,即使事物 B 将X = 2并提交了事务, 事务 A 第三次读取记录时,读到的记录都是X = 1的这条记录。

  8. 事务A提交事务

  9. 另一个事务查看X,这是读取X = 3