Skip to content

事务隔离

简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。

在 MySQL 中,事务支持是在引擎层实现的。你现在知道,MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。

比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。

事务问题

脏读

读到了别的事务 修改过 但未提交的数据

不可重复读

指的是变没变化的问题。数据被修改了导致前后两次查询结果不一样。

原来是 A,现在是 B,就是不可重复读。

幻读

指的是存不存在的问题,原来存在的,现在不存在了,就是幻读。

为什么可重复读无法解决幻读?

  • 范围查询问题:“可重复读”主要关注的是保护已存在的数据行不被修改或删除,但它没有机制来阻止其他事务在查询结果集所涵盖的范围内插入新行。这是因为范围查询的结果集是动态的,依赖于查询执行时的数据状态。

  • 当前读问题:在可重复读的隔离级别下,普通查询都是快照读,不会出现幻读的问题。

    在当前读的情况下,才会出现幻读的问题。

    比如事务中采用当前读的做法,for update。读最新数据是和快照数据不一样的。

MySQL 为了解决可重复读下的幻读问题,引入了间隙锁

事务隔离级别

  • 读未提交(read uncommitted)
  • 读已提交(read committed)
  • 可重复读(repeatable read)
  • 串行化(serializable )

详细解释

  • 读未提交是指:一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读已提交是指:一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读是指:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
  • 串行化:读的时候可以并发读,但是不能写。写的时候加排它锁,其它事务不能读也不能写。

mysql默认事务隔离级别是:REPEATABLE-READ(可重复读)


四种隔离级别在事务情况下的查询结果:

  • 读未提交

    事务A 读到了事务B 未提交的事务。

    v1 = 2;v2 = 2;v3=2;

  • 读已提交

    事务 A 只能读到事务B提交的事务。

    v1=1;v2=2; v3=2;

  • 可重复读

    事务提交前读到的数据保持一致。

    v1=1; v2=1; v3=2

  • 串行化

    v1=1; v2=1; v3=2;

实现逻辑

  • 读未提交

    访问数据的时候,直接访问记录上的最新值。

  • 读已提交

    创建视图,在整个事务期间,每次 SQL 开始执行时创建视图。(保证读取到其它事务的提交结果)

    SQL执行阶段的查询结果以视图结果为准。

  • 可重复读

    在事务开始时创建视图(保证整个事务阶段读取到的数据一致),整个事务执行阶段查询结果以视图结果为准。

  • 串行化

    在事务执行期间,对访问的记录直接加锁,避免其它事务并行访问。

查询隔离级别

sql
 select @@global.transaction_isolation;
sql
show variables like 'transaction_isolation';

使用场景

什么时候使用可重复读?

在数据校对的场景下,假如存在一张余额表,存在一张交易表。

此时需要根据交易表的本月交易量来判断余额。

如果在判断过程中,产生了新的交易,更新了这两张表,可能对判断结果产生影响。


可重复读不会出现问题,将整个判断余额过程中设置为一个事务提交。两张表的数据在事务开始阶段,通过创建视图的方式保存,整个事务过程都是静态的,不会受到其它更新操作的影响。

长事务问题

事务为了回滚,会保存数据到回滚日志(undo log)中,

当使用长事务的时候,它可能用到的回滚记录都会保存,导致占用磁盘空间过大。同时还会占用锁资源,影响整体使用。

查找数据库中持续时间超过60s的事务:

information_schema 库的 innodb_trx 表。

sql
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

避免长事务

业务方面:

  1. 减少事务中非必要事务,比如只读事务。
  2. 控制SQL执行时间,避免长时间执行。

数据库方面:

  1. 监控 information_schema 库的 innodb_trx 表。超出阈值报警。
  2. 借助第三方工具自动 kill 长事务。

MySQL事务隔离级别和实现原理(看这一篇文章就够了!)

一致性视图

可重复读的情况,在事务开始时,便会创建视图。这个视图有区分 MySQL 的视图,是一致性视图

并不会对当前数据库涉及到的数据打快照,而是通过逻辑上的判断,获取开始事务之前的数据。


通过区分数据的版本(transcation-id),保证自己获取的数据。

因此,可重复读情况下,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。

事务在启动的时候,会根据事务的情况将所有事务分为3组。

  • 当前活跃事务位于未提交事务集合
  • 当前活跃事务的最小值,作为低水位。小于低水位的都是已提交事务,可见。
  • 当前活跃事务的最大值+1,作为高水位。高于高水位的都是未开始事务,不可见。

一个数据的 row_txd_id 可能出现以下几种情况:

  1. row_txd_id < 低水位。

    可见。

  2. row_txd_id ≥ 高水位。

    不可见。

  3. row_txd_id 在黄色区域。

    • 如果 row_txd_id 在活跃事务集合里面,不可见(事务未提交)。
    • 如果 row_txd_id 不在活跃事务集合里面,代表虽然处于这个范围,但是事务不活跃(事务已经提交),可见。

当前读和快照读

在MySQL中,有两种读取数据的方式,分别是“当前读”和“快照读”。

“当前读”是指在查询过程中,读取最新的数据,这种读取方式可以确保读取到最新的数据,但可能会导致数据不一致或者读取的数据被其他事务修改而造成读取失败。

MySQL中的当前读包括以下两种方式:

  1. 读取共享锁(Shared Locks):通过SELECT … FOR SHARE 或 SELECT … LOCK IN SHARE MODE语句,获取共享锁来读取数据。这种方式会锁定所读取的数据,防止其他事务对该数据进行修改,但允许其他事务读取该数据。
  2. 读取排他锁(Exclusive Locks):通过SELECT … FOR UPDATE语句,获取排他锁来读取数据。这种方式会锁定所读取的数据,防止其他事务对该数据进行读取和修改,直到当前事务完成操作并释放锁为止。

需要注意的是,当前读虽然可以确保读取到最新的数据,但如果多个事务同时进行当前读取,可能会导致锁竞争和死锁等问题,因此需要谨慎使用。

另一种读取方式是“快照读”(Snapshot Read),它读取的是查询开始时的数据快照,可以避免锁竞争和死锁等问题,但可能会导致读取到的数据不是最新的。在MySQL中,通过设置事务隔离级别来控制当前读和快照读的行为。

update语句为什么使用当前读?

UPDATE 语句 默认使用 当前读 方式更新数据,在最新事务版本上进行更新。

由于 MVVC 机制,导致同一条数据具有多个版本,跟 row_txd_id 绑定。假如使用快照读去更新数据,会导致快照版本之后的记录消失,出现数据不准确和事务版本不一致的问题。


当前读加锁问题

更新时会对读取的记录加排他锁,防止其他数据更新和查询。

如果读取的表涉及到索引,会加索引锁,禁止更新和查询索引。


使用当前读更新数据时,可能导致锁竞争和死锁问题。如果多个事务同时处理相同数据,很有可能造成死锁。

比如相互之间持有对方事务需要的锁,就可能导致死锁的出现。


MVCC 总结

MySQL的MVCC(Multi-Version Concurrency Control)是一种并发控制技术,用于在并发环境下保证事务的一致性和隔离性。

InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己的一致性视图。普通查询语句是一致性读,一致性读会根据 row trx_id 和一致性视图确定数据版本的可见性。

  • 对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
  • 对于读提交,查询只承认在语句执行前就已经提交完成的数据;

而更新都是当前读,总是读取已经提交完成的最新版本,并将修改操作应用到该版本上。

MVCC主要包括以下几个组件:

  1. 事务标识符:每个事务都有一个唯一的事务标识符 row_txd_id,用于标识该事务的快照版本。
  2. 版本链:每个数据行都有一个版本链,用于保存该行的所有版本。版本链通常是一个双向链表,每个版本都包含前一个版本和后一个版本的引用。
  3. 回滚段:用于保存已提交的事务的快照版本,以便在事务回滚时使用。

在MySQL中,MVCC主要应用于InnoDB存储引擎中。通过MVCC技术,InnoDB可以实现读取已提交(Read Committed)和可重复读(Repeatable Read)两种事务隔离级别,并支持多版本并发控制,提供了较好的性能和稳定性。

undo日志版本链

undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段 trx_id 和 roll_pointer 把这些undo日志串联起来形成一个历史记录版本链。

活跃事务 Id 集合

最小的活跃事务 Id

< 最小的活跃事务 Id 5

已提交事务 可见 1,2,3,4 未提交的 不可见 17,18 不在活跃事务id集合里面的事务 可见的 6,7,8,9 活跃事务集合 不可见 自己 可见