MySQL的事务

Thu, May 6, 2021 阅读时间 1 分钟

事务的特性

事务就是一组原子性的SQL语句。这一组SQL语句要么全部执行成功,要么全部不执行。只要有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。事务处理系统需要包含以下四个特性(ACID):

  • 原子性(atomicity)

一个事务必须被看作是不可分割的最小工作单元。不可能执行成功其中的一部分操作,要么全部提交成功,要么全部失败回滚,不能存在部分成功部分失败的状态。

  • 一致性(consistency)

数据库总是从一个一致性的状态,转换到另一个一致性的状态。

这句话很不好理解,并且在其他很多场景下比如分布式系统中也有一致性的概念,但其实不太一样。数据库的一致性指的是要保证约束条件的一致。举个例子,比如某个字段有唯一性约束,那么在事务执行完成之后,该字段也应该还是唯一的,不能因为事务的执行导致约束发生变化,约束可以是数据库层面的,也可以是业务层面的,如果某个约束数据库无法保证的话,那么就需要我们在应用程序中给予保证。

  • 隔离性(isolation)

事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务所干扰,多个并发事务之间要相互隔离。这一点大部分数据库都没有完美遵守,比如MySQL就提供了四种隔离等级。

  • 持久性(durability)

一旦事务提交,那么所作的修改就会永久保存到数据库中。换句话说,事务所作的修改要持久化,即使数据库宕机也不会导致数据丢失。

事务隔离等级

关于上述的第三点,隔离性,实际上比较复杂。SQL标准中定义了四种隔离等级,规定了一个事务中所作的修改,哪些在事务内和事务间是可见的,哪些是不可见的。以下介绍一下四种隔离等级。

READ UNCOMMITTED(读未提交)

顾名思义,就是可以读到其他事务还未提交的内容。

在未提交读这个级别下,所有事务中的修改,即使没有提交,其他事务也可以看到,这就叫做脏读。显而易见,这个级别会导致很多问题,并且性能上来说,也没有比其他隔离级别好很多,所以几乎是没人用的。

READ COMMITTED(提交读)

顾名思义,和上一个等级的差别就在于,该隔离等级下,每个事务是能读取到当前已提交的事务所作的修改,即一个事务在提交之前,对其他事务都是不可见的。这个等级是大多数数据库的默认隔离等级,但不是MySQL的。该级别下存在不可重复读幻读的问题。

不可重复读问题:

如果在一个事务执行过程中,有另一个事务从未提交状态,转变为了提交状态。那么那个事务所修改的行就会发生改变。如果在当前事务中前后两次查询这个行,就有可能两次查到的信息不一致,这就是不可重复读的问题,这个问题MySQL通过可重复读的隔离级别解决。

幻读问题:

所谓幻读,就是指在某个事务中多次读取某个范围的记录时,另一个事务在该范围中执行了插入操作并提交,则该事务后续再次读取这个范围时会出现行数增加的问题,插入的行就叫做幻行,这就叫幻读。

MySQL通过MVCC(多版本并发控制)和间隙锁(Gap Lock)解决了幻读的问题。(MVCC相关内容可以查看InnoDB非锁定一致性读这篇文章,间隙锁的相关内容可以查看MySQL的锁这篇文章)

REPEATABLE READ(可重复读)

该隔离等级和提交读的差别在于,这个等级解决了不可重复读的问题,保证了在一次事务中多次读取同一行数据的结果是一致的。解决方法是,在第一次SELECT的时候缓存查询记录,然后在后续的SELECT中复用这个缓存(具体可以查看InnoDB非锁定一致性读这篇文章)。但是标准的RR隔离级别依然无法解决幻读的问题。如上所说这个问题MySQL通过间隙锁彻底解决了。

SERIALIZABLE(串行化)

最高的隔离级别。通过强制事务串行执行,可以避免前述的脏读、幻读、不可重复读的所有问题。可串行化隔离级别下会在每一行数据上都加锁,所以效率也是最低的。只有在非常需要保证数据的一致性的时候且可以接受没有并发的情况,才会采用该级别。


具体的事务隔离等级和存在的问题如下表:(注意以下是标准隔离等级带来的问题,MySQL中的RR隔离级别下解决了幻读的问题)

隔离级别 是否脏读 是否不可重复读 是否幻读 是否加锁
未提交读
提交读(RC)
可重复读(RR)
可串行化

MySQL的事务使用

配置方式

参数transaction_isolation用于配置MySQL的事务隔离等级,默认是RR的隔离级别。MariaDB和MySQL5.7以下版本用的是参数tx_isolation

MariaDB [(none)]> show variables like "tx_isolation";
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.001 sec)

事务的使用方式

显式启动事务,使用begin或者start transaction,要注意的是,执行了这两句之后直到第一个SQL语句执行时,这个事务才真正开始启动。如果要马上启动,可以用start transaction with consistent snapshot命令,即立刻生成一张快照,这样这个事务才真正启动了。

提交用commit,回滚用rollback

commit work and chain命令,意思是提交一个事务再开启一个新的事务,等价于commit + begin。

自动提交

设置set autocommit=0,这会将事务的自动提交关掉。

由于InnoDB的默认每条语句都是一个事务,意味着每执行一次语句,这个事务就会一直启动并且不会自动提交,直到主动执行commit或者rollback或者断开连接。因此最好还是设置autocommit=1

长事务

长事务指的就是执行时间很长的事务,事务长时间不提交会造成很多问题:

  1. 事务中的写操作还会给行添加X锁,并且只有事务结束后才能释放,导致很多其他事务都被阻塞住。
  2. 一个事务中的每次操作都会记录到undo log中,insert undo log可以在事务结束后就被删除,undo log过长也会占用很多内存和存储空间。
  3. 长事务还会导致主备延迟(这部分内容可以查看MySQL的复制这篇文章)。

所以应该避免长事务,下面是一些可以参考的建议

  1. 设置autocommit=1,单独的sql语句让他自动提交,避免因为忘了提交造成的长事务。
  2. 监控information_schema.Innodb_trx表,设置长事务阈值,超过就报警或者kill掉,可以使用percona的pt-kill工具杀会话。
  3. 对于高于MySQL5.6版本的数据库,可以设置innodb_undo_tablespaces=2或者更大的值,这个参数用来设置undo log回滚段的磁盘文件个数,如果不设置,那么undo log会保存到公共表空间ibdata中,如果真的出现大事务导致undo log回滚段过大,这样设置后清理起来也更方便。
  4. 在测试阶段要输出所有的general_log,分析日志行为提前发现问题。
  5. 确认是否有不必要的只读事务,有些框架会不管什么语句都用begin/commit包起来,但是有些只读的select语句其实并不需要像事务那样读取的,这种事务可以去掉。
  6. 业务连接数据库的时候,根据业务本身的预估,设置SET_MAX_EXECUTION_TIME的值,控制每个SQL语句执行的最长时间,避免单个语句意外执行时间太长导致的长事务。