MySQL 事务
标题是MySQL事务,实际上说的是InnoDB事务。因为MyISAM引擎并不支持事务。
事务的四大特性
- 原子性 Atomicity 事务是不可分割的,事务内的操作要么全部成功,要么全部回滚
- 一致性 Consistency 事务前后的数据一致性
- 隔离性 Isolation 事务之间是相互隔离的
- 持久性 Durability 事务一旦提交,它对数据库的改变就是永久的(而不是存入缓存,断电后会丢失)
MySQL中与事务相关的命令
- 事务开启:begin 或 start transaction
- 事务提交:commit
- 事务回滚:rollback
- 打开或关闭自动提交:set autocommit=0/1
事务隔离级别
| 隔离级别 | 出现脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交RU | ✅ | ✅ | ✅ |
| 读已提交RC | ❎ | ✅ | ✅ |
| 可重复读RR | ❎ | ❎ | 在innodb中❎ |
| 串行化 | ❎ | ❎ | ❎ |
参数:transaction_isolation,5.6之前是tx_isolation
- 读未提交 READ-UNCOMMITTED:可以读到另一个事务未提交的数据,实际上就是完全不隔离
- 读已提交 RED-COMMITTED:可以读到另一个事务已提交的数据
- 可重复读 REPEATABLE-READ:在自己的事务中,多次读取数据的结果是一致的,不管其他事务是否有提交
- 串行化 SERIALIZABLE:修改数据发现冲突时直接加锁,实际上就是完全隔离,不存在多个事务同时修改同一个数据的情况
事务隔离实现
InnoDB通过多版本并发控制机制 MVCC来实现RC和RR。
InnoDB中每开启一个事务,都会分配一个transaction id,这个id是不重复且严格递增的。
InnoDB中每一行记录允许有多个版本,每次更新会记录下对应的transaction id,记到row trx_id,版本更新的同时会生成undo log。
根据可重复读的定义,在事务中多次读取数据的结果一致,也就是说在事务启动那一刻开始,如果数据版本是在此之前创建的则可以读到,如果在此后创建的则无法读到。
为此InnoDB为每个事务创建了一个数组,用于保存在事务启动那一刻正在执行但还未提交的事务ID(这是一个快照),数组里ID的最小值称为低水位,当前系统中已经创建的事务ID的最大值+1称为高水位。这个数组+高低水位构成了当前事务的 一致性视图(read view)。
有了一致性视图,实现事务的可重复读就很简单:
- 如果某一行记录的row trx_id < 低水位,表示在当前事务创建之前就生成了,可以读到
- 如果row trx_id >= 高水位,表示是在将来启动的事务中生成,无法读到
- 如果row trx_id 在高低水位之间,则判断row trx_id是否在数组中。在数组中表示这个版本还未提交,不可见;不在数组中表示这个版本已提交,可见。还有一种情况是row trx_id=transaction id,表示是当前事务自己的更新,也可见。
- 根据上述3条,如果最新的row trx_id对应数据不可见,则根据undo log计算出历史版本,直到计算出可见版本
需要特别注意一致性视图的创建时间,理论上是在事务开启时创建,在具体实现上,对于begin或start transaction命令并不真正创建一致性视图,而是从这之后的第一个操作InnoDB表的SQL语句开始(包括select);如果需要立即启动事务并创建一致性视图,需使用命令start transaction with consistent snapshot
而在 读已提交(RC) 隔离级别下,一致性视图的创建时间是在每一条语句执行之前,其他并无区别。
一致性读和当前读
在可重复读隔离级别下,实现了事务中多次读取数据的一致性,但如果事务中存在更新操作是否有问题?假如A事务读取到字段值a=1,此时B事务更新a=a+1并提交,事务A再次修改a=a+1并提交,最后a的值是多少?
乍一看如果要遵守事务的可重复读,事务A不应受到事务B影响,读到a的值为1并更新为2,但这又与实际情况矛盾,因为事务B已经先把a更新为2了,我们希望的结果是a=3。
这里就要考虑 一致性读(consistent read) 和 当前读(current read) 的问题,假如事务A是只读事务,不更新a的值,则事务A中读到a的值始终为1,这叫一致性读;假如事务A涉及到a的更新操作,需注意更新操作都是先读后写,这里必须读到最新的当前值才能避免计算不出错,这叫做当前读。
回到上述问题,事务A对a进行了更新,用到了当前读,最终a的值是3。
需注意如果select语句加了锁也会变成当前读,例如以下sql:
mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;前者加了读锁,后者加了写锁,两者都可以实现当前读。
如何解决幻读问题
通常在可重复读隔离级别下会存在幻读问题,但InnoDB通过引入next-key lock来解决了这个问题。
读已提交和可重复读的异同
- 二者都有一致性视图,只不过RC的一致性视图是在执行每一条语句之前创建,RR是在事务中第一个操作InnoDB表的语句开始
- 引入next-key lock是为了解决RR级别下的幻读问题;RC级别下没有next-key lock和间隙锁
- RR级别遵从两阶段锁协议,需要到事务结束后释放锁资源;RC级别在执行完一个sql后会直接释放掉“不满足条件的行锁”
因此RC级别的锁范围更小,释放更快,可以支持更多并发。
事务查询
表:information_schema.innodb_trx