mysql幻读的详解、实例及解决办法
(原)
脏读/不可重复读的概念都⽐较容易理解和掌握,这⾥不在讨论
事务隔离级别(tx_isolation)
mysql 有四级事务隔离级别每个级别都有字符或数字编号
读未提交 READ-UNCOMMITTED | 0:存在脏读,不可重复读,幻读的问题
读已提交 READ-COMMITTED | 1:解决脏读的问题,存在不可重复读,幻读的问题
可重复读 REPEATABLE-READ | 2:解决脏读,不可重复读的问题,存在幻读的问题,默认隔离级别,使⽤ MMVC机制实现可重复读
序列化 SERIALIZABLE | 3:解决脏读,不可重复读,幻读,可保证事务安全,但完全串⾏执⾏,性能最低
我们可以通过以下命令查看/设置全局/会话的事务隔离级别
mysql> SELECT @@_isolation, @@tx_isolation;
+-----------------------+------------------+
| @@_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 @@_isolation = 0;
SET @@_isolation = 'READ-UNCOMMITTED';
SET @@_isolation = 1;
SET @@_isolation = 'READ-COMMITTED';
SET @@_isolation = 2;静默管理最新通知>openstack 入门科普
SET @@_isolation = 'REPEATABLE-READ';
SET @@_isolation = 3;
SET @@_isolation = 'SERIALIZABLE';
幻读
⾸先我们要搞明⽩何谓幻读,⽬前⽹上的众多解释幻读的博⽂个⼈感觉仔细设想⼀下就能出推翻的例⼦,就像博⽂把⾮阻塞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 时发现此记录已存在,⽆法插⼊,此时就发⽣了幻读。
这⾥给出 mysql 幻读的⽐较形象的场景(借⽤我在知乎上的回答):vspring
table users: id primary key
事务T1
事务T2
step1 T1: SELECT * FROM `users` WHERE `id` = 1;
step2 T2: INSERT INTO `users` VALUES (1, 'big cat');
step3 T1: INSERT INTO `users` VALUES (1, 'big cat');
step4 T1: SELECT * FROM `users` WHERE `id` = 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锁),其他事务则⽆法插⼊此索引的记录,故杜绝了幻读。
mysql怎么读英语
在 SERIALIZABLE 隔离级别下,step1 执⾏时是会隐式的添加⾏(X)锁 / gap(X)锁的,从⽽ step2 会被阻塞,step3 会正常执⾏,待 T1 提交后,T2 才能继续执⾏(主键冲突执⾏失败),对于 T1 来说业务是正确的,成功的阻塞扼杀了扰乱业务的T2,对于T1来说他前期读取的结果是可以⽀撑其后续业务的。
所以 mysql 的幻读并⾮什么读取两次返回结果集不同,⽽是事务在插⼊事先检测不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测读获取到的数据如同⿁影⼀般。
这⾥要灵活的理解读取的意思,第⼀次select是读取,第⼆次的 insert 其实也属于隐式的读取,只不过是在 mysql 的机制中读取的,插⼊数据也是要先读取⼀下有没有主键冲突才能决定是否执⾏插⼊。
不可重复读侧重表达读-读,幻读则是说读-写,⽤写来证实读的是⿁影。
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 的记录不存在,开始执⾏事务:redmond time
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锁
line heightstep2: 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可以实现事务对某记录的预先占⽤,如果记录存在,它就是本事务的,如果记录不存在,那它也将是本是⽆的,只要本是⽆还在,其他事务就别想占有它。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。