MYSQL实现原理 - 事务的隔离级别
版本
版本 | 日期 | 说明 |
v1 | 2025-02-10 |
准备
为后续故事的顺利展开,这里创建一个账户表
create database test;
CREATE TABLE `test`.`account` (
`user_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(200) NOT NULL DEFAULT '' COMMENT '用户名称',
`amount` int unsigned NOT NULL DEFAULT 0 COMMENT '金额',
`created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户表';
事务
MySQL事务是数据库管理系统执行过程中的一个逻辑单位,它由一组在数据库中执行的操作构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位
示例
账户1和账户2进行100元交易
-- 开启事务
begin;
-- 账户1向账户2转100元
update test.account set amount = amount- 100 where user_id = 1 and amount > 100;
update test.account set amount = amount + 100 where user_id = 2;
-- 提交事务
commit ;
ACID特性
- 原子性(Atomicity):事务是一个原子操作单元,事务中的操作要么全部成功执行,要么全部失败回滚,以保证数据库状态的完整性。
- 一致性(Consistency):事务的执行必须使数据库从一个一致状态转换到另一个一致状态,满足所有业务规则和约束条件。
- 隔离性(Isolation):并发事务的执行不能相互干扰,每个事务在逻辑上都是独立的,以保证数据并发访问的正确性。
- 持久性(Durability):一旦事务提交成功,其对数据库所做的更改就是永久性的,即使系统发生故障也能恢复。
理论上在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样子的话对性能影响太大,我们肯定希望能最大可能的并行处理更多的事务,那么事务并发执行会引入哪些问题呐?
事务并发执行会遇到的问题
脏写(Dirty Write)
发生时间编号 | Session A | Session B |
---|---|---|
1 | begin; | |
2 | begin; | |
3 | update test.account set amount = 100 where user_id = 2; | |
4 | update test.account set amount = 200 where user_id = 2; | |
5 | commit ; | |
6 | rollback ; |
脏读(Dirty Read)
发生时间编号 | Session A | Session B |
---|---|---|
1 | begin; | |
2 | begin; | |
3 | update test.account set amount = 300 where user_id = 2; | |
4 | select amount from test.account where user_id = 2; 如果读到了amount = 300 则说明读到了脏读 | |
5 | commit ; | |
6 | rollback ; |
不可重复度(Non-Repeatable Read)
发生时间编号 | Session A | Session B |
---|---|---|
1 | begin; | |
2 | select amount from test.account where user_id = 2; 此时读到的 amount 值为 100 | |
3 | update test.account set amount = 300 where user_id = 2; | |
4 | select amount from test.account where user_id = 2; 如果此时读到的 amount 值为 300 ,说明发生了不可重复读 | |
5 | update test.account set amount = 400 where user_id = 2; | |
6 | select amount from test.account where user_id = 2; 如果此时读到的 amount 值为 400 ,说明发生了不可重复读 |
幻读(Phantom)
发生时间编号 | Session A | Session B |
---|---|---|
1 | begin; | |
2 | select count(*) from test.account where user_id > 0; 假设此时读到的数量为100 | |
3 | insert into test.account (user_name, amount) values ("张三", 0); | |
4 | select count(*) from test.account where user_id > 0; 如果此时读到的数量为101,说明发生了幻读 |
如上图,在Session A中开启事务后,进行一次查询,会返回此时的数据数量,之后通过Session B插入数据,然后再 通过 Session A 进行一次查询,如果次数读取到的数量包含了 Session B插入的数据,说明发生了幻读
有的同学就会疑惑了,在 Session A 的事务中,Session B新增了数据会导致幻读现象,那么如果删除了数据呐,是不是也算幻读呐?这里需要明确的是这种现象不叫幻读,幻读强调的是在一个事务中相同条件下多次读取,后者读到了前者没有读到的数据,针对的是新增记录情况。如果针对数据进行了删除操作,这是对每一条被删除的数据都发生了不可重复读现象
事务的隔离级别
以上几个事务并发问题按照严重程度来排序,是以下顺序:
脏写 > 脏读 > 不可重复读 > 幻读
针对事务并发执行遇到的这些问题,结合使用场景,舍弃部分隔离性,在sql标准中添加了四种隔离级别:
- READ UNCOMMITTED :未提交读。
- READ COMMITTED :已提交读。
- REPEATABLE READ :可重复读。
- SERIALIZABLE :可串行化。
隔离级别与事务并发问题可发生情况,具体如下:
隔离级别 | 脏写 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
READ UNCOMMITTED | F | T | T | T |
READ COMMITTED | F | F | T | T |
REPEATABLE READ | F | F | F | T |
SERIALIZABLE | F | F | F | F |
- READ UNCOMMITTED 隔离级别下,可能发生 脏读 、 不可重复读 和 幻读 问题。
- READ COMMITTED 隔离级别下,可能发生 不可重复读 和 幻读 问题。
- REPEATABLE READ 隔离级别下,可能发生 幻读 问题。
- SERIALIZABLE 隔离级别下,各种问题都不可以发生。
如何设置事务的隔离级别
通过SQL语句设置
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
其中的 level 可选值有4个:
level: {
REPEATABLE READ
| READ COMMITTED
| READ UNCOMMITTED
| SERIALIZABLE
}
GLOBAL、SESSION
这两个关键字可以对不同范围的事务产生不同的影响,也可以为空
GLOBAL
全局范围影响
用法:
SET GLOBAL TRANSACTION ISOLATION LEVEL level;
影响范围:
-
只对执行完该语句之后产生的会话起作用
-
当前已经存在的会话无效
SESSION
会话范围影响
用法:
SET SESSION TRANSACTION ISOLATION LEVEL level;
影响范围:
-
对当前会话的所有后续的事务有效
-
该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务
-
如果在事务之间执行,则对后续的事务有效
无关键词
只对执行语句的下一个事务产生影响
用法:
SET TRANSACTION ISOLATION LEVEL level;
影响范围:
-
对当前会话的下一个将开启的事务有效
- 下一个事务执行完,后续的事务将恢复默认隔离级别
- 该语句不能在已开启的事务中执行,会报错
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
ERROR 1568 (25001): Transaction characteristics can't be changed while a transaction is in progress
mysql>
服务器启动时设置
可以通过修改启动参数 transaction-isolation 的值 来修改mysql服务器的默认隔离级别。
比如启动时添加如下参数设置,那么MYSQL的默认隔离级别隔离级别就会从原来的
--transaction-isolation=SERIALIZABLE
通过设置系统变量修改
可以通过修改系统变量transaction_isolation的方式来设置事务的隔离级别,详情见:
https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_transaction_isolation
查看当前会话的默认隔离级别
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.02 sec)
mysql>
或者
mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)
mysql>
Innodb 不同事务隔离级别的实现方式
读未提交(Read Uncommitted)
- 在这种隔离级别下,
SELECT
语句不会加锁,因此可能会读取到未提交事务修改的数据,即“读脏”。 - 这是并发性最高但一致性最差的隔离级别,因为它允许读取到其他事务尚未提交的数据
串行化(Serializable)
- 在这种隔离级别下,所有的
SELECT
语句都会被隐式地转化为SELECT ... IN SHARE MODE
,这意味着如果有未提交的事务正在修改某些行,那么所有读取这些行的SELECT
语句都会被阻塞。 - 这是一致性最好的隔离级别,但并发性最差,因为它将事务完全串行化,从而避免了所有并发问题。
- 在这种隔离级别下,
UPDATE
和DELETE
操作也会与其他事务互斥,即同一时间只能有一个事务对这些数据进行修改。
可重复读(Repeated Read, RR)
- 这是InnoDB的默认隔离级别。
- 在这种隔离级别下,普通的
SELECT
语句使用快照读(snapshot read),这是一种不加锁的一致性读,底层使用MVCC来实现。 - 加锁的
SELECT
(如SELECT ... IN SHARE MODE
或SELECT ... FOR UPDATE
)、UPDATE
和DELETE
等语句的锁策略取决于查询条件:- 如果在唯一索引上使用唯一的查询条件,那么会使用记录锁(record lock),而不会封锁记录之间的间隔。
- 如果使用范围查询条件,那么会使用间隙锁(gap lock)和临键锁(next-key lock),以锁住索引记录之间的范围,避免范围间插入记录,从而避免产生幻影行记录和不可重复的读。
读提交(Read Committed, RC)
- 这是互联网最常用的隔离级别。
- 在这种隔离级别下,普通的
SELECT
语句也是快照读,但加锁的SELECT
、UPDATE
和DELETE
等语句的锁策略与可重复读有所不同:- 除了在外键约束检查和重复键检查时会封锁区间外,其他时刻都只使用记录锁。
- 这意味着其他事务的插入操作仍然可以执行,因此可能会导致读取到幻影记录。
总结
本篇讲述了引入事务隔离级别的原因,及事务隔离级别的查看和设置方式,事务隔离级别的底层实现逻辑描述需要更长篇幅,这里先做简述,后续会逐块展开讲解