mysql 退化range锁_InnoDB这个将近20年的"bug"修复了

原标题:InnoDB这个将近20年的"bug"修复了

有点意外,惊喜ing。

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引擎诞生以来一直都是如此。

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

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

我们看看下面的案例。

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

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

# 5.6版本

[root@yejr.run]> selectversion;

+------------+

| 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 t1G

*************************** 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_locks 和 innodb_lock_waits 可以确认:

[root@yejr.run]> select * from INNODB_LOCKsG

*************************** 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_waitsG

*************************** 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 statusG 的结果中确认,这里不赘述。

2. MySQL 8.0.18 之后终于变天了

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

InnoDB: An unnecessary next key lockwas taken whenperforming

a SELECT...FOR [ SHARE| UPDATE] querywitha WHEREcondition that

specifies a range, causing one too many rowstobe locked. The

most common occurrences ofthis issue have been addressed so

that onlyrowsandgaps that intersectthe searched rangeare

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里的部分摘抄:

commitd1b0afd75ee669f54b70794eb6dab6c121f1f179

Author: Jakub Łopuszański

Date: Wed Jul 1716: 34: 012019+ 0200

Bug #29508068 UNNECESSARY NEXT-KEY LOCK TAKEN

Whendoing a SELECT...FOR [ SHARE| UPDATE] witha WHEREcondition specifying a range,

we were locking "one row too much".

This patchfixes locking behaviour inseveral (hopefuly) most common cases, so that

we onlylockrowsandgaps which intersectthe searched range.

- Added MTR todemonstrate currentlocking policyforendofrange

- Got rid ofgoto

- Extracted logic ofdetermining relation betweenrangeandrowtoseparate function

- Extracted reoccuring patterns ofmodifications ofsearch_tuple so it iseasier toaddsame forstop_tuple

- Added prebuilt->m_stop_tuple andmade sure it isinsyncwithprebuilt->m_mysql_handler->end_range forduring read_range_first andread_range_next

- Added row_can_be_in_range field

- Donotlockthe row(just the gap) ifthe rowissame lengthandafterthe stop_tuple

- Donotlockthe row(just the gap) ifthe rowissame lengthandequal tostop_tuple andstrictinequality was used forendofrange

- Donotlockthe row(just the gap) ifthe rowislonger thanstop_tuple andits prefix isafterthe stop_tuple

- Donotlockthe row(just the gap) ifthe rowislonger thanstop_tuple andits prefix isequal tostop_tuple andstrictinequality was used forendofrange

- Donotlockthe rownor gap ifwe already saw a rowsame lengthandequal tostop_tuple inprevious iteration

Reviewed- by: Pawel Olchawa

RB: 22293

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

写到这里,不禁想嘚瑟一下,加入我的 「MySQL优化课」课程优势就体现出来了,一旦有重大的知识更新,总是能比别人先一步知道,图便宜买一些万年不更新的旧课,甚至是盗版视频,都是享受不到这种快感的。

有点标题党,贻笑大方了。水平有限,理解有偏差的地方,还请不吝留言指正。

由叶老师主讲的知数堂「MySQL优化课」课程早已升级到MySQL 8.0版本了,现在上车刚刚好,扫码开启MySQL 8.0的修行之旅吧。返回搜狐,查看更多

责任编辑:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值