MySQL 事务

简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在 MySQL 中,事务是在引擎层实现的。虽然 MySQL 是支持多引擎的系统,但不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。

事务的属性

事务是由一组 SQL 语句组成的逻辑处理单元,事务具有以下 4 个属性,通常简称为事务的 ACID 属性:

  • 原子性(Atomicity)
    • 事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
  • 一致性(Consistent)
    • 事务的一致性定义基本可以理解为是事务对数据完整性约束的遵循。这些约束可能包括主键约束、外键约束或是一些用户自定义约束。事务执行的前后都是合法的数据状态,不会违背任何的数据完整性。
    • 当然这个含义中也隐含着对开发者的要求,就是不能写出错误的事务逻辑,比如银行的转账不能只加钱不减钱,这是应用层面的一致性要求。
  • 隔离性(Isolation)
    • 数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的『独立』环境执行。这意味着事务处理过程中的中间状态对外部是不可见的。
  • 持久性(Durable)
    • 事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

并发执行事务带来的问题

  • 脏读(Dirty Reads)
    • 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些『脏数据』,并据此做进一步的处理,就会与未提交的数据产生依赖关系。这种现象被形象地叫做『脏读』。
  • 不可重复读(Non-Repeatable Reads)
    • 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做不可重复读。
  • 幻读(Phantom Reads)
    • 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为幻读。

为了解决这些问题,就有了『隔离级别』这个概念。在谈隔离级别之前,你首先要知道,你隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。

数据库的隔离级别

SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。下面逐一解释:

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

不同隔离级别可能出现的问题如下表所示:

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

假设下面是按照时间顺序执行的两个事务。

我们来看看在不同的隔离级别下,事务 A 会有哪些不同的返回结果,也就是图里面 V1、V2、V3 的返回值分别是什么。

  • 若隔离级别是『读未提交』, 则 V1 的值是 2。这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都是 2。
  • 若隔离级别是『读提交』,则 V1 是 1,V2 的值是 2。事务 B 的更新在提交后才能被 A 看到。所以,V3 的值也是 2。
  • 若隔离级别是『可重复读』,则 V1、V2 是 1,V3 是 2。V2 之所以还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
  • 若隔离级别是『串行化』,则在事务 B 执行『将 1 改成 2』的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2。

我们可以看到在不同的隔离级别下,数据库行为是有所不同的。Oracle 数据库的默认隔离级别是『读提交』,而 MySQL 数据库的默认却是『可重复读』。因此对于一些从 Oracle 迁移到 MySQL 的应用,为保证数据库隔离级别的一致,你一定要记得将 MySQL 的隔离级别设置为『读提交』。

事务隔离的实现

在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。 在『可重复读』隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在『读提交』隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。这里需要注意的是,『读未提交』隔离级别下直接返回记录上的最新值,没有视图概念;而『串行化』隔离级别下直接用加锁的方式来避免并行访问。

MVCC

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。

当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如上图所示,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作。同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。

你一定会问,回滚日志总不能一直保留吧,什么时候删除呢?答案是,在不需要的时候删除。那什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的 read-view 的时候。因此,建议你尽量不要使用长事务。长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以在这个事务提交之前,数据库里面它可能用到的回滚日志都必须保留,这就会占用大量的存储空间。

在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。

引用