Skip to content
Xiaoyu's Blog
Go back

可重复读导致的幻读

😀
八股文背完好久,又忘记了,和同事今天讨论又重新实践了一下,记录一下。

前置准备:


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来避免其他事务修改:

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请参考

💪🏻 参考文章


Share this post:

Previous Post
redis持久化策略
Next Post
随机生成六位不碰撞的数字