不可思议的一致性读场景

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
云数据库 RDS PostgreSQL,高可用系列 2核4GB
简介: 不可思议的一致性读场景

偶尔会被问到,老叶你上课是不是很简单,只要一份教材在手就可以反复讲很多年,甚至会问,你上课是不是只要照着念PPT就行?

我呸,都什么年代了,怎么还有这种想法。哪怕是在应试教育著称的中小学里,也是每个学期都要更新备课材料的,怎么可能一份教案讲一辈子,无非是中小学的课程内容变化没那么快。在以知识更新日新月异的IT行业,居然还有人抱着这种思想,简直了。

抱怨归抱怨,今天我要说一个在上课过程中被同学们问倒(是真的把我问倒了)的一个案例。

先交代下运行环境:

# MySQL版本:8.0.17 under MacOS
[root@yejr.me]>\s
mysql  Ver 8.0.17 for macos10.14 on x86_64 (MySQL Community Server - GPL)

Connection id:      19
...
Server version:     8.0.17 MySQL Community Server - GPL

# 事务隔离级别:RR
[root@yejr.me]> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+

# 测试表结构及数据
[root@yejr.me]> SHOW CREATE TABLE t1\G
**************** 1. row ****************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `c1` int(11) NOT NULL,
  `c2` int(11) DEFAULT NULL,
  `c3` int(11) DEFAULT NULL,
  PRIMARY KEY (`c1`), -- c1列是主键
  KEY `c2` (`c2`)  -- c2列是辅助索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

[root@yejr.me]> select * from t1;
+----+------+------+
| c1 | c2   | c3   |
+----+------+------+
|  0 |    0 |    0 |
|  1 |    1 |    1 |
|  2 |    2 |    2 |
|  3 |    3 |    3 |
+----+------+------+

好,表演开始。

session1 session2
begin;
#发起快照读
select * from t1 where c2=2;
...
2 | 2 | 2


begin;
#发起快照读
select * from t1 where c2=2;
...
2 | 2 | 2
#更新后立即提交事务
update t1 set c1=30 where c2=2;
commit;

select * from t1 where c2=2;
...
30 | 2 | 2


#再读一次,确认保持一致性
select * from t1 where c2=2;
...
2 | 2 | 2

#再来一次当前读
select * from t1 where c2=2 for update;
...
30 | 2 | 2

#恢复快照读
select * from t1 where c2=2;
...
2 | 2 | 2

#更新数据
update t1 set c1=c1+1 where c2=2;
...
Rows matched: 1 Changed: 1 Warnings: 0

#更新完毕后读取
select * from t1 where c2=2;
...
2 | 2 | 2
31 | 2 | 2
#神奇的一幕发生了,可以看到新旧两条记录

#提交事务后再读取
commit;
select * from t1 where c2=2;
...
31 | 2 | 2
#这次正常了,只能看到最新版本的数据

好,表演结束。相信看完后,你跟我的第一反应都是“握了个草,为毛会这样,这不科学”,可事实上的确如此,我再三测试了几次,都确认是这样的结果。后来我请教了下InnoDB核心开发者之一苏斌老师(苏斌老师之前在知数堂做过一次公开课分享,主题是 MySQL 8.0 InnoDB新特性)。一开始他也觉得这个案例不太可思议,后来经过查阅确认,认为这是符合一致性读的规则,看文档的解释:

A consistent read means that InnoDB uses multi-versioning to present to

a query a snapshot of the database at a point in time.

The query sees the changes made by transactions that committed before that
point of time, and no changes made by later or uncommitted transactions.

# 注意从这段开始的说明
The exception to this rule is that the query sees the changes made by earlier
statements within the same transaction.

This exception causes the following anomaly: If you update some rows in a
table, a SELECT sees the latest version of the updated rows, but it might
also see older versions of any rows.

If other sessions simultaneously update the same table, the anomaly means
that you might see the table in a state that never existed in the database.

简言之,上述文档说明了几点:

  1. InnoDB利用MVCC机制保证在事务范围内任意时间点的一致性读需求(也就是:RR级别下,在同一个事务内任意时间点的一致性读,总是能读取到同样的数据)
  2. RR级别下,是在发起第一个SELECT(不包含SELECT ... FOR UPDATE/FOR SHARE这种加锁读,以后另起一篇说这个事)时,创建的快照,因此能读取到在此之前已经提交的事务数据,在本事务之后修改的事务数据是看不到的
  3. 上述第2条规则的一个例外场景时,能读取到在本事务内自己修改的数据。因此当在事务内更新完一条记录后发起SELECT可以读取到更新后的数据,同时也可能读取到旧版本的数据

在本案例中,由于两个session都是直接更新主键列,又由于InnoDB引擎的特殊性,主键列会被选择作为聚集索引。对InnoDB主键的更新是不能inplace的,需要新创建一条记录。因此对主键索引的更新时,相当于此时同一条记录在表内有两个版本,一个是更新前(该版本后续会被删除),一个是更新后的,然后等待提交。在session2中,第一次SELECT后创建了一个快照版本 [2,2,2],而后的当前读可以读取到最新数据 [30,2,2],因为sesison1已经提交,不会被阻塞。而session2中的更新,会在当前读的基础上进行更新,所以更新后的版本是 [31,2,2],更新完毕后又再次进行一致性读,此时就可以看到新旧两个版本的数据了(因为旧版本对本事务而言,还在快照里)。本案例给我们的几点启示是

  1. 当你想更新一条记录时,最好一开始就对其先加锁(SELECT ... FOR UPDATE),而后再在事务中进行更新,这样就可以避免被其他session给更新了。虽然一开始就加锁可能会造成更多的锁等待和死锁概率,但为了数据一致性,也必须如此了。或者,可以把事务隔离级别降为RC,这样每次SELECT总能看到已提交的最新版本
  2. 永远”不要更新主键列
  3. 想要做到第2点,就需要让主键列“只用作主键”,不具备业务属性,也即是我们一直强调的一个开发规范“每个InnoDB表都要有一个自增整型列做主键,且该列没有业务用途

不知道我这样解释清楚了没有。InnoDB的这种做法,看起来像是合理的,但仔细想想又好像不太合理,我去提了个bug(#96205),但被拒了,囧...

Enjoy MySQL :)

            </div>
相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
7月前
|
人工智能 JavaScript 开发工具
MCP详解:背景、架构与应用
模型上下文协议(MCP)是由Anthropic提出的开源标准,旨在解决大语言模型与外部数据源和工具集成的难题。作为AI领域的“USB-C接口”,MCP通过标准化、双向通信通道连接模型与外部服务,支持资源访问、工具调用及提示模板交互。其架构基于客户端-服务器模型,提供Python、TypeScript等多语言SDK,方便开发者快速构建服务。MCP已广泛应用于文件系统、数据库、网页浏览等领域,并被阿里云百炼平台引入,助力快速搭建智能助手。未来,MCP有望成为连接大模型与现实世界的通用标准,推动AI生态繁荣发展。
6333 66
|
Prometheus Kubernetes 监控
在K8S中,Pod处于OOM状态如何排查?
在K8S中,Pod处于OOM状态如何排查?
|
编解码 前端开发 图形学
【技术深度解析】多平台适配下的UI适配难题:U3D游戏UI错乱的终极解决方案
【7月更文第12天】随着移动设备市场的多元化,Unity游戏开发者面临的一大挑战是如何在不同分辨率和屏幕尺寸的设备上保持UI的一致性和美观性。游戏在高分辨率平板与低分辨率手机上呈现出的UI布局混乱、按钮错位等问题,严重影响玩家体验。本文旨在探讨Unity UI(UGUI)在多平台适配中的最佳实践,通过优化Canvas Scaler设置、灵活运用RectTransform和Anchor Points,以及高效利用设计工具,确保UI的完美适配。
1823 1
|
C语言 C++ 开发者
C/C++陷阱——变量名和函数名的冲突问题
C/C++陷阱——变量名和函数名的冲突问题
|
存储 监控 数据库
IoTDB控制台工具Workbench
IoTDB控制台工具Workbench
1434 0
|
XML 搜索推荐 安全
lucene、solr、es的区别以及应用场景
@[TOC](目录) Lucene、Solr 和 Elasticsearch(ES) 都是基于 Lucene 引擎的搜索引擎,它们之间有相似之处,但也有一些不同之处。 Lucene 是一个低级别的搜索引擎库,它提供了一种用于创建和维护全文索引的 API,以及一些搜索和排序算法。Lucene 主要用于构建自定义搜索引擎,例如在 Java 应用程序中使用。 Solr 是 Lucene 的一个扩展,它提供了一个完整的搜索引擎框架,包括了索引、搜索、排序、过滤等功能。Solr 旨在为大规模数据集提供高性能的全文搜索功能,因此它支持分布式搜索、实时搜索和自定义排序和过滤器等功能。 Elasticsear
777 0
|
Java Maven
2022新版版本IDEA创建Maven项目卡死-Bug
在使用新版本IDEA的时候,我是2022.1版本,创建Maven项目时,我发现有时候IDEA会卡死。
976 0
|
Oracle 前端开发 算法
JDK源码(21)-Unsafe
JDK源码(21)-Unsafe
349 0
JDK源码(21)-Unsafe
|
Windows
windows下未知原因导致按键全部变成了快捷键(按D返回桌面)解决办法
windows下未知原因导致按键全部变成了快捷键(按D返回桌面)解决办法
1522 0
|
运维 监控 Kubernetes
分享实录 | 阿里巴巴DevOps文化浅谈
近些年DevOps火遍全国,似乎不说DevOps研发效率就是低下的,技能就是落伍的。然而真是这样么?为了让大家更好的了解DevOps文化,3月27日《云效说码》分享特别邀请了阿里巴巴资深技术专家陈鑫(花名:神秀)进行视频直播分享,聊聊他对DevOps的理解以及阿里巴巴的DevOps文化落地要诀。
6494 0
分享实录 | 阿里巴巴DevOps文化浅谈