MySQL事务

MySQL事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你既需要删除人员的基本资料,也要删除该人员相关的信息,如信箱、文章等等,这样,这些数据库操作语句就构成一个事务

  • 在MySql中只有使用了Innodb数据库引擎的数据库或表才支持事务
  • 事务处理可以用来维护数据库的完整性,保证成批的SQL语句要么全部执行,要么全部不执行
  • 事务用来管理insert、update、delete语句

一般来说,事务是必须满足四个条件(ACID):

  • 原子性(Atomicity):一个事务中的所有操作要么全部完成,要么全部不完成,不会在结束中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样
  • 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的数据必须完全符合所有的预设规则,这包含资料的精度,串联性以及后续数据库可以自发性地完成预定的工作
  • 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • 持久性(Durability):事务处理结束后,对数据的修改是永久的,即使系统故障也不会丢失。

在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。因此要显式地开启一个事务务须使用命令 BEGIN 或 START TRANSACTION,或者执行命令 SET AUTOCOMMIT=0,用来禁止使用当前会话的自动提交。

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

隔离性和隔离级别

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

  • 脏读(读取未提交数据)

    A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹,此时由于B事务更改了它,这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据,但事务B良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。

  • 不可重复读(前后多次读取,数据内容不一致)

    事务A在执行读取操作,由整个事务A比较大,前后读取同一条数据需要经历很长的时间 。而在事务A第一次读取数据,比如此时读取了小明的年龄为20岁,事务B执行更改操作,将小明的年龄更改为30岁,此时事务A第二次读取到小明的年龄时,发现其年龄是30岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读

  • 幻读(前后多次读取,数据总量不一致)

    事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。

SQL标准的事务隔离级别包括:

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

image-20210904144955378

  • 若隔离级别是”读未提交”:则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。
隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能

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

我们可以看到在不同的隔离级别下, 数据库行为是有所不同的。 Oracle数据库的默认隔离级别其实就是“读提交”, 因此对于一些从Oracle迁移到MySQL的应用, 为保证数据库隔离级别的一致,
你一定要记得将MySQL的隔离级别设置为“读提交”。配置的方式是, 将启动参数transaction-isolation的值设置成READ-COMMITTED。 你可以用show variables来查看当前的值。

事务隔离的实现

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

image-20210904150457737

当前值是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的库。 最终
只好为了清理回滚段, 重建整个库。

事务的启动方式

如前面所述, 长事务有这些潜在风险, 我当然是建议你尽量避免。 其实很多时候业务开发同学并不是有意使用长事务, 通常是由于误用所致。 MySQL的事务启动方式有以下几种:

  • 显式启动事务语句, begin 或 start transaction。 配套的提交语句是commit, 回滚语句是
    rollback。
  • set autocommit=0, 这个命令会将这个线程的自动提交关掉。 意味着如果你只执行一个
    select语句, 这个事务就启动了, 而且并不会自动提交。 这个事务持续存在直到你主动执行commit 或 rollback 语句, 或者断开连接

有些客户端连接框架会默认连接成功后先执行一个set autocommit=0的命令。 这就导致接下来的
查询都在事务中, 如果是长连接, 就导致了意外的长事务。

因此, 我会建议你总是使用set autocommit=1, 通过显式语句的方式来启动事务。