前置准备:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_code
-- ----------------------------
DROP TABLE IF EXISTS `t_code`;
CREATE TABLE `t_code` (
`id` bigint(20) NOT NULL,
`trade_code` varchar(255) DEFAULT NULL,
`is_used` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of t_code
-- ----------------------------
BEGIN;
INSERT INTO `t_code` VALUES (0, '0', 0);
INSERT INTO `t_code` VALUES (1, '111', 1);
INSERT INTO `t_code` VALUES (2, '222', 0);
INSERT INTO `t_code` VALUES (3, '333', 1);
INSERT INTO `t_code` VALUES (4, '444', 0);
INSERT INTO `t_code` VALUES (5, '555', 0);
INSERT INTO `t_code` VALUES (6, '666', 0);
INSERT INTO `t_code` VALUES (7, '777', 1);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
📝 RR导致幻读的实例
我们知道可重复读会导致幻读,那到底是哪种情况导致的呢?
1.一个事务更新、插入都不会引起另一个事务幻读

InnoDB实现的RR通过mvcc机制避免了这种幻读现象。
2.一个事务插入,另一个事务范围更新会引起幻读
2.1 A线程开启事务,并进行条件查询
begin;
select * from t_code where is_used = 1;结果:

2.2 B线程开启事务,新增一条数据,并提交事务
begin;
insert into t_code value(8,888,1);
commit;结果:

2.3 A线程如果这时候去查数据还是正确的,并不会产生幻读

2.4 但是当A线程进行一次update操作后,便会产生幻读的情况
update t_code set trade_code = "used" where is_used = 1;
select * from t_code where is_used = 1;结果:

如果你在B线程insert以后还未commit事务时,就让A线程去update,这时候A线程是会被卡主的。
那为什么update以后会导致幻读呢?
🤗 当前读和快照读
因为update的where操作是发起了一次当前读!!!
出现了上面的情况我们需要知道为什么会出现这种情况。在查阅了一些资料后发现在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的时候需要知道最新的数据。也正是因为这样所以才导致幻读。
🤥 如何解决幻读
在快照读情况下,mysql通过mvcc来避免幻读。
在当前读情况下,mysql通过X锁或next-key来避免其他事务修改:
- 使用串行化读的隔离级别
- (update、delete)当where条件为主键时,通过对主键索引加record locks(索引加锁/行锁)处理幻读。
- (update、delete)当where条件为非主键索引时,通过next-key锁处理。next-key是record locks(索引加锁/行锁) 和 gap locks(间隙锁,每次锁住的不光是需要使用的数据,还会锁住这些数据附近的数据)的结合。

Next-Key Lock即在事务中select时使用如下方法加锁,这样在另一个事务对范围内的数据进行修改时就会阻塞(为什么有共享锁会阻塞?不能在有共享锁的记录上加X锁):
select * from table where id<6 lock in share mode;--共享锁
select * from table where id<6 for update;--排他锁
关于next-key locks请参考
💪🏻 参考文章
- https://juejin.im/post/5c68a4056fb9a049e063e0ab
- https://zhuanlan.zhihu.com/p/35500144
- https://www.jianshu.com/p/69fd2ca17cfd
- https://blog.csdn.net/AAA821/article/details/81017704
- https://dbaplus.cn/news-11-2518-1.html
- 《高性能MySQL》