MySQL 事务
引言
事务是数据库确保数据并发安全的机制,或者说,事务设计初衷是让用户无需手动确保数据安全,将该问题交给数据库自身解决即可,而MySQL内部具备多种事务级别,用以实现各种情况下的事务隔离机制
事务的性质
- 如同索引机制的实现与引擎相关般,在MySQL中,事务的具体实现也根据不同引擎有所区分,而 InnoDB 是 MySQL 默认的、主流的、完整支持事务的存储引擎,
接下来介绍的事务机制均为 InnoDB 的行为 事务是原子的若事务未被提交(如mysqld异常挂掉,客户端异常结束等),数据会自动回滚至事务的开始位置
事务的提交形式
事务具备两种提交方式,分别为手动提交和自动提交,一般情况下,手动提交常用于需要确保多条SQL语句为原子性的情况,而自动提交则用于确保单SQL原子使用
-
手动提交:
- 使用begin; / start transaction;命令均可启动一个事务,之后在commit前的所有SQL语句都处于该事务中
- commit; 命令可提交事务
- savepoint save1; 命令可创建一个保存点(save1为参数,自定义命名即可)
- rollback to save1; 命令可回滚事务至保存点save1,若直接使用rollback; 命令可回滚事务至事务的开始位置
-
自动提交:
- 默认情况下自动提交是开启的,此时,单SQL就属于一个事务,会在单SQL开始前自动进行类似begin操作启动新事务,单SQL执行后自动使用"commit;"进行该语句的事务提交;若关闭自动提交,则会在所有SQL前启动新事务,用户手动commit后关闭该事务,即,
从效果上来看,关闭自动提交 + 多SQL + commit 与 begin + 多SQL + commit的行为是一致的,均可保证commit前的所有SQL共同的原子性 - 在MySQL中使用 show variables like 'autocommit';命令即可查询是否处于自动提交状态(OFF:关闭自动提交,ON:开启自动提交)
- 使用set autocommit = 0; 命令可关闭自动提交
- 使用set autocommit = 1; 命令可开启自动提交
- 默认情况下自动提交是开启的,此时,单SQL就属于一个事务,会在单SQL开始前自动进行类似begin操作启动新事务,单SQL执行后自动使用"commit;"进行该语句的事务提交;若关闭自动提交,则会在所有SQL前启动新事务,用户手动commit后关闭该事务,即,
因此,无论是否使用begin; / start transaction; 启动事务,所有的SQL都可确保在事务中进行
事务的隔离级别
事务的隔离级别指的是对于并发事务的操作行为,由于上面提到过"MySQL的InnoDB引擎中的所有SQL都是在事务中的",因此,事务的隔离级别可以简化理解为,多线程并发访问数据库的保护级别
隔离级别有4种,分别为:
- 读未提交(RU):
"读没有commit的内容",即并发事务访问同一数据时,一个事务在commit之前做的修改可以立即被另一事务看到 - 读提交(RC):
"读已提交内容",即并发事务访问同一数据时,一个事务中的修改在commit前不能被另一事务看到 - 可重复读(RR):
"在同一事务中,不同时刻使用相同select语句读取到的结果一定是相同的",即并发事务访问同一数据时,一个事务中的修改不能被另一事务看到,必须等待观测的事务commit后才能观测到另一事务的修改,注意,RR隔离级别为MySQL的默认隔离级别- 关于可重复读的使用场景,比如说,按对公司的贡献发工资,观测事务要执行两条select查询各个贡献的成员,有一个目标初始时符合第一条select,其贡献为B,然后在观测者事务执行第一条select后,出现新事务增加了目标的贡献,导致目标又符合第二条select,贡献为A,此时,若不可重复读就会导致为目标发放两份工资
- 串行化:
简单理解就是加互斥锁,确保事务串行操作,实际效果是,并发两个事务,两事务内部可同时使用select,效果为可重复读,但更改数据的操作只有一个事务可以执行,其他事务必须等待直至可更改事务commit
无锁实现前三种隔离级别的机制------------MVCC(多版本并发控制)
-
MVCC实际是为无锁解决并发读写冲突而诞生的,其原理为:
启动一个事务后,若尝试对某数据进行修改,则保存一份元数据快照,存储至MySQL内部一个内存块(undo log)中,然后创建一份更改后的新数据,并将其链接到元数据快照,若同一事务内多次更改同一条数据,则会形成链表结构......,而所谓的无锁解决读写冲突,其实就是对不同快照进行操作,这点会在下面详细讲解
-
实现MVCC的前置条件其一:其实,每次插入数据,MySQL都会在该条数据中添加一些
隐藏字段:
DB_TRX_ID:表示该条数据最新被插入/修改的事务IDDB_ROW_ID:隐式主键,在数据中不具备主键时自动添加,用于数据库快速索引DB_ROLL_PTR:回滚指针,也就是上述MVCC原理中提到的"链接"用指针
-
实现MVCC的前置条件其二:
Read View类cppclass ReadView { // 省略... private: /** 高水位,大于等于这个ID的事务均不可见*/ trx_id_t m_low_limit_id 比特就业课 /** 低水位:小于这个ID的事务均可见 */ trx_id_t m_up_limit_id; /** 创建该 Read View 的事务ID*/ trx_id_t m_creator_trx_id; /** 创建视图时的活跃事务id列表*/ ids_t m_ids; /** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG, * 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/ trx_id_t m_low_limit_no; /** 标记视图是否被关闭*/ bool m_closed; // 省略... }; // 重点字段: // m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID // up_limit_id; //记录m_ids列表中事务ID最小的ID(没有写错) // low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(也没有写错) // creator_trx_id //创建该ReadView的事务ID,事务ID一般递增分配,也就是事务越新越值越大解释:在单个事务中,首次进行快照读的时候,会创建一个ReadView对象,其中会记录当前事务ID,当前并发的事务ID列表,以及其中最小的事务ID和下一个将被分配的事务IO(
首次快照读创建,也可以说,事务的创建与Read View对象创建之间存在时间窗口,ReadView对象创建后,当前事务中,其记录的这四个值不会被变更)注:快照读,就是读快照(上述提到的链表中的非头节点)时,
可以理解为在RC/RR隔离级别下的正常select语句都是快照读(SELECT * FROM t WHERE id = 1FOR UPDATE;/SELECT * FROM user WHERE id = 1FOR SHARE;这种是指定读取最新的,这算是非正常的) -
无锁并发的具体实现规则:

每次读数据时,都会尝试从头遍历链表,针对链表节点中DB_TRX_ID分为下列情况:
- 小于up_limit_id:说明该事务在Read View创建前已经结束,则该节点的数据可见
- 大于low_limit_id:说明该事务在Read View创建后开始,则该节点的数据不可见
- 等于creator_trx_id:说明该操作就是本事务进行的,自然当前事务可见
(即,在可重复读下,观测事务修改数据,会造成select结果不同的情况,这是符合预期的,因为可重复读指的是不同事务间) - 当DB_TRX_ID位于up_limit_id和low_limit_id之间时,则需要判断该ID是否存在于m_ids列表中,若存在,则表明该事务在Read View前创建且创建后仍在执行,则该节点数据不可见;若不存在,则表明该事务在Read View创建前结束,则该节点数据可见(注:
m_ids列表中的ID可能不连续,因为会存在先开始的事务后结束以及后开始的事务先结束的情况)
MVCC实现RC/RR隔离级别
简而言之,上面描述的Read View对象仅在第一次快照读时创建之后不做修改就是RR(可重复读)隔离级别的实现原理,而RC的实现原理则为,每次快照读都创建新Read View对象,以此做到可以看到提交后的数据
补充知识点------幻读
在可重复读(RR)情况下,事务 A 新增的数据,在事务 B 的快照读 中不会被看到,因此 B 多次执行相同的 SELECT,不会因为事务 A 的 INSERT 而突然多出新的记录。
但在一般数据库的 RR 实现中,如果仅依靠普通行锁,很难解决其他事务 INSERT 导致的幻读问题。因为待插入的记录原本并不存在,对一条不存在的记录本身无法直接加行锁。
例如:
sql
-- 事务 B
BEGIN;
SELECT * FROM user WHERE age = 20;
-- 第一次查询得到 3 条记录
-- 事务 A
BEGIN;
INSERT INTO user VALUES (..., 20);
COMMIT;
-- 事务 B
SELECT * FROM user WHERE age = 20;
-- 如果能够看到事务 A 新插入的记录,则变成 4 条
事务 B 两次查询的结果中,出现了之前不存在的新记录,就好像产生了"幻觉",因此这种现象称为幻读(Phantom Read)。
不过,MySQL InnoDB 在 RR 隔离级别下,通过 MVCC + 锁机制 可以解决典型的幻读问题:
- 对于普通
SELECT快照读 :通过 ReadView 保证同一事务中的一致性视图,新提交的INSERT不会出现在当前事务的快照中。 - 对于
SELECT ... FOR UPDATE、SELECT ... FOR SHARE等当前读 :InnoDB 会通过 Next-Key Lock(记录锁 + 间隙锁) 锁住记录以及相应的间隙,阻止其他事务在范围内插入新的记录。
因此,可以简单理解为:
MySQL InnoDB 的 RR 级别能够解决典型的幻读问题。
oDB 会通过 Next-Key Lock(记录锁 + 间隙锁) 锁住记录以及相应的间隙,阻止其他事务在范围内插入新的记录。
因此,可以简单理解为:
MySQL InnoDB 的 RR 级别能够解决典型的幻读问题。