事物
事务(Transaction)是一组作为单个逻辑工作单元执行的SQL语句,它具有ACID特性:
- 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败,不能部分成功或部分失败
- 一致性(Consistency):事务执行前后,数据库的状态保持一致
- 隔离性(Isolation):多个事务并发执行时,每个事务的执行结果不受其他事务的影响
- 持久性(Durability):事务一旦提交,对数据库的修改将永久保存
事务隔离级别
并发控制时会出现问题
- 脏读:一个事务读取另一个事务未提交的数据
- 不可重复读:同一事务内多次读取数据时,数据发生变化
- 幻读:事务之间的数据插入或删除导致查询结果不一致
MySQL的隔离级别可以解决上述问题,支持四种隔离级别
-
读未提交(Read Uncommitted)
- 允许脏读、不可重复读和幻读
- 事务可以读取其他事务未提交的数据
-
读已提交(Read Committed)
- 解决了脏读问题,但仍然可能发生不可重复读和幻读
- 事务只能读取其他事务已提交的数据
-
可重复读(Repeatable Read)
- 解决了脏读和不可重复读问题,但仍然可能发生幻读
- 事务会对读取的数据加锁,保证该数据在事务内不会被修改
-
串行化(Serializable)
- 解决了脏读、不可重复读和幻读问题
- 对同一行记录,写操作会加锁,读操作会加共享锁,从而避免幻读
sql
-- 查看当前事务隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
-- 设置事务隔离级别
-- READ-UNCOMMITTED(最低级别)
-- READ-COMMITTED
-- REPEATABLE-READ(MySQL 的默认隔离级别)
-- SERIALIZABLE(最高级别)
SET GLOBAL transaction_isolation = 'REPEATABLE-READ';
-- 开启事务
START TRANSACTION;
-- 提交事务
COMMIT;
MVCC
在MySQL中,默认的隔离级别是可重复读,可以解决脏读和不可重复读的问题,但不能解决幻读问题。如果想要解决幻读问题,就需要采用串行化的方式,也就是将隔离级别提升到最高,但这样一来就会大幅降低数据库的事务并发能力。
主要解决了在高并发环境下的数据库读写冲突问题,它允许事务处理更加高效,同时提高了数据库的整体性能和可靠性
什么是快照读和当前读?
- 快照读读取的数据是快照数据,不加锁的简单的SELECT操作都属于快照读
- 当前读读取的数据是当前最新的数据,需要加锁的SELECT操作都属于当前读
快照读就是普通的读操作,而当前读包括了加锁的读取和DML操作
什么是 MVCC
MVCC(Multi-Version Concurrency Control,多版本并发控制),通过数据行的多个版本管理来实现数据库的并发控制,它的思想就是保存数据的历史版本
在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
MVCC实现原理
MVCC 的实现主要依赖于:隐藏字段、undo log
、Read View。
在内部实现中,InnoDB 通过数据行的 DB_TRX_ID 和 Read View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改
隐藏字段
每条行记录除了自定义之外,InnoDB 还维护了以下隐藏字段
sql
CREATE TABLE users (
id INT,
name VARCHAR(50)
-- 以下是InnoDB自动维护的隐藏字段
-- DB_TRX_ID -- 事务ID:记录最后一次修改该行的事务ID
-- DB_ROLL_PTR -- 回滚指针:指向上一个版本的记录
-- DB_ROW_ID -- 如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引
);
undo log
undo log
是用于事物回滚;当读取记录时,该记录被其他事物占用,则需要通过undo log
来找到之前的数据版本,实现非锁定读。undo log
主要分为两类:
- insert undo log:事务对
insert
操作的undo log
,insert
操作的记录只对事务本身可见,对其他事务不可见,事务提交后,该undo log
可以直接删除 - update undo log:事务对
update
操作的undo log
,事务提交后,该undo log
会被purge线程
删除
不同事物或者相同事物对同一行记录的update
或者delete
操作,会使该记录行的undo log
成为一条链表,链首就是最新的记录,链尾就是最早的旧记录
Read View
Read View 记录了当前事务能看到的版本,这些版本的数据基于事务开始时的快照。
Read View的结构,Read View主要是用来做可见行判断,主要字段有
- m_low_limit_id:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见
- m_up_limit_id:活跃事务列表 m_ids 中最小的事务 ID,如果 m_ids 为空,则 m_up_limit_id 为 m_low_limit_id。小于这个 ID 的数据版本均可见
- m_ids:Read View 创建时其他未提交的活跃事务 ID 列表。创建 Read View时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids 不包括当前事务自己和已提交的事务(正在内存中)
- m_creator_trx_id:创建该 Read View 的事务 ID
RC 和 RR 隔离级别下 MVCC 的差异
在事务隔离级别 RC 和 RR (InnoDB 存储引擎的默认事务隔离级别)下,InnoDB 存储引擎使用 MVCC(非锁定一致性读),但它们生成 Read View 的时机却不同
- 在 RC 隔离级别下,每次select查询会生成并获取最新的Read View(m_ids 列表)
- 在 RR 隔离级别下,同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View
MVCC 解决不可重复读问题
举个例子:
事物1 | 事物2 | 事物3 | |
---|---|---|---|
T1 | START TRANSACTION; | ||
T2 | START TRANSACTION; | START TRANSACTION; | |
T3 | update users set name='Job' where id=1; | ||
T4 | select * from users where id=1; | select * from users where id=1; | |
T5 | update users set name='Tom' where id=1; | ||
T6 | select * from users where id=1; | select * from users where id=1; | |
T7 | commit; | update users set name='Bill' where id=1; | |
T8 | select * from users where id=1; | ||
T9 | update users set name='Andrew' where id=1; | ||
T10 | commit; | select * from users where id=1; | |
T11 | commit; |
在RR隔离级别下
-
T2 时刻
-
T4 时刻
-
T6 时刻
- T8 时刻
- T10 时刻
在RC隔离级别下
- T2 时刻
- T4 时刻
- T6 时刻
- T8 时刻
- T10 时刻
// todo 按照 read view 分析 ...
MVCC➕Next-key-Lock 防止幻读
-
执行普通 select,此时会以 MVCC 快照读的方式读取数据
RR 隔离级别只会在事务开启后的第一次查询生成 Read View ,并使用至事务提交。所以在生成 Read View 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 "幻读"
-
执行 select...for update/lock in share mode、insert、update、delete 等当前读
当前读读取的是最新的数据,并且会对读取到的数据加锁,防止其他事务修改或删除该数据。在执行 insert、update、delete 操作时,会以当前读的方式读取数据,并加锁,防止其它事务在查询范围内插入数据,从而实现防止幻读