最近,在一次 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. 洛谷 P5044 - [IOI2018] meetings 会议(笛卡尔树+DP+线段树)

    洛谷题面传送门 一道笛卡尔树的 hot tea. 首先我们考虑一个非常 naive 的区间 DP:\(dp_{l,r}\) 表示区间 \([l,r]\) 的答案,那么我们考虑求出 \([l,r]\) ...

  2. 【基因组注释】ncRNA注释

    目录 1. ncRNA 2. 软件 tRNA注释 rRNA注释 其他ncRNA注释 3. 注释 tRNA rRNA snRNA.miRNA等 4. snRNA.miRNA等结果的统计 1. ncRNA ...

  3. [linux] 大批量删除任务

    一不小心投了巨多任务,或者投递的资源不合理时,想批量杀掉这些任务. kill的方法就不说了,我这里用qdel的方法. 用了这么一条命令: qstat |sed '1,2d' |awk -F' ' '{ ...

  4. [R] ignore.case区分大小写参数

    字符串操作的函数(如contains),很多都包含ignore.case参数,默认是T,即不分大小写,稍不注意就会掉坑里,最好的习惯是下意识地加入这个参数. 举个例子: 我要选择An的列,就用下面这个 ...

  5. 卷积神经网络(Convolutional Neural Networks)CNN

     申明:本文非笔者原创,原文转载自:http://www.36dsj.com/archives/24006 自今年七月份以来,一直在实验室负责卷积神经网络(Convolutional Neural ...

  6. Flink 实践教程-进阶(2):复杂格式数据抽取

    作者:腾讯云流计算 Oceanus 团队 流计算 Oceanus 简介 流计算 Oceanus 是大数据产品生态体系的实时化分析利器,是基于 Apache Flink 构建的具备一站开发.无缝连接.亚 ...

  7. Hive(三)【DDL 数据定义】

    目录 一.DDL数据定义 1.库的DDL 1.1创建数据库 1.2查询数据库 1.3查看数据库详情 1.4切换数据库 1.5修改数据库 1.6删除数据库 2.表的DDL 2.1创建表 2.2管理表(内 ...

  8. 使用 Addressables 来管理资源

    使用 Addressables 来管理资源 一.安装 打开Package Manager,在Unity Technologies的目录下找到Addressables,更新或下载. 二.配置 依次打开W ...

  9. 关于form表单提交ajaxForm和ajaxSubmit的用法与区别

    前几天在学习form表单提交时看到这两种方法,这两种方法都是实现form的ajax提交的方法,看了很多资料还是不太明白其用法和区别,最后直接自己写demo,很快就理解,所以说实操是学习的最快捷直接的途 ...

  10. 【Linux】【Shell】【Basic】流程控制

    1. 选择执行: 1.1. if 单分支的if语句: if 测试条件 then 代码分支 fi 双分支的if语句: if 测试条件; then 条件为真时执行的分支 else 条件为假时执行的分支 f ...