脏读/不可重复读的概念都比较容易理解和掌握,这里不在讨论
mysql 有四级事务隔离级别 每一个级别都有字符或数字编号mysql
读未提交 READ-UNCOMMITTED | 0:存在脏读,不可重复读,幻读的问题
读已提交 READ-COMMITTED | 1:解决脏读的问题,存在不可重复读,幻读的问题
可重复读 REPEATABLE-READ | 2:解决脏读,不可重复读的问题,存在幻读的问题,默认隔离级别,使用 MMVC机制 实现可重复读
序列化 SERIALIZABLE | 3:解决脏读,不可重复读,幻读,可保证事务安全,但彻底串行执行,性能最低
咱们能够经过如下命令 查看/设置 全局/会话 的事务隔离级别sql
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';
首先咱们要搞明白何谓幻读,目前网上的众多解释幻读的博文我的感受仔细设想一下就能找出推翻的例子,就像博文把 非阻塞IO 等同为 异步IO,而后好多文章都纷纷借用,其实这俩货是彻底不一样,非阻塞IO 是 同步IO 中的一种模式,并不是 异步IO。错误的观点都被大众认同的 "正确化" 了,扯远了,回归主题。安全
幻读会在 RU / RC / RR 级别下出现,SERIALIZABLE 则杜绝了幻读,但 RU / RC 下还会存在脏读,不可重复读,故咱们就以 RR 级别来研究幻读,排除其余干扰。session
注意: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 幻读的比较形象的场景(借用我在知乎上的回答):spa
table users: id primary key设计
事务T13d
事务T2code
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 隔离级别下,step一、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级别下只要对 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失败。
在此级别下,咱们便不须要对 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能够实现事务对某记录的预先占用,若是记录存在,它就是本事务的,若是记录不存在,那它也将是本是无的,只要本是无还在,其余事务就别想占有它。