InnoDB的行锁,原来为你做了这么多!

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: 这周暂时也没有荒腔走板环节,太忙了最近。。。

荒腔走板


这周暂时也没有荒腔走板环节,太忙了最近。。。


从事务的隔离级别谈起

众所周知,事务有四大特性,简称ACID:原子性、一致性、隔离性、持久性。

对于隔离性,简单来说就是多个事务之间是彼此隔离的,互不影响。但想要做到完全的互不影响是很难的,因为数据的强一致性,很多时候需要牺牲性能去达成。比如如果我们能接受事务的串行执行,那一定是互不影响的。然而现实是,MySQL作为一个数据库,必然是要支持一定程度的并行执行的,也就是多个事务同时去执行。

凡并行程序,往往是在性能和数据一致性上做取舍。较好的解决方案要么是最终一致,要么是尽量缩小串行执行的范围。

如果多个事务同时并行执行,在没有隔离的情况,可能会发生脏读、不可重复读、幻读的问题。

案例数据(demo表):

id(主键) c(普通索引) d(无索引)
5 5 5
10 10 10
15 15 15
20 20 20
25 25 25

脏读

一个事务读取了另一个事务未提交的数据。

网络异常,图片无法展示
|
脏读

不可重复读

一个事务读取同一行数据,多次读取结果不同。

网络异常,图片无法展示
|
不可重复读

幻读

一个事务读取到了别的事务插入的数据。

网络异常,图片无法展示
|
幻读

但InnoDB因为使用了MVCC,读取的是“快照”版本,有一些不同,但如果不上锁,同样可能会有幻读问题。

网络异常,图片无法展示
|
InnoDB的幻读

事务用了四种不同的隔离级别用来解决这些问题。

  • Read uncommitted(未提交读)
  • Read Committed(已提交读,简称RC)
  • Repeatable Reads(可重复读,简称RR)
  • Serializable(串行化)

隔离级别越高,解决的问题越多,但并发性能也会越差。它们之间的关系如下表:

隔离级别 脏读 不可重复读 幻读
Read uncommitted
Read Committed
Repeatable Reads
Serializable

但InnoDB有些许不同,InnoDB默认的隔离级别是RR,但是通过MVCC和间隙锁来一定程度上的解决了幻读的问题。这也是我们今天这篇文章后面会详细介绍的。


无锁思想:MVCC

MVCC即“多版本并发控制”,但是它在很多情况下避免了加锁操作,因此开销更低。

主流的关系型数据库都实现了MVCC,但实现机制各有不同。实际上MVCC也没有一个统一的标准。但大都实现了非阻塞的读操作,写操作也只是锁定必要的行。本文以下内容所说的MVCC都指的是InnoDB实现的MVCC。

在Mysql的InnoDB引擎,是通过给每行记录后面保存两个隐藏的列来实现的。一个是保存行的创建时间,另一个保存了行的过期时间(或删除时间)。

实际上存储的并不是实际的一个时间戳,而是“系统版本号”。

每次开启一个事务,系统版本号都会递增。事务开始时,系统版本号会作为事务的版本号,用来和查询到的行的版本号进行比较。

MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作,其它两个隔离级别不能工作。因为READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。

在MySQL中,正常的SELECT语句,后面不加FOR UPDATELOCK IN SHARE MODE的,就是用的MVCC去读。

MVCC和我们在应用层面去实现的“乐观锁”有一样的思想:用版本号,在尽量无锁的情况下实现一定程度的一致性。


InnoDB行锁的概念

InnoDB的行锁(也称为临键锁) Next-Key Locks,是MySQL对外暴露的锁的基本单位,它会智能选择记录锁或间隙锁,锁住一行或多行或一个间隙。而记录锁又分为共享锁和排他锁,间隙锁的概念下面有一个插入意向锁。这些锁的关系大概是这样:

网络异常,图片无法展示
|
锁的关系

记录锁

所谓记录锁 Record Locks,就是锁住确定的一行行记录。它分为共享锁和排它锁。分别对应不同的SQL写法。

共享锁

共享锁 Shared Locks ,简称S锁。使用以下SQL可能触发:

SELECT ... LOCK IN SHARE MODE
复制代码

之所以说“可能”触发,是因为它查到了数据库有确定的记录才会锁住这些记录,否则会变成间隙锁。这个其实很好理解,找到了数据,才锁它。如果没找到数据,就锁这个间隙。

排他锁

排他锁 Exclusive locks ,简称X锁。使用一下SQL可能触发:

SELECT ... FOR UPDATE

这里的“可能”含义与上面同理,不赘述。

间隙锁

间隙锁 Gap - Lock,顾名思义,锁住一个间隙。上文我们提到过,InnoDB默认的隔离级别是RR,但是通过间隙锁来一定程度上的解决了幻读的问题。它是怎么解决的呢?就是通过间隙锁来解决的。

上面两种SQL,如果没有查找到确定的记录,就会根据条件去锁住一个间隙。间隙锁是根据已有数据的一个左开右闭的区间。

还是这个案例数据(假设数据都是从1开始):

id(主键) c(普通索引) d(无索引)
5 5 5
10 10 10
15 15 15
20 20 20
25 25 25

对于下面这些区间的操作,会有对应的间隙锁:(0, 5], (5, 10], (10, 15], (15, 20], (20, 25], (25, 正无穷)。

什么意思呢?假如你的SQL查询的范围不同,那它锁住的区间就不同。比如:

-- 锁住(0, 5]
SELECT * FROM demo where id = 3;
-- 锁住(10, 15]
SELECT * FROM demo where id = 11;

间隙锁其实是“共享”的。也就是说,多个事务可以获取同一个区间的间隙锁。

网络异常,图片无法展示
|
间隙锁不互相阻塞

插入意向锁

插入意向锁 Insert Intention Locks,代表当前事务准备插入一行数据。使用INSERT/UPDATE/DELETE等语句会获得插入意向锁。

插入意向锁和插入意向锁之间是兼容的,只要插入的键值不同,就不会相互阻塞。比如以下两个SQL,在不同的事务中,哪怕它们在同一个间隙,只要没有间隙锁,就不会阻塞:

网络异常,图片无法展示
|
插入两条不同的记录不阻塞

但如果两个事务插入同一个key,那就会阻塞。

网络异常,图片无法展示
|
两个插入意向锁阻塞的情况

插入意向锁可以保证两个事务插入key不同的数据的时候不冲突,提升并发性。

但是间隙锁会阻塞插入意向锁!这也可以理解,因为InnoDB想在RR隔离级别就解决幻读问题。所以A事务用SELECT语句获取了一个间隙锁,自然不希望B事务在这个期间往这个间隙插入一条新的记录。

网络异常,图片无法展示
|
间隙锁阻塞插入意向锁


与索引的关系

不管哪种行级锁,行级锁的其实都是索引。所以在上面的demo中,如果对id(主键)或者column c(普通索引)操作,都会触发相应的行级锁,但如果对column d(无索引)做同样的操作,InnoDB就会对表中所有数据加锁,实际效果跟表级锁一样。

所以一定要注意,如果要上锁,需要注意是否走了索引,不要弄成了表级锁造成安全事故。


一个死锁案例

最后给一个关于间隙锁和插入意向锁的死锁案例吧,也是之前在项目上遇到过的真实案例。

过程

  • 事务A select ... for update 查找一个不存在的数据,获得间隙锁;
  • 事务B select ... for update 查找一个不存在的数据,获得相同位置的间隙锁;
  • 事务A insert into ... 到这个间隙,尝试获得插入意向锁,但被事务B持有的间隙锁阻塞。
  • 事务B insert into ... 到这个间隙,尝试获得插入意向锁,但被事务A持有的间隙锁阻塞,MySQL监测到死锁,回滚事务B;
  • 因为事务B回滚,所以事务B持有的间隙锁被释放,所以之前事务A的插入语句不再阻塞,顺利执行插入操作。

报错信息:Deadlock found when trying to get lock; try restarting transaction

用图把这个过程描述出来:

网络异常,图片无法展示
|
死锁过程

最终结果

事务A插入成功,事务B因为死锁被回滚。

总结

间隙锁主要还是用于防止幻读的情况,所以多个事务能够同时获取同一段间隙锁本身并没有问题,间隙锁能够阻塞插入意向锁也并没有问题。

而插入意向锁可以看成是一种特殊的间隙锁,是用于在同一个间隙,插入不同的数据,不会互相阻塞。它比普通间隙锁的数据一致性更低,但并发性能更好;

解决思路

普通间隙锁还是主要用于读操作防止幻读。所以我们在想进行插入操作的时候,其实没有必要对即将插入的间隙使用普通间隙锁,直接使用insert语句产生的插入意向锁就好了。

如果要保证数据的一致性,可以使用插入意向锁配合主键、唯一键等约束。

附录

那些在文章中用过的SQL:

-- 建表
create table demo
(
 id int unsigned not null,
 c int unsigned not null,
 d int unsigned not null,
 constraint demo_pk
  primary key (id),
 constraint idx_c
  unique (c)
);
-- 插数据
INSERT INTO demo VALUES
(5, 5, 5),
(10, 10, 10),
(15, 15, 15),
(20, 20, 20),
(25, 25, 25);
-- 禁止自动提交事务
SET AUTOCOMMIT = 0;
-- 开启事务
BEGIN;
-- 提交事务
COMMIT;
-- 会获取锁的查询
SELECT * FROM demo WHERE id < 5 FOR UPDATE ;


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
关系型数据库 MySQL 索引
mysql innodb 行锁解锁后出现1213 死表现象 Deadlock found when
mysql innodb 行锁解锁后出现1213 死表现象 Deadlock found when
232 0
mysql innodb 行锁解锁后出现1213 死表现象 Deadlock found when
|
存储 关系型数据库 MySQL
深入浅出InnoDB中的行锁
深入浅出InnoDB中的行锁
深入浅出InnoDB中的行锁
|
关系型数据库 MySQL 索引
MySQL 与InnoDB 下的锁做朋友 (四)行锁/记录锁
MySQL 与InnoDB 下的锁做朋友 (四)行锁/记录锁
MySQL 与InnoDB 下的锁做朋友 (四)行锁/记录锁
|
关系型数据库 MySQL
【MySQL】innodb行锁变表锁
【MySQL】innodb行锁变表锁
102 0
【MySQL】innodb行锁变表锁
|
SQL MySQL 关系型数据库
MySQL · 引擎分析 · InnoDB行锁分析
前言 理解InnoDB行锁,分析一条SQL语句会加什么样的行锁,会锁住哪些数据范围对业务SQL设计和分析线上死锁问题都会有很大帮助。对于InnoDB的行锁,已经有多篇月报进行了介绍,这里笔者借鉴前面月报的内容,综合自己的理解,对源码的基础实现做一个介绍(会包含部分表锁介绍),然后结合具体SQL语句分析加锁类型和加锁范围。
2123 0
|
关系型数据库 MySQL
InnoDB行锁,如何锁住一条不存在的记录?
《InnoDB,5项最佳实践,知其所以然?》发布后,不少同学留言希望讲讲MySQL的InnoDB行锁机制。要细聊MySQL的行锁,难以避免的要从事务的四种隔离级别说起。
524 0
|
关系型数据库 MySQL
Innodb:为什么lock in share mode在show engine看不到行锁信息
水平有限 有误请指出版本:Percona MySQL 5.7.22对于锁的学习我做了一些输出详细参考如下:https://github.com/gaopengcarl/percona-server-locks-detail-5.7.22.git其中有readme 一、问题提出 不知道有没有朋友和我一样用lock in share mode做加锁实验,但是却在show engine innodb status中看不到加锁信息,今天刚好有朋友在问@在树枝上吹风,今天就做了一下简单的debug,因为我也挺纳闷的。
4321 0
|
存储 算法 关系型数据库
MySQL探秘(七):InnoDB行锁算法
在上一篇《InnoDB一致性非锁定读》中,我们了解到InnoDB使用一致性非锁定读来避免在一般的查询操作(SELECT FOR UPDATE等除外)时使用锁。然而锁这个事情是无法避免的,数据的写入,修改和删除都需要加锁。今天我们就继续学习InnoDB锁相关的知识。
|
关系型数据库 MySQL 数据库
mysql innodb引擎下的行锁由于意外没有被释放,导致后面的请求无法继续,怎么办?
版权声明:欢迎转载,请注明沉默王二原创。 https://blog.csdn.net/qing_gee/article/details/80108576 在一个@Transactional注解的方法中进行调试,但莫名其妙的就报了下面这个错误: 从内容上看不出个所以然,大概的猜测是事务开启了,一直没有commit,导致MySQL的行被锁住了。
1616 0
|
关系型数据库 MySQL RDS
RDS for MySQL InnoDB 行锁等待和锁等待超时的处理
RDS for MySQL InnoDB 行锁等待和锁等待超时的处理   1. Innodb 引擎表行锁等待和等待超时发生的场景 2.Innodb 引擎行锁等待情况的处理 2.1 Innodb 行锁等待超时参数 innodb_lock_wait_timeout 2.2 大量
4929 0