MySQL中脏读、不可重复读、幻读

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: 简单聊聊mysql的脏读、不可重复读、幻读

最近,在一次 mysql 死锁的生产事故中,我发现,关于 mysql 的锁、事务等等,我所知道的东西太碎了,所以,我试着用几个例子将它们串起来。具体做法就是通过不断地问问题、回答问题,再加上“适当”的比喻,来逐步构建脑子里的“知识树”。

需要提醒一下,这篇博客并不适合小白,因为你需要先了解排它锁、共享锁、事务,最重要的是你需要知道事务中的锁是什么时候加上、什么时候打开的。而这篇博客更多的是希望把这些碎片化的知识给连接起来。

项目环境
mysql 版本:5.7.28-winx64

OS:win 10

数据库脚本:

hljs
DROP TABLE IF EXISTS demo_user;

CREATE TABLE demo_user (
id varchar(32) NOT NULL COMMENT '用户id',
name varchar(16) NOT NULL COMMENT '用户名',
gender tinyint(1) DEFAULT '0' COMMENT '性别',
age int(3) unsigned DEFAULT NULL COMMENT '用户年龄',
gmt_create datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录创建时间',
gmt_modified timestamp NULL DEFAULT NULL COMMENT '记录最近修改时间',
deleted tinyint(1) DEFAULT '0' COMMENT '是否删除',
phone varchar(11) NOT NULL COMMENT '电话号码',
PRIMARY KEY (id),
KEY idx_phone (phone),
KEY idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

insert into demo_user(id,name,gender,age,gmt_create,gmt_modified,deleted,phone) values ('222','zzs001',0,18,'2021-12-13 15:11:03','2021-12-13 09:59:12',0,'18826');
insert into demo_user(id,name,gender,age,gmt_create,gmt_modified,deleted,phone) values ('111','zzf001'0,18,'2001-08-27 11:00:11','2001-08-27 11:00:13',0,'18822');
脏读
准备工作
在讲脏读之前,我们先开启两个会话,并把事务隔离级别更改为读未提交(read uncommitted)。这时,id 为 222 的用户初始年龄为 18。

mysql_lock_01.png

万事俱备,我们开始吧。

什么是脏读
脏读,就是读到了其他会话还没有提交的修改。下面用例子说明:

mysql_lock_02.png

可以看到,会话 2 修改了 id 为 222 的用户,在还没提交或回滚事务之前,会话 1 就读到了这些改动。

脏读的本质就是,还没结束的写操作被读操作分割了。所以,为了解决脏读,就必须让写操作不可被读操作分割(当然,也不能被其他写操作分割),即保证所谓的原子性。

如何解决脏读
那么,应该如何实现呢?这里给出两种方案。

第一种,给读增加锁。为了保证写操作的原子性,从更新操作开始到事务结束(注意,不是事务开始到事务结束),会话 2 都应该锁着 id 为 222 的记录,会话 1 的读操作要等会话 2 的事务结束后才能执行。上面的例子中,我们理所当然地会认为是会话 2 的写操作没有加排它锁导致的脏读,然而并非如此,通过SELECT * FROM information_schema.INNODB_TRX;可以发现,会话 2 已经锁住了 id 为 222 的记录,但会话 1 的读操作并没有等待,为什么呢?根本原因在于会话 1 的读是无锁读,在读未提交的事务隔离级别中,无锁读不需要等待写操作。所以,我们需要给读加上锁(共享锁和排它锁均可,但为了并发读,建议用共享锁),如下:

mysql_lock_03.png

可以看到,因为会话 2 的更新操作还没结束,所以,会话 1 需要一直等待,直到会话 2 的事务结束,这就避免了脏读的问题。你可能会觉得奇怪,实际项目好像不是这样的吧?没错,因为我们用的更多的是第二种方案。

第二种方案,将事务隔离级别更改为读已提交(read committed)。第一种方案中,读写是串行的,然而,我们既要读写并行,又不想出现脏读。需求刁钻但合理,于是,就有了第二种方案。如下:

mysql_lock_04.png

可以看到,会话 2 的更新操作还没结束,会话 1 就读到了同一条记录,结果却没有产生脏读。如何实现的呢?

首先,我说说自己以前的理解:逻辑上有点像 java 中的CopyOnWriteArrayList,当事务隔离级别为已提交时,不会在实际记录上进行写操作,而是将需要修改的记录缓存一份进行更改,事务提交时才把这部分缓存刷入实际记录,而这个过程,其他会话可以正常读实际记录,而不会读到修改中的数据。

后来了解 MVCC 才知道我是错的,就 id 为 222 的这行数据,mysql 会同时保留多个版本,而此时的会话 1 只能看到更早的已提交版本。具体见文末参考资料:什么是 MVCC。

不可重复读
准备工作
在讲不可重复读之前,我们可以把事务隔离级别设置为读未提交(read uncommitted),也可以设置为读已提交(read committed)。

什么是不可重复读
不可重复读,就是在同一个事务中,多次读相同的记录但读到了不同的结果。下面用例子说明:

mysql_lock_05.png

可以看到,会话 1 第一次读 id 为 222 的用户年龄为 18,在事务还没结束之前,会话 2 将他的年龄更改为 19,会话 1 再次读就会出现前后不一致的情况。

不可重复读的本质就是,还没结束的读操作被写操作分割了。所以,为了解决不可重复读,就必须让读操作不可被写操作分割,即保证所谓的原子性。

如何解决不可重复读
那么,应该如何实现呢?和解决脏数据一样,这里也给出两种方案。

第一种方案,给读增加锁来。为了保证读操作的原子性,从读操作开始到事务结束(注意,不是事务开始到事务结束),会话 1 都应该锁着 id 为 222 的记录,会话 2 的写操作要等会话 1 的事务结束后才能执行。所以,我们需要给读加上锁(共享锁和排它锁均可,但为了并发读,建议用共享锁),如下:

mysql_lock_06.png

可以看到,会话 2 的写操作需要等待会话 1 的事务结束才能执行,在事务结束之前,会话 1 读几次数据都不会出现不可重复读。

第二种方案,将事务隔离级别更改为可重复读(repeatable read)。第一种方案中,读写是串行的,然而,我们既要读写并行,又不想出现不可重复读。于是,就有了第二种方案。如下:

mysql_lock_07.png

可以看到,会话 1 的读操作并没有加锁,会话 2 的写操作也不需要等待,最终却没有产生不可重复读。如何实现的呢?

首先,我说说自己以前错误的理解:当第一次读到 id 为 222 的记录时,mysql 会把这条记录放在当前事务的缓存区里,下次读这条数据的时候直接从缓存拿就好,不需要去读实际记录,所以,其他会话的写操作并不需要等待。

不过,和解决脏读一样,这里也是用到了 MVCC。有人可能会问,同样是 MVCC 为什么,RR 可以解决可重复读,而 RC 不行?具体的原理见文末参考资料:什么是 MVCC。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
SQL Java 关系型数据库
MySQL最终弹-并发(脏读,不可重复读,幻读及区别),JDBC的使用和安装,最全万字(二)
MySQL最终弹-并发(脏读,不可重复读,幻读及区别),JDBC的使用和安装,最全万字
|
Java 关系型数据库 MySQL
MySQL最终弹-并发(脏读,不可重复读,幻读及区别),JDBC的使用和安装,最全万字(一)
MySQL最终弹-并发(脏读,不可重复读,幻读及区别),JDBC的使用和安装,最全万字
MySQL最终弹-并发(脏读,不可重复读,幻读及区别),JDBC的使用和安装,最全万字(一)
|
4月前
|
关系型数据库 MySQL 数据库
MySQL的脏读、幻读、不可重复度是什么
**脏读**:事务读取了未提交的修改,可能导致不一致。**不可重复读**:同一事务中相同查询返回不同结果,因其他事务修改。**幻读**:事务在范围查询时,另一事务插入数据,使行数不一致。SQL-92定义四种事务隔离级别来防止这些问题:序列化、可重复读、提交读、未提交读。
|
6月前
|
SQL 关系型数据库 MySQL
⑨【MySQL事务】事务开启、提交、回滚,事务特性ACID,脏读、幻读、不可重复读。
⑨【MySQL事务】事务开启、提交、回滚,事务特性ACID,脏读、幻读、不可重复读。
57 0
|
6月前
|
SQL 关系型数据库 MySQL
【MySQL】脏读、不可重复读、幻读介绍及代码解释
【MySQL】脏读、不可重复读、幻读介绍及代码解释
|
6月前
|
存储 SQL 关系型数据库
MySQL MVCC多版本并发控制(脏读和不可重复读解决原理)
MySQL MVCC多版本并发控制(脏读和不可重复读解决原理)
152 0
MySQL MVCC多版本并发控制(脏读和不可重复读解决原理)
|
关系型数据库 MySQL
【面试题精讲】MySQL-事务隔离-不可重复读
【面试题精讲】MySQL-事务隔离-不可重复读
|
数据库
【面试题精讲】MySQL-事务隔离-脏读
【面试题精讲】MySQL-事务隔离-脏读
|
关系型数据库 MySQL 数据库
数据库事务的陷阱:探讨MySQL脏读现象
在数据库系统中,事务是一种用于管理和维护数据完整性的机制。然而,在多用户并发访问数据库的情况下,可能会出现各种隔离性问题,其中之一就是脏读。脏读是指在事务A修改了数据,但事务B在事务A提交之前读取了这些未提交的数据,从而导致事务B读取到了不正确的数据。在MySQL数据库中,脏读是一个需要特别关注的问题。
250 0