开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第12天,点击查看活动详情
一谈到事务,大家很快想到的就是事务的 ACID 属性,即:
- 原子性(A),指操作要么全做,要么全不做。
- 隔离性(I),指两个事务操作之间应该是隔离的,互不影响的。
- 一致性(C),指数据库中的数据全部符合现实世界中的约束,称其为符合一致性的。一致性是目的,原子性、隔离性是手段。
- 持久性(D),指对应的数据库操作所修改的数据都应该保留在磁盘上,不丢失。
01-事务实现方式
一个事务,可能存在如下一种状态:
- 活动状态,事务中的操作正在执行。
- 部分提交状态,事务中所有的操作都已执行完成,但尚未提交,此时操作的结果是保存在内存中的(未持久化到磁盘)。
- 失败状态,事务中的操作执行遇到错误。
- 中止状态,事务处于失败状态后,回滚所有操作后,事务进入到中止状态。
- 提交状态,部分提交状态将内存中的数据持久化到磁盘后(提交),事务变成提交状态。
InnoDB 中开启事务的方式有如下几种:
BEGIN ;START TRANSACTION [options];后可以跟几种参数:READ ONLY,只读事务,即事务中后续操作只读数据,不修改。READ WRITE,读写事务,操作可读、可写。WITH CONSISTENT SNAPSHOT启动一致性读视图,(这个在后面讲 MVCC 时会重点介绍)。
注:
BEGIN或START TRANSACTION并不会立即启动一个事务,而是等到第一个操纵表(增删改查)的语句,才真正开启一个事务。 或者,WITH CONSISTENT SNAPSHOT会立即创建一个一致性读视图,开启事务。
提交事务的方式有两种:
COMMIT ;语句来主动提交。autocommit = 1开启自动提交。如果没有特别通过上述方式开启事务,每个语句一个事务,在语句执行完毕后,自动提交。- MySQL 支持
COMMIT WORK AND CHAIN ;的写法,语义为“提交当前事务,并开启下一个事务”。
回滚事务的方式:
ROLLBACK ;来主动回滚事务中所有的操作。SAVEPOINT sp_name;搭配ROLLBACK TO sp_name ;可以在事务中设置保存点,并在遇到错误时,仅回滚到某个保存点。
MySQL 中,InnoDB 引擎支持事务,MyISAM 不支持事务。 这也是 InnoDB 称为默认数据引擎的一个重要原因。
01.1-事务 ID
前面我们介绍了事务的创建方式,可以通过 BEGIN 或 START TRANSACTION 的方式开启事务。
如果某个事务执行过程中对某张表执行了增、删、改操作,那么 InnoDB 数据引擎会为其分配一个独一无二的事务 ID,也就是记录中三个隐藏列之一的 DB_TRX_ID。
事务 ID 是如何生成的?或者说保存在哪里?如何实现事务 ID 递增的?
- InnoDB 在内存中维护一个全局变量,作为下一个要分配的事务 ID。当需要为某个事务分配事务 ID 时,赋予该值,并将该变量值递增加1。
- 当这个全局变量为256的倍数时,将其持久化到系统表空间,页号为5的页中 MAX_TRX_ID 中。
- 系统重启时,从磁盘中读取 MAX_TRX_ID 并增加256后赋予全局变量。 (之所以这样设计,避免在不到256倍数时,系统重启导致事务 ID 重复。 这样虽然会有最多256的浪费,但事务 ID 不会重复)
02-事务的隔离性
从之前的文章介绍中,我们了解到 MySQL 是一个典型的服务端、客户端应用,它们之间通过 TCP 进行连接。 连接建立后,称之为 Session(会话)。 每个客户端可以在一次会话中执行多个事务。 从服务端的角度,它需要同时处理多个事务,并且避免事务之间互相影响,导致客户端操作结果异常。
多个事务同时执行面临的问题(总之是数据不一致问题):
- 脏写,事务 A 修改了未提交事务 B 修改过的数据,当事务 B 回退后,事务 A 的修改消失了。
- 脏读,事务 A 读取了未提交事务 B 修改过的数据,当事务 B 回退后,事务 A 读到了一个不存在的数据。
- 不可重复读,事务 A 在执行期间,能够读到其他提交事务更改的内容。
- 幻读,事务 A 根据条件 Condition 查询出的记录,由于能读到其他以提交事务 B 的修改内容,导致 A 根据条件 Condition 查询出的内容变多。
这4种常见问题,它们的严重性依次降低。
前面也提到了,事务的隔离性是保证数据一致性的一种手段。 SQL 标准中的事务隔离级别:
- 读未提交,指:尚未提交的事务作出的变更,可以被其他事务看到。
- 读提交,指:已提交事务作出的变更,可以被其他事务看到。Oracle 的默认级别。
- 可重复读,指:事务执行过程中看到的数据,与事务开始时看到的数据是一致的。MySQL 中的默认级别。
- 串行化,指:对一条记录,读时加读锁,写时加写锁,读、写锁冲突时,后访问事务必须等待前一个事务完成。
从1->4,隔离级别逐渐升高,对应地,效率(应该指并行能力)也逐渐降低。 服务端还必须考虑执行事务的性能,如果按照 SQL 标准中的串行化隔离级别,服务器并行执行事务的能力就比较低。
如何查看当前的事务隔离级别?可通过执行如下命令: SHOW VARIABLES LIKE 'transaction_isolation%'。
隔离级别与上述问题之间的关系? 隔离级别越低,上述问题中越严重的越可能发生。
- 读未提交,脏读可能发生,不可重复读可能发生,幻读可能发生
- 读提交,脏读不可能发生,不可重复读可能发生,幻读可能发生
- 可重复读,脏读不可能发生,不可重复读不可能发生,幻读可能发生
- 串行,脏读不可能发生,不可重复读不可能发生,幻读不可能发生
注:脏写在上述隔离级别中均不允许发生。 Oracle 支持读提交和串行两种事务隔离级别。Oracle 中默认的事务隔离级别是读提交。 MySQL 支持全部4种事务隔离级别,但在可重复读上进行了优化,使幻读不可能发生。 MySQL 中可通过
transaction_isolation查看当前的事务隔离级别。
03-MVCC(多版本并发控制)
03.1-一致性读视图
InnoDB 中实现事务隔离级别中读已提交、可重复性读级别的支持,通过 MVCC,即多版本并发控制(multi-version concurrent control)实现。 MVCC 中的核心是一致性读视图(consistent read view),由以下部分组成:
- m_ids,生成 ReadView 时,活跃的读写事务的事务 id 组成的列表。
- min_trx_id,上述 m_ids 中的最小值,可表示为 min(m_ids)。 如果某个事务的事务 id 比当前值还小,说明肯定是已提交的事务。 为什么会这样,仔细看下 m_ids 的定义。(注,需要了解的是,事务 id 的大小与事务创建的时间相关的,id 越小,创建的越早)
- max_trx_id,生成 ReadView 时,系统中应该分配给下一个事务的事务 id。
- creator_trx_id,创建当前 ReadView 的事务的事务 id。
如何判断某条记录的版本链中,哪个版本对当前事务可见呢?
- 如果版本中的 DB_TRX_ID 与 creator_trx_id 相等,说明该版本是当前事务修改产生的,对当前事务可见;
- 如果 DB_TRX_ID < min_trx_id,说明该版本是一个已提交事务修改产生的,且在 ReadView 创建之前就已提交,当前事务可见。
- 如果 DB_TRX_ID >= max_trx_id,说明该版本是当前 ReadView 创建之后开启的事务修改产生的,对当前事务不可见。
- 如果 DB_TRX_ID 在 [min_trx_id, max_trx_id),则需要根据情况判断:
- 如果 DB_TRX_ID 在 m_ids 中,说明该版本是由创建当前 ReadView 时仍活跃的(未提交的)事务修改产生的,当前事务不可见;
- 否则,DB_TRX_ID 不在 m_ids 中,说明该版本是由创建当前 ReadView 时已提交的事务修改产生的,当前事务可见。
注:有些地方也称 max_trx_id、min_trx_id 为高、低水位
使用读提交隔离级别的事务,在每次查询开始时都会生成一个独立的 ReadView。 使用可重复读隔离级别的事务,第一次读取数据时创建一个 ReadView,整个事务执行期间都复用这一个 ReadView。 两者的区别在于:生成 ReadView 的时机不同。
一致性读视图的实现,涉及到一个称为 undo log 的日志,它也是事务回滚过程中需要使用到的日志文件。 undo log 会在后面的文章中详细介绍。
03.2-当前读
当前读读取的是聚簇索引上记录的最新版本,它可能是由其他事务插入或更新的(记录中的 DB_TRX_ID 表明了修改它的事务 ID)。
根据一致性读视图中的“高低水位”算法,当前事务又可能看不到这些更新。
但是像 SELECT ... FOR UPDATE;、SELECT ... LOCK IN SHARE MODE 或 UPDATE ... 等使用的是当前读(非一致性视图读),使能够读取、修改当前值而不管其 DB_TRX_ID 的值。
当前读读取的是聚簇索引上的记录内容。 快照读,是从聚簇索引上的记录内容开始,沿着记录中的 DB_ROLL_PTR(指向 undo log 中的记录)指向的链表,根据一致性读视图(快照、高低水位)判断哪些可见,哪些不可见。
更新语句(先查询再更新)或者加锁的查询语句,读取的都是当前读。