MySQL的事务

假设A要转100块给B,会涉及到以下操作:

  1. 检查A的余额是否>=100
  2. A的余额减掉100
  3. B的余额加100

现在假设在执行到第2步的时候数据库崩溃了,那么A可能白白损失掉100块。这个问题就引出了一个解决方案:事务。

事务就是将一组操作打包看成一个整体,要么都成功,要么都失败。它具有以下特点:

再举个例子来理解ACID: 比如说,在事务中执行一个充值 100 元的交易,先记录一条交易流水,流水号是 888,然后把账户余额从 100 元更新到 200 元。对应的 SQL 是这样的:


mysql> begin;  -- 开始事务
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account_log ...;  -- 写入交易流水
Query OK, 1 rows affected (0.01 sec)

mysql> update account_balance ...;  -- 更新账户余额
Query OK, 1 rows affected (0.00 sec)

mysql> commit; # 提交事务
Query OK, 0 rows affected (0.01 sec)

我们来看一下,事务可以给我们提供什么样的保证?首先,它可以保证,记录流水和更新余额这两个操作,要么都成功,要么都失败,即使是在数据库宕机、应用程序退出等等这些异常情况下,也不会出现,只更新了一个表而另一个表没更新的情况。这是事务的原子性(Atomic)。事务还可以保证,数据库中的数据总是从一个一致性状态(888 流水不存在,余额是 100 元)转换到另外一个一致性状态(888 流水存在,余额是 200 元)。对于其他事务来说,不存在任何中间状态(888 流水存在,但余额是 100 元)。其他事务,在任何一个时刻,如果它读到的流水中没有 888 这条流水记录,它读出来的余额一定是 100 元,这是交易前的状态。如果它能读到 888 这条流水记录,它读出来的余额一定是 200 元,这是交易之后的状态。也就是说,事务保证我们读到的数据(交易和流水)总是一致的,这是事务的一致性 (Consistency)。

实际上,这个事务的执行过程无论多快,它都是需要时间的,那修改流水表和余额表对应的数据,也会有先后。那一定存在一个时刻,流水更新了,但是余额还没更新,也就是说每个事务的中间状态是事实存在的。数据库为了实现一致性,必须保证每个事务的执行过程中,中间状态对其他事务是不可见的。比如说我们在事务 A 中,写入了 888 这条流水,但是还没有提交事务,那在其他事务中,都不应该读到 888 这条流水记录。这是事务的隔离性 (Isolation)。最后,只要事务提交成功,数据一定会被持久化到磁盘中,后续即使发生数据库宕机,也不会改变事务的结果。这是事务的持久性 (Durability)。

InnoDB是通过事务日志来保证事务的持久性的。我们知道InnoDB在更新数据的时候,并不是马上修改磁盘里的真实数据,因为这样会设计到大量的随机I/O,而是先更新内存,然后写事务日志(redo log),这个日志是写到缓冲区里面的,由于它是写到缓冲区并且是顺序写的,所以性能很好,最后会在合适的时机把事务日志刷新到磁盘,这个时机可以通过innodb_flush_log_at_trx_commit来控制:

redo log保证了持久性,如果数据更新已经写入日志文件,即使数据还没有真正更新到磁盘上,数据库崩溃重启后,会根据redo log来做恢复。那么原子性如何来保证呢?如果有一个操作失败了是要做回滚的,答案是通过undo log。