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

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

项目环境

mysql 版本:5.7.28-winx64

OS:win 10

数据库脚本:

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,'188******26');
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,'188******22');

脏读

准备工作

在讲脏读之前,我们先开启两个会话,并把事务隔离级别更改为读未提交(read uncommitted)。这时,id 为 222 的用户初始年龄为 18。

万事俱备,我们开始吧。

什么是脏读

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

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

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

如何解决脏读

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

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

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

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

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

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

不可重复读

准备工作

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

什么是不可重复读

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

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

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

如何解决不可重复读

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

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

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

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

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

这里我还是说说自己不严谨的理解。当第一次读到 id 为 222 的记录时,mysql 会把这条记录放在当前事务的缓存区里,下次读这条数据的时候直接从缓存拿就好,不需要去读实际记录,所以,其他会话的写操作并不需要等待。这种读被称为快照读,如果读已提交中的写是 copy on write,那可重复读的读就是 copy on read。

幻读

未完待续。

结语

以上只是自己对 mysql 锁、事务的一点点思考。因为我并没有看过底层的逻辑,所以都是一些抽象层面的解读。如有错误,欢迎指正。

最后,感谢阅读。

参考资料

MySQL中的锁(表锁、行锁,共享锁,排它锁,间隙锁)

本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/15552479.html

简单聊聊mysql的脏读、不可重复读的更多相关文章

  1. 数据库事务隔离级别 - 分析脏读 & 不可重复读 & 幻读

    一 数据库事务的隔离级别 数据库事务的隔离级别有4个,由低到高依次为Read uncommitted .Read committed .Repeatable read .Serializable ,这 ...

  2. Java -- JDBC 事务处理, 事务的隔离级别 脏读 不可重复读 等...

    1. 事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功. 数据库开启事务命令 •start transaction 开启事务 •Rollback  回滚事务 •Commit ...

  3. spring事务隔离级别以及脏读 不可重复读 幻影读

    隔离级别 声明式事务的第二个方面是隔离级别.隔离级别定义一个事务可能受其他并发事务活动活动影响的程度.另一种考虑一个事务的隔离级别的方式,是把它想象为那个事务对于事物处理数据的自私程度. 在一个典型的 ...

  4. MySQL事务(脏读、不可重复读、幻读)

    1. 什么是事务? 是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作:这些操作作为一个整体一起向系统提交,要么都执行.要么都不执行:事务是一组不可再分割的操作集合(工作逻辑单元): ...

  5. Mysql事务,并发问题,锁机制-- 幻读、不可重复读--专题

    1.什么是事务 事务是一条或多条数据库操作语句的组合,具备ACID,4个特点. 原子性:要不全部成功,要不全部撤销 隔离性:事务之间相互独立,互不干扰 一致性:数据库正确地改变状态后,数据库的一致性约 ...

  6. MySQL使用可重复读作为默认隔离级别的原因

    一般的DBMS系统,默认都会使用读提交(Read-Comitted,RC)作为默认隔离级别,如Oracle.SQL Server等,而MySQL却使用可重复读(Read-Repeatable,RR). ...

  7. MySQL选用可重复读之前一定要想到的事情

    原文地址:http://blog.itpub.net/29254281/viewspace-1398273/ MySQL选用可重复读隔离级别之前一定要想到的事情.间隙锁 MySQL在使用之前有三个务必 ...

  8. 聊聊mysql的事务

    今天来聊聊事务的四大特性以及其实现原理,需结合之前写的mysql是如何实现mvcc的来理解,因为大多数的实现都是基于mvcc的,理论介绍完后会通过实例来演示mvcc又是如何实现这些隔离级别的 事务的四 ...

  9. mysql系列:加深对脏读、脏写、可重复读、幻读的理解

    关于相关术语的专业解释,请自行百度了解,本文皆本人自己结合参考书和自己的理解所做的阐述,如有不严谨之处,还请多多指教. 事务有四种基本特性,叫ACID,它们分别是: Atomicity-原子性,Con ...

随机推荐

  1. Redis 很屌,不懂使用规范就糟蹋了

    这可能是最中肯的 Redis 使用规范了 码哥,昨天我被公司 Leader 批评了. 我在单身红娘婚恋类型互联网公司工作,在双十一推出下单就送女朋友的活动. 谁曾想,凌晨 12 点之后,用户量暴增,出 ...

  2. 各种多项式操作的 n^2 递推

    zszz,使用 NTT 可以在 \(\mathcal O(n\log n)\) 的时间内求出两个多项式的卷积.以及一个多项式的 \(\text{inv},\ln,\exp,\text{sqrt}\) ...

  3. go搭建beego框架 go安装bee报错 github.com/derekparker/delve@v1.4.0: parsing go.mod:

    go搭建beego框架 go安装bee报错 go使用beego框架的时候,需要安装bee,安装的时候遇到一些坑报错,这里跟大家分享一下,有助于快速安装bee,搭建好beego环境 1. 首先切换到go ...

  4. vim——批量缩进

    批量缩进 第一种 按esc,退出编辑模式,到命令模式,并在英语输入法下输入":" 将所要批量缩进的行号写上,按照格式:"行号1,行号2>"输入命令,如要将 ...

  5. sig mesh 培训-18304

    1.mesh 的传输速率 ---有效数据最长的长度是10个字节 ---最小时间间隔是10ms,重传1次 --建议数据包之间不少于100ms 1S = 10*10 =100个字节 2.目前telink ...

  6. TCP的慢启动、拥塞避免、重传、快恢复乱七八糟总是记不清?11个连环问让你一次性打通任督二脉

    摘要:如果你的开发过程涉及数据传输,一直在重传.超时之类的方案里有困惑的话,不妨重新学一学可靠性最精致的TCP协议. 本文分享自华为云社区<TCP的慢启动.拥塞避免.重传.快恢复乱七八糟总是记不 ...

  7. 零基础学习java------day27-28---------电影评分数据案例,. RPC案例

    一.  电影评分数据案例 movie:电影id rate:用户评分 timeStamp:评分时间 uid:用户id 简化数据: 需求: (1)每个用户评分最高的3部电影 (2)每个用户评分的平均值 ( ...

  8. 100个Shell脚本—【脚本6】拷贝目录

    [脚本6]拷贝目录 编写shell脚本,把/root/目录下的所有目录(只需要一级)拷贝到/tmp/目录下: 一.脚本 #!/bin/bash cd /root list=(`ls`) for i i ...

  9. Mybatis-运行原理

    一.mybatis分层图 二.运行流程 根据全局配置文件创建sqlSessionFactory对象 根据全局配置文件的io流来构建SqlSessionFactoryBuilder对象: 解析(XmlC ...

  10. IDE搬进浏览器里——JetBrains Projector

    发展 提起 JetBrains,你会想到什么?各路强大的 IDE,比如 Android Studio.IDEA.WebStorm--这些对于开发者来说耳熟能详的产品都出自这家公司,这些 IDE 的功能 ...