Skip to content

事务 ​

事务会保证一组数据库操作要么全都成功,要么全部失败。事务的支持是在引擎实现的。MyISAM 引擎不支持事务,InnoDB 引擎支持事务。

特性

  • 原子性 Atomicity
  • 一致性 Consistency
  • 隔离性 Isolation
  • 持久性 Durability

隔离性 ​

在多事务同时执行时,可能会出现脏读、不可重复读和幻读的问题:

  • 脏读:事务 B 在执行中读取到了事务 A 修改过但未提交的数据;
  • 不可重复读:事务 B 前后读取两次,在读第二次前事务 A 对数据进行了修改,导致事务 B 第二次读取时记录内容不一致;
  • 幻读:事务 B 前后读取两次,在第二次读取前事务 A 增加或删除了记录,导致事务 B 第二次读取时获得的记录数量不一致;

隔离级别 ​

MySQL 给出了四个隔离级别,通过隔离级别来解决上面的问题:

  • 读未提交:事务未提交时其所作更改就对其他事务可见;
    直接返回记录最新值,不使用阅读视图。
  • 读提交:事务提交后所做更改对其他事务可见;
    阅读视图在每条 SQL 开始执行时创建。
  • 可重复读:事务在执行过程中可见的数据与其启动时一致,未提交前所作更改其他事务不可见;
    阅读视图在事务启动时创建,在整个事务期间使用该视图。
  • 串行化:对同一记录根据需求加读锁和写锁,当锁冲突时需要等待前一事务执行完成后才能执行;
    使用加锁的形式避免冲突。

MySQL 会在读数据采用一致性读,即利用一致性阅读视图来实现。但在更新数据时,会采用当前读的方法来保证更新数据生效。
对于当前读来说, SELECT 语句可以加上 lock in share mode 或 for update 变成当前读。其中第一个是读锁(共享锁),第二个是写锁(排它锁)。

并发版本控制 ​

具体通过多版本并发控制 (MVCC) 来实现,不同时刻启动的阅读视图 (read-view).
在 MySQL 中,每条更新记录会同时记录一条回滚操作到 undo log 中,记录中的最新值可以通过回滚来回到之前的状态。例如一串操作:1 → 2, 2 → 3, 3 → 4,对应的 undo log 是:2 → 1, 3 → 2, 4 → 3.
假设在第一个更新创建了 read-view A, 在所有操作完成后创建了 read-view B. 对于 read-view A 来说,假如需要回到最原始的值 1,需要依次回滚所有操作;假如有新事务将当前值 4 修改为 5,这个操作对 read-view A,B 所在的事务不冲突。

示例:
一条记录 r,当前值为 1.
|事务 A|事务 B|读未提交|读提交|可重复读|串行化| |:--😐:--😐:--😐:--😐:--😐:--😐:--😐 |R(r)||A: 1|A: 1|A: 1|A: 1| ||R(r)|B: 1|B: 1|B: 1|B: 1| ||W(r): 2||||| |R(r)||A: 2|A: 1|A: 1|A: 1| ||Commit||||| |R(r)||A: 2|A: 2|A: 1|A: 1| |Commit|||||| |R(r)||A: 2|A: 2|A: 2|A: 2|

特别的

  • 不同数据库的默认隔离级别不同,Oracle 的默认隔离级别是读提交,而 MySQL 的默认隔离级别是可重复读。在使用由 Oracle 数据库迁移到 MySQL 数据库的应用时需要修改隔离级别。启动时设置启动参数 transaction-isolation 为 READ-COMMITTED.
    使用语句 SHOW VARIABLES LIKE 'transaction-isolation'; 查看当前的隔离级别。
  • 系统会在判断没有比当前 undo log 更早的 read-view 存在时删除该日志,所有不建议使用长事务。
    长事务会导致在该事务提交前所有回滚记录都必须保留,占用很大存储空间。同时它也会占有锁资源。

一致性读视图
阅读视图是基于整个库的。其基本逻辑是:

  • 对于每一个事物,拥有一个 transaction id, 它按申请顺序严格递增,在事务开始时从事务系统申请。
  • 每行记录有多个版本,每次事务更新时创建新版本。记录 row trx_id,内容为事务的 transaction id. 旧版本会被保留,新版本可以直接提取到信息。行旧记录版本不是物理存在,而是当前版本根据 undo log 计算得来的。
  • 事务数组
    InnoDB 为每个事务创建数组来保存事务启动瞬间当前活跃的所有事务的 ID (启动但未提交)
    • ID 最小值记为低水位;ID 最大值加 1 作为高水位;
    • 数组和高水位共同组成一致性阅读视图。
    • 数据版本可见性规则:
      所有事务:
      [已提交事务 [未提交事务集合] 未开始事务]
               ^       ↑      ^
             低水位  当前事务 高水位
      
      活跃事务数组:
      [低水位,……,高水位+1]
      以可重复读隔离级别为例:
      • 当 row trx_id 落在已提交事务,说明当前版本是已提交的事务或当前事务自己生成的。该数据可见;
      • 当 row trx_id 落在未开始事务,说明当前版本是将来事务生成的。数据不可见;
      • 当 row trx_id 落在未提交事务集合,则对比事务数组(因为序列会标记事务开始时已经提交事务为低水位,其后都标记为未提交或未开始。但这里面的事务有可能会在后续提交):
        • 若 row trx_id 在数组中,表示当前版本是还未提交事务生成。数据不可见;
        • 若 row trx_id 不在数组中,表示当前版本是已提交的事务生成的。该数据可见。

总结下来:一个记录的版本对于一致性阅读视图来说有:

  • 事务自己更新该记录,可见;
  • 版本未提交,不可见;
  • 版本已提交,在创建阅读视图后提交,不可见;
  • 版本已提交,在创建阅读视图前提交,可见。

启动方式 ​

  1. 显示启动。 使用 begin transaction 或者 start transaction 来启动,提交语句是 commit, 回滚语句是 rollback. 这种启动方式会在它之后第一个操作表的语句开启事务。如果想立即启动一个事务,使用 start transaction with consistent snapshot.
    在读提交级别下,start transaction with consistent snapshot 等同于 begin transaction, 因为它每个语句都会创建一个一致性阅读视图。
  2. 设置关闭线程自动提交。 使用 set autocommit=0 来关闭自动提交。当就算只执行一个 SELECT 语句时也会开启一个事务。这个事务只有当主动 commit 或 rollback,或者断开连接时才结束。

    特别地

    为避免有些客户端框架在连接成功后自动执行 set autocommit=0 的命令造成长连接带来的长事务,推荐使用 set autocommit=1 并用显示的方式启动事务。

提交并开启下一个事务 ​

在显示使用事务的时候,可以使用 commit workk and chain 命令来提交当前事务并开启下一个事务。这样可以减少执行一次 begin 语句的交互开销,也可以知道每个语句是否在事务中。

查找长事务
使用命令来查找时间超过 60 秒的事务:

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