InnoDB这个将近20年的"bug"修复了

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
云数据库 RDS MySQL,高可用系列 2核4GB
简介: InnoDB这个将近20年的"bug"修复了

0. 背景信息

1. MySQL 8.0.18 以前是怎么加锁的

2. MySQL 8.0.18 之后终于变天了


0. 背景信息

最近在课程中讲到InnoDB行锁时,讲到一个知识点

InnoDB行锁规则上,有这样的一个原则:


对有唯一属性的索引(主键/唯一索引)进行范围条件加锁时,

向右遍历(假设是普通正序索引,而且不加ORDER BY … DESC约束)过程中,

会一直扫描并加next-key锁到第一个不满足条件的记录为止,

但如果是RC级别,这个next-key lock会退化成gap lock,而RR下不会退化。


简言之,就是 "锁会被扩大化",从InnoDB引擎诞生以来一直都是如此。

其实严格来说,这个算是问题或缺陷,甚至也可以认为是bu

1. MySQL 8.0.18 以前是怎么加锁的

我们看看下面的案例。

首先,确认版本、隔离级别、表结构、索引以及数据。

建议:在PC端阅读本文体验更好。

# 5.6版本
[root@yejr.run]> select version();
+------------+
| version()  |
+------------+
| 5.6.39-log |
+------------+

#隔离级别
[root@yejr.run]> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+

#表数据
[root@yejr.run]> select * from t1;
+----+----+----+----+
| c1 | c2 | c3 | c4 |
+----+----+----+----+
|  0 |  0 |  0 |  0 |
|  1 |  1 |  1 |  0 |
|  3 |  3 |  3 |  0 |
|  4 |  2 |  2 |  0 |
+----+----+----+----+

#表结构&索引,c1是主键(有唯一属性),c2是辅助索引
[root@yejr.run]> show create table t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `c1` int(10) unsigned NOT NULL DEFAULT '0',
  `c2` int(10) unsigned NOT NULL DEFAULT '0',
  `c3` int(10) unsigned NOT NULL DEFAULT '0',
  `c4` int(10) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`c1`),
  KEY `c2` (`c2`)
) ENGINE=InnoDB;

下面的两个案例中,session2的请求会被阻塞

时间点 session1 sessioin2
T1 begin; begin;
T2 select * from t1 where c1<=1 for update;
T3
delete from t1 where c1=3;

被阻塞
时间点 session1 sessioin2
T1 begin; begin;
T2
delete from t1 where c1=3;
T3 select * from t1 where c1<=1 for update;

一样会被阻塞

原因在于 select * from t1 where c1<=1 for update 这个SQL中,除了对 c1<=1 的所有记录加上 LOCK_X|LOCK_ORDINARY(排他的next-key lock)之外,还需要对 c1=3 这条记录也加同样的锁

查看 information_schema 下的两个视图 innodb_locksinnodb_lock_waits 可以确认:

[root@yejr.run]> select * from INNODB_LOCKs\G

1. row **
lock_id: 2849:26:3:4 --请求的锁
lock_trx_id: 2849 --被阻塞的事务
lock_mode: X --拍他锁
lock_type: RECORD --锁类型是 LOCK_ORDINARY(即next-lock)
lock_table: `test`.`t1`
lock_index: PRIMARY
lock_space: 26
lock_page: 3
lock_rec: 4
lock_data: 3
2. row **
lock_id: 2848:26:3:4 --持有的锁
lock_trx_id: 2848 --持有锁的事务
lock_mode: X --排他锁 LOCK_X
lock_type: RECORD --锁类型是 LOCK_ORDINARY(即next-lock)
lock_table: `test`.`t1` --表
lock_index: PRIMARY --索引
lock_space: 26 --table space id
lock_page: 3 --page no
lock_rec: 4 --heap no
lock_data: 3 --被加锁的row data,即c1=3这条记录
2 rows in set (0.00 sec)

[root@yejr.run]> select * from INNODB_LOCK_waits\G
1. row **
requesting_trx_id: 2849 --请求锁的事务(被阻塞状态)
requested_lock_id: 2849:26:3:4 --请求的锁
blocking_trx_id: 2848 --持有锁的事务
blocking_lock_id: 2848:26:3:4 --持有的锁

当然了,也可以从 show engine innodb status\G 的结果中确认,这里不赘述。

2. MySQL 8.0.18 之后终于变天了

这个存在了将近20年的"bug",终于在2019.10.14发布的MySQL 8.0.18版本中被解决(修复)了,当时我居然没注意到这个release note。

InnoDB: An unnecessary next key lock was taken when performing 
a SELECT...FOR [SHARE|UPDATE] query with a WHERE condition that
specifies a range, causing one too many rows to be locked. The
most common occurrences of this issue have been addressed so
that only rows and gaps that intersect the searched range are
locked. (Bug #29508068)

简言之:就是不再需要对不必要的数据上锁啦。

再看看上面几个案例在最新的MySQL 8.0.19版本下的表现。

时间点 session1 sessioin2
T1 begin; begin;
T2 select * from t1 where c1<=1 for update;
T3
delete from t1 where c1=3;

不再被阻塞

看下加锁详情

select ENGINE_LOCK_ID,ENGINE_TRANSACTION_ID,THREAD_ID,OBJECT_NAME,INDEX_NAME,LOCK_TYPE,LOCK_MODE,LOCK_STATUS,LOCK_DATA from data_locks;
+-----------------------------------+-----------------------+-----------+-------------+------------+-----------+---------------+-------------+-----------+
| ENGINE_LOCK_ID | ENGINE_TRANSACTION_ID | THREAD_ID | OBJECT_NAME | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+-----------------------------------+-----------------------+-----------+-------------+------------+-----------+---------------+-------------+-----------+
| 4868124032:1127:140327172372248 | 18983 | 351 | t1 | NULL | TABLE | IX | GRANTED | NULL |
| 4868124032:44:4:4:140327176578584 | 18983 | 351 | t1 | PRIMARY | RECORD | X,REC_NOT_GAP | GRANTED | 3 |
| 4868123176:1127:140327172370216 | 18982 | 350 | t1 | NULL | TABLE | IX | GRANTED | NULL |
| 4868123176:44:4:2:140327176573976 | 18982 | 350 | t1 | PRIMARY | RECORD | X | GRANTED | 0 |
| 4868123176:44:4:3:140327176573976 | 18982 | 350 | t1 | PRIMARY | RECORD | X | GRANTED | 1 |
+-----------------------------------+-----------------------+-----------+-------------+------------+-----------+---------------+-------------+-----------+

可以看到,select * from t1 where c1<=1 for update; 这个SQL只会对 c1=[0,1] 两条记录加上 LOCK_X|LOCK_ORDINARY 锁,不会再对 c1=3 加锁了。

这个有年头的"bug"终于被搞定了,可喜可贺。

最后,来看下关于这个"bug"的描述。当然了,公开的bug系统看不到,需要用MOS账号才可以。下面是从代码git log里的部分摘抄:

commit d1b0afd75ee669f54b70794eb6dab6c121f1f179
Author: Jakub Łopuszański <jakub.lopuszanski@oracle.com>
Date: Wed Jul 17 16:34:01 2019 +0200

Bug #29508068 UNNECESSARY NEXT-KEY LOCK TAKEN

When doing a SELECT...FOR [SHARE|UPDATE] with a WHERE condition specifying a range,
we were locking "one row too much".
This patch fixes locking behaviour in several (hopefuly) most common cases, so that
we only lock rows and gaps which intersect the searched range.

- Added MTR to demonstrate current locking policy for end of range
- Got rid of goto
- Extracted logic of determining relation between range and row to separate function
- Extracted reoccuring patterns of modifications of search_tuple so it is easier to add same for stop_tuple
- Added prebuilt->m_stop_tuple and made sure it is in sync with prebuilt->m_mysql_handler->end_range for during read_range_first() and read_range_next()
- Added row_can_be_in_range field
- Do not lock the row (just the gap) if the row is same length and after the stop_tuple
- Do not lock the row (just the gap) if the row is same length and equal to stop_tuple and strict inequality was used for end of range
- Do not lock the row (just the gap) if the row is longer than stop_tuple and its prefix is after the stop_tuple
- Do not lock the row (just the gap) if the row is longer than stop_tuple and its prefix is equal to stop_tuple and strict inequality was used for end of range
- Do not lock the row nor gap if we already saw a row same length and equal to stop_tuple in previous iteration

Reviewed-by: Pawel Olchawa <pawel.olchawa@oracle.com>
RB:22293

所以,还是赶紧升级到MySQL 8.0的最新版本吧,不光功能更强,连锁也进一步优化了。

写到这里,不禁想嘚瑟一下,加入我的「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;
相关文章
|
11月前
|
Cloud Native 持续交付 云计算
云原生技术的崛起与未来展望
本文探讨了云原生技术的核心概念、发展历程及其在现代IT架构中的关键作用。随着云计算的普及,云原生作为一种优化云应用构建和部署的方法,正逐渐成为企业数字化转型的重要推力。文章分析了容器化、微服务、持续集成/持续部署(CI/CD)等关键技术如何支撑起灵活、高效、可扩展的云原生架构,并讨论了面临的挑战与未来的发展趋势。
364 12
|
6月前
|
存储 SQL 关系型数据库
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
|
XML JavaScript Java
【JAVA XML 探秘】DOM、SAX、StAX:揭秘 Java 中 XML 解析技术的终极指南!
【8月更文挑战第25天】本文详细探讨了Java中三种主流的XML解析技术:DOM、SAX与StAX。DOM将XML文档转换为树状结构,便于全方位访问和修改;SAX采取事件驱动模式,适用于大型文件的顺序处理;StAX则兼具DOM和SAX的优点,支持流式处理和随机访问。文中提供了每种技术的示例代码,帮助读者理解如何在实际项目中应用这些解析方法。
608 1
|
9月前
|
存储 关系型数据库 分布式数据库
PolarDB 开源基础教程系列 1 架构解读
PolarDB 是阿里云研发的云原生分布式数据库,基于 PostgreSQL 开源版本,旨在解决传统数据库在大规模数据和高并发场景下的性能和扩展性问题。其主要特点包括: 1. **存储计算分离架构**:通过将计算与存储分离,实现极致弹性、共享一份数据以降低成本、透明读写分离。 2. **HTAP 架构**:支持混合事务处理和分析处理(HTAP),能够在同一系统中高效执行 OLTP 和 OLAP 查询。 3. **优化的日志复制机制**:采用只复制元数据的方式减少网络传输量,优化页面回放和 DDL 锁回放过程。 4. **并行查询与索引创建**:引入 MPP 分布式执行引擎。
427 8
|
Docker Windows 容器
7-12|在window上 运行docker-compose文件
7-12|在window上 运行docker-compose文件
|
应用服务中间件 nginx
nginx动静分类,静态资源放到nginx中
这篇文章介绍了如何将项目中的静态资源部署到Nginx服务器中,包括将静态资源移动到Nginx目录、删除项目中的静态资源、替换静态资源访问路径、检查页面情况、修改Nginx配置文件以及重启Nginx查看效果的详细步骤。
nginx动静分类,静态资源放到nginx中
|
SQL 缓存 关系型数据库
(二)全解MySQL:一条SQL语句从诞生至结束的多姿多彩历程!
在上篇文章中,我们以《MySQL架构篇》拉开了MySQL数据库的的序幕,上篇文章中将MySQL分层架构中的每一层都进行了详细阐述。而在本篇中,则会进一步站在一条SQL的角度,从SQL的诞生开始,到SQL执行、数据返回等全链路进行分析。
516 1
|
前端开发 OLAP 应用服务中间件
深入了解 OLTP:在线事务处理系统
【8月更文挑战第31天】
905 0
|
SQL 存储 缓存
Mysql优化高级篇(全)
目录前言1. 简介1.1 安装1.2 MySQL逻辑架构存储引擎2. 索引优化分析2.1 原因2.2 常见通用的join查询2.3 索引2.3.1 索引分类2.3.2 索引结构2.3.3 索引情况2.4 性能分析2.4.1 id 前言 本篇文章主要涉及mysql的高级篇,主要是mysql的架构介绍、索引优化分析、查询截取分析、mysql锁机制以及主从复制等 在这之前的学习可参考我之前的文章进行学习 数据库知识 链接 数据库查询常用语句语法 博客链接 数据库中增删改常用语法语句(全) 博客
78822 19
Mysql优化高级篇(全)
|
数据采集 算法 编译器
倚天710规模化应用 - 性能优化 -自动反馈优化分析与实践
编译器优化分成静态优化与动态优化,静态优化指传统编译器gcc/llvm时,增加的优化等级,如O1,O2,O3,Ofast,此时,编译器会依据编译优化等级增加一些优化算法,如函数inline、循环展开以及分支静态预测等等。一般情况下,优化等级越高,编译器做的优化越多,性能会更会好。在阿里生产环境中,单纯依赖于静态优化,并不能达到程序运行流畅目的,通过分析CPU硬件取指令、执行指令,往往会出现一些分支预测失败导致iCacheMiss率高的场景,限制了程序的性能进一步提升。基于此,业务引入了动态反馈优化工具,依据生产环境的实际运行数据,反哺指导编译器对程序代码进一步调整编译优化策略,提高分支预准确率