目录
- 事务隔离级别(tx_isolation)
- mysql怎么实现的可重复读
- 举例说明MVCC的实现
- MVCC逻辑流程-插入
- MVCC逻辑流程-删除
- MVCC逻辑流程-修改
- MVCC逻辑流程-查询
- 幻读
- 快照读和当前读
- 如何解决幻读
- RR级别下防止幻读
- SERIALIZABLE级别杜绝幻读
- 总结
事务隔离级别有四种,mysql默认使用的是可重复读,mysql是怎么实现可重复读的?为什么会出现幻读?是否解决了幻读的问题?
事务隔离级别(tx_isolation)
Read Uncommitted(未提交读)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。读取未提交的数据,也被称之为脏读(Dirty Read)。该级别用的很少。
Read Committed(提交读)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变,换句话说就是事务提交之前对其余事务不可见。这种隔离级别也支持不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select查询可能返回不同结果。
Repeatable Read(可重复读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题(mysql彻底解决了幻读问题?请往下看)。
Serializable(可串行化)
这是最高的隔离级别,它强制事务都是串行执行的,使之不可能相互冲突,从而解决幻读问题。换言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
mysql 有四级事务隔离级别 每个级别都有字符或数字编号
级别 | symbol | 值 | 描述 |
读未提交 | READ-UNCOMMITTED | 0 | 存在脏读、不可重复读、幻读的问题 |
读已提交 | READ-COMMITTED | 1 | 解决脏读的问题,存在不可重复读、幻读的问题 |
可重复读 | REPEATABLE-READ | 2 | mysql 默认级别,解决脏读、不可重复读的问题,存在幻读的问题。使用 MMVC机制 实现可重复读 |
序列化 | SERIALIZABLE | 3 | 解决脏读、不可重复读、幻读,可保证事务安全,但完全串行执行,性能最低 |
我们可以通过以下命令 查看/设置 全局/会话 的事务隔离级别
mysql> SELECT @@global.tx_isolation, @@tx_isolation;
+-----------------------+------------------+
| @@global.tx_isolation | @@tx_isolation |
+-----------------------+------------------+
| REPEATABLE-READ | READ-UNCOMMITTED |
+-----------------------+------------------+
1 row in set (0.00 sec)
# 设定全局的隔离级别 设定会话 global 替换为 session 即可 把set语法温习一下
# SET [GLOABL] config_name = 'foobar';
# SET @@[session.|global.]config_name = 'foobar';
# SELECT @@[global.]config_name;
SET @@gloabl.tx_isolation = 0;
SET @@gloabl.tx_isolation = 'READ-UNCOMMITTED';
SET @@gloabl.tx_isolation = 1;
SET @@gloabl.tx_isolation = 'READ-COMMITTED';
SET @@gloabl.tx_isolation = 2;
SET @@gloabl.tx_isolation = 'REPEATABLE-READ';
SET @@gloabl.tx_isolation = 3;
SET @@gloabl.tx_isolation = 'SERIALIZABLE';
在MySQL的众多存储引擎中,只有InnoDB支持事务,所有这里说的事务隔离级别指的是InnoDB下的事务隔离级别。
mysql怎么实现的可重复读
MVCC多版本并发控制(Multi-Version Concurrency Control)是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现读已提交和可重复读取隔离级别。
在《高性能MySQL》中对MVCC的解释如下
举例说明MVCC的实现
新建一张表test_zq如下
id | test_id | DB_TRX_ID | DB_ROLL_PT |
MVCC逻辑流程-插入
在插入数据的时候,假设系统的全局事务ID从1开始,以下SQL语句执行分析参考注释信息:
begin;-- 获取到全局事务ID
insert into `test_zq` (`id`, `test_id`) values('5','68');
insert into `test_zq` (`id`, `test_id`) values('6','78');
commit;-- 提交事务
id | test_id | DB_TRX_ID | DB_ROLL_PT |
5 | 68 | 1 | NULL |
6 | 78 | 1 | NULL |
可以看到,插入的过程中会把全局事务ID记录到列 DB_TRX_ID 中去
MVCC逻辑流程-删除
对上述表格做删除逻辑,执行以下SQL语句(假设获取到的事务逻辑ID为 3)
begin;--获得全局事务ID = 3
delete test_zq where id = 6;
commit;
执行完上述SQL之后数据并没有被真正删除,而是对删除版本号做改变,如下所示:
id | test_id | DB_TRX_ID | DB_ROLL_PT |
5 | 68 | 1 | NULL |
6 | 78 | 1 | 3 |
MVCC逻辑流程-修改
修改逻辑和删除逻辑有点相似,修改数据的时候 会先复制一条当前记录行数据,同事标记这条数据的数据行版本号为当前是事务版本号,最后把原来的数据行的删除版本号标记为当前是事务。
执行以下SQL语句:
begin;-- 获取全局系统事务ID 假设为 10
update test_zq set test_id = 22 where id = 5;
commit;
执行后表格实际数据应该是:
id | test_id | DB_TRX_ID | DB_ROLL_PT |
5 | 68 | 1 | 10 |
6 | 78 | 1 | 3 |
5 | 22 | 10 | NULL |
MVCC逻辑流程-查询
此时,数据查询规则如下:
- 查找数据行版本号早于当前事务版本号的数据行记录
也就是说,数据行的版本号要小于或等于当前是事务的系统版本号,这样也就确保了读取到的数据是当前事务开始前已经存在的数据,或者是自身事务改变过的数据 - 查找删除版本号要么为NULL,要么大于当前事务版本号的记录
这样确保查询出来的数据行记录在事务开启之前没有被删除
根据上述规则,我们继续以上张表格为例,对此做查询操作
begin;-- 假设拿到的系统事务ID为 12
select * from test_zq;
commit;
执行结果应该是:
id | test_id | DB_TRX_ID | DB_ROLL_PT |
5 | 22 | 10 | NULL |
这样,同一个事务中,就实现了可重复读。
幻读
首先我们要搞明白何谓幻读,目前网上的众多解释幻读的博文个人感觉仔细设想一下就能找出推翻的例子,就像博文把 非阻塞IO 等同为 异步IO,然后好多文章都纷纷借用,其实这俩货是完全不同,非阻塞IO 是 同步IO 中的一种模式,并非 异步IO。错误的观点都被大众认同的 “正确化” 了,扯远了,回归主题。
幻读会在 RU / RC / RR 级别下出现,SERIALIZABLE 则杜绝了幻读,但 RU / RC 下还会存在脏读,不可重复读,故我们就以 RR 级别来研究幻读,排除其他干扰。
注意:RR 级别下存在幻读的可能,但也是可以使用对记录手动加 X锁 的方法消除幻读。SERIALIZABLE 正是对所有事务都加 X锁 才杜绝了幻读,但很多场景下我们的业务sql并不会存在幻读的风险。SERIALIZABLE 的一刀切虽然事务绝对安全,但性能会有很多不必要的损失。故可以在 RR 下根据业务需求决定是否加锁,存在幻读风险我们加锁,不存在就不加锁,事务安全与性能兼备,这也是 RR 作为mysql默认隔是个事务离级别的原因,所以需要正确的理解幻读。
幻读错误的理解:说幻读是 事务A 执行两次 select 操作得到不同的数据集,即 select 1 得到 10 条记录,select 2 得到 11 条记录。这其实并不是幻读,这是不可重复读的一种,只会在 R-U R-C 级别下出现,而在 mysql 默认的 RR 隔离级别是不会出现的。
这里给出我对幻读的比较白话的理解:
幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。
什么是幻读,如下:
InnoDB实现的RR通过mvcc机制避免了这种幻读现象。
另一种幻读:
table users: id primary key
事务T1
事务T2
step1 T1: SELECT * FROM
users
WHEREid
= 1;
step2 T2: INSERT INTOusers
VALUES (1, ‘big cat’);
step3 T1: INSERT INTOusers
VALUES (1, ‘big cat’);
step4 T1: SELECT * FROMusers
WHEREid
= 1;
T1 :主事务,检测表中是否有 id 为 1 的记录,没有则插入,这是我们期望的正常业务逻辑。
T2 :干扰事务,目的在于扰乱 T1 的正常的事务执行。
在 RR 隔离级别下,step1、step2 是会正常执行的,step3 则会报错主键冲突,对于 T1 的业务来说是执行失败的,这里 T1 就是发生了幻读,因为 T1 在 step1 中读取的数据状态并不能支撑后续的业务操作,T1:“见鬼了,我刚才读到的结果应该可以支持我这样操作才对啊,为什么现在不可以”。T1 不敢相信的又执行了 step4,发现和 setp1 读取的结果是一样的(RR下的 MMVC机制)。此时,幻读无疑已经发生,T1 无论读取多少次,都查不到 id = 1 的记录,但它的确无法插入这条他通过读取来认定不存在的记录(此数据已被T2插入),对于 T1 来说,它幻读了。
其实 RR 也是可以避免幻读的,通过对 select 操作手动加 行X锁(SELECT … FOR UPDATE 这也正是 SERIALIZABLE 隔离级别下会隐式为你做的事情),同时还需要知道,即便当前记录不存在,比如 id = 1 是不存在的,当前事务也会获得一把记录锁(因为InnoDB的行锁锁定的是索引,故记录实体存在与否没关系,存在就加 行X锁,不存在就加 next-key lock间隙X锁),其他事务则无法插入此索引的记录,故杜绝了幻读。
在 SERIALIZABLE 隔离级别下,step1 执行时是会隐式的添加 行(X)锁 / gap(X)锁的,从而 step2 会被阻塞,step3 会正常执行,待 T1 提交后,T2 才能继续执行(主键冲突执行失败),对于 T1 来说业务是正确的,成功的阻塞扼杀了扰乱业务的T2,对于T1来说他前期读取的结果是可以支撑其后续业务的。
所以 mysql 的幻读并非什么读取两次返回结果集不同,而是事务在插入事先检测不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测读获取到的数据如同鬼影一般。
这里要灵活的理解读取的意思,第一次select是读取,第二次的 insert 其实也属于隐式的读取,只不过是在 mysql 的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。
不可重复读侧重表达 读-读,幻读则是说 读-写,用写来证实读的是鬼影。
快照读和当前读
出现了上面的情况我们需要知道为什么会出现这种情况。在查阅了一些资料后发现在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,不是数据库最新的数据。这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库最新版本数据的方式,叫当前读 (current read)。
- select 快照读
当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据。之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。 - 当前读
对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的版本号记录,写操作后把版本号改为了当前事务的版本号,所以即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。也正是因为这样所以才导致幻读。
Next-Key Lock即在事务中select时使用如下方法加锁,这样在另一个事务对范围内的数据进行修改时就会阻塞(为什么有共享锁会阻塞?不能在有共享锁的记录上加X锁):
如何解决幻读
在快照读情况下,mysql通过mvcc来避免幻读。
在当前读情况下,mysql通过X锁或next-key来避免其他事务修改:
- 使用串行化读的隔离级别
- (update、delete)当where条件为主键时,通过对主键索引加record locks(索引加锁/行锁)处理幻读。
- (update、delete)当where条件为非主键索引时,通过next-key锁处理。next-key是record locks(索引加锁/行锁) 和 gap locks(间隙锁,每次锁住的不光是需要使用的数据,还会锁住这些数据附近的数据)的结合。
select * from table where id<6 lock in share mode;--共享锁
select * from table where id<6 for update;--排他锁
RR级别下防止幻读
RR级别下只要对 SELECT 操作也手动加行(X)锁即可类似 SERIALIZABLE 级别(它会对 SELECT 隐式加锁),即大家熟知的:
# 这里需要用 X锁, 用 LOCK IN SHARE MODE 拿到 S锁 后我们没办法做 写操作
SELECT `id` FROM `users` WHERE `id` = 1 FOR UPDATE;
如果 id = 1 的记录存在则会被加行(X)锁,如果不存在,则会加 next-lock key / gap 锁(范围行锁),即记录存在与否,mysql 都会对记录应该对应的索引加锁,其他事务是无法再获得做操作的。
这里我们就展示下 id = 1 的记录不存在的场景,FOR UPDATE 也会对此 “记录” 加锁,要明白,InnoDB 的行锁(gap锁是范围行锁,一样的)锁定的是记录所对应的索引,且聚簇索引同记录是直接关系在一起的。
id = 1 的记录不存在,开始执行事务:
step1: T1 查询 id = 1 的记录并对其加 X锁
step2: T2 插入 id = 1 的记录,被阻塞
step3: T1 插入 id = 1 的记录,成功执行(T2 依然被阻塞中),T1 提交(T2 唤醒但主键冲突执行错误)T1事务符合业务需求成功执行,T2干扰T1失败。
SERIALIZABLE级别杜绝幻读
在此级别下,我们便不需要对 SELECT 操作显式加锁,InnoDB会自动加锁,事务安全,但性能很低
step1: T1 查询 id = 2 的记录,InnoDB 会隐式的对齐加 X锁
step2: T2 插入 id = 2 的记录,被阻塞
step3: T1 插入 id = 2 的记录,成功执行(T2 依然被阻塞中)
step4: T1 成功提交(T2 此时唤醒但主键冲突执行错误)T1事务符合业务需求成功执行,T2干扰T1失败。
总结
RR 级别作为 mysql 事务默认隔离级别,是事务安全与性能的折中,可能也符合二八定律(20%的事务存在幻读的可能,80%的事务没有幻读的风险),我们在正确认识幻读后,便可以根据场景灵活的防止幻读的发生。
SERIALIZABLE 级别则是悲观的认为幻读时刻都会发生,故会自动的隐式的对事务所需资源加排它锁,其他事务访问此资源会被阻塞等待,故事务是安全的,但需要认真考虑性能。
InnoDB的行锁锁定的是索引,而不是记录本身
,这一点也需要有清晰的认识,故某索引相同的记录都会被加锁,会造成索引竞争,这就需要我们严格设计业务sql,尽可能的使用主键或唯一索引对记录加锁。索引映射的记录如果存在,加行锁,如果不存在,则会加 next-key lock / gap 锁 / 间隙锁
,故InnoDB可以实现事务对某记录的预先占用,如果记录存在,它就是本事务的,如果记录不存在,那它也将是本事务的,只要本事务还在,其他事务就别想占有它。