【MySQL】【锁的前置知识】数据库的锁有哪些?怎么看?锁的是什么?什么情况下会加什么锁?什么情况下会死锁?死锁和锁超时的处理一样么?
1 前言
数据库中的锁,是一个很大的问题,从哪看起呢?该怎么看呢?所以在看锁之前,了解一些相关的前置知识,然后再去细看不同的场景下会加什么样的锁方便你快速理解。
官网,当然我们这里看的 引擎是 InnoDB 哈,那我们从以下几个问题看起:
(1)数据库中的锁有哪些(怎么知道呢,网上的文章五花八门的各种都有,该如何下手)
(2)锁去哪看,怎么看一个表加了哪些锁呢,或者某个操作加了哪些锁呢
(3)锁的是什么
(4)体验死锁
2 环境准备
我的环境是:MySQL 8.0.16(不建议用 8 以下的,因为我本地有一个 5.7的,我看锁的信息有时候跟 8 对比着看不准),这是我腾讯云上的一台轻量级应用服务器上装的一个 MySQL。
我这里准备了一张表和几条数据:
CREATE TABLE `t_demo` ( `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键id', `name` varchar(64) DEFAULT NULL COMMENT '名字', `nick_name` varchar(64) DEFAULT NULL COMMENT '昵称', `password` varchar(128) DEFAULT NULL COMMENT '密码', `gender` tinyint(1) DEFAULT NULL COMMENT '性别 0女 1男', `birthday` datetime DEFAULT NULL COMMENT '出生年月', `identity_card` varchar(24) DEFAULT NULL COMMENT '身份证', `deleted` tinyint(1) DEFAULT NULL COMMENT '逻辑删除标志', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; INSERT INTO `demo`.`t_demo`(`id`, `name`, `nick_name`, `password`, `gender`, `birthday`, `identity_card`, `deleted`) VALUES (1, '111', '张飞', '11', 1, '2024-02-27 11:13:05', NULL, 0); INSERT INTO `demo`.`t_demo`(`id`, `name`, `nick_name`, `password`, `gender`, `birthday`, `identity_card`, `deleted`) VALUES (3, '333', '关于', '33', 1, '2024-02-27 11:13:07', NULL, 0); INSERT INTO `demo`.`t_demo`(`id`, `name`, `nick_name`, `password`, `gender`, `birthday`, `identity_card`, `deleted`) VALUES (5, '555', '刘备', '55', 1, '2024-02-27 11:13:09', NULL, 0);
你可能涉及到的 MySQL 服务相关操作:
安装参考,默认的配置位置:/etc/my.cnf
配置修改完可能涉及的重启:
# 重启 MySQL服务 systemctl restart mysqld.service # 列出所有已安装的服务,显示系统上所有可通过Systemd管理的服务 systemctl list-unit-files --type=service # 查看服务状态 systemctl status mysqld
3 锁
3.1 数据库锁的分类
想要知道标准答案,现在的互联网上的文章啥也都有,你在不知道的情况下,你也不知道它说的对不对或者还要自己去尝试验证,所以最好的就看它的官网或者从书上去找,这里我们看下官网的:
这里我按照我的理解从锁的覆盖面来划分下:
(1)全局锁:flush tables(全局锁)
(2)表锁:lock table(表锁)、意向锁、自增锁、MDL锁
(3)行锁:记录锁、间隙锁、临键锁、插入意向锁
MySQL 官网的 InnoDB 锁类型,另外我之前也写过一篇也可以参考。
可能会疑惑 flush tables(全局锁)、lock table(表锁)、MDL锁 怎么没看到上边的图里有呢?
因为这三个是 MySQL 来管理的,前置在 InnoDB,类似有一个仓库是 MySQL,仓库里边有 InnoDB 的内部仓,InnoDB 有自己的锁来管理自己的内部仓,但仓库的大门 MySQL 也有自己的锁。也就是层次位置不同哈。
锁的分类,具体的可以参考官网和我之前写的一篇哈,这里就不细看了。
3.2 锁怎么看,去哪看
我们首先认识一下区间表示:
(1,3)表示 3 的间隙锁
(1,3] 表示 3 的临键锁 = (1, 3) + [3]
(3,+oo) 表示 正无穷的间隙锁 正无穷这个特殊点它的间隙锁和临键锁可以理解为相等 因为毕竟这个正无穷不存在 是个宏观概念(别被这里的 3 误导,3只是你表里最大的行id值)
然后我们说怎么看锁,直接说答案,有一个原表,位置:`performance_schema`.data_locks,可以直接看哪个事务,对哪些表,加了什么样的锁:
还有事务可以配合着看,他俩处于不同的 schema 下噢:
select * from `performance_schema`.data_locks; select * from `information_schema`.INNODB_TRX;
困惑一:记得有个 information_schema,它跟 performance_schema 有什么区别么?
目的和用途:
information_schema
:它提供了关于MySQL数据库服务器的元数据信息。例如,你可以查询数据库、表、列、索引、权限、存储引擎等的详细信息。这是DBA和开发者常用的数据库,用于获取有关数据库结构的详细信息。performance_schema
:它提供了关于MySQL服务器性能的信息。这包括线程执行、锁定、存储引擎API调用、内存使用情况等的信息。它主要用于性能调优和监控。结构和内容:
information_schema
:它包含了许多视图(views),这些视图反映了MySQL服务器的各种元数据信息。performance_schema
:它包含了一系列的表,这些表提供了关于MySQL服务器性能的实时数据。访问权限:
- 默认情况下,所有用户都可以查询
information_schema
中的信息,但某些信息可能受到权限的限制。performance_schema
的访问权限可能受到更严格的控制,因为其中的数据可能涉及服务器的敏感性能信息。数据更新频率:
information_schema
中的数据通常是根据数据库的当前状态来更新的,当数据库结构发生变化时,它也会相应地更新。performance_schema
中的数据是实时更新的,它提供了有关MySQL服务器当前性能的即时信息。使用场景:
- 当你需要了解数据库的结构、表、列、索引等信息时,你会使用
information_schema
。- 当你需要监控MySQL服务器的性能,查找性能瓶颈或进行性能调优时,你会使用
performance_schema
。上述来自于我们的文心一言的回答哈,也就是说 information_schema 侧重于基础元数据信息,performance_schema 侧重于运行时性能数据信息。 同时这两个数据库都坚决不要主动修改,否则dead。
我们尝试一个,对某个数据加个 X 锁:
begin; # for update 对 id = 3 数据加写锁 select * from t_demo where id = 3 for UPDATE;
我们来看结果:
关注的:
LOCK_TYPE:
(1)TABLE 表示表级锁
(2)RECORD 表示行级锁
LOCK_MODE:
(1)IX、IS 意向锁
(2)X,REC_NOT_GAP 排它锁 行级别的 锁一条记录
(3)S,REC_NOT_GAP 共享锁 行级别的 锁一条记录
(4)X,GAP 排它锁,锁一个间隙
(5)S,GAP 共享锁,锁一个间隙
(6)X 排它锁 临键锁(记录锁+间隙锁)比如:(1,3】 表示 3的临键锁即 3的记录锁 + (1,3)的间隙锁
(7)X,INSERT_INTENTION 排它锁,插入意向锁
LOCK_STATUS:
(1)GRANTED:获得
(2)WAITING:阻塞等待
LOCK_DATA:
当 LOCK_TYPE = TABLE 即是表锁时,为空
当 LOCK_TYPE = RECORD 时,是多少就是多少,有个特殊的:supremum pseudo-record:表示正无穷 它的临键锁和间隙锁可以理解为相等
3.3 锁的是什么
从上边的表里,可以感觉到,或者从数据的存储来看,对于 InnoDB 来说,数据都是存储在 B+ 树上的,它的聚簇索引也就是主键索引的所有叶子节点的数据就是一个表的数据,其他索引的叶子节点都是记录主键ID,所以每次锁的是什么?就是锁索引。
锁的是索引,那锁的单位是什么?加锁的单位是临键锁也就是 LOCK_MODE = X,只不过在某些情况下 他会退缩成记录锁 LOCK_MODE = X,REC_NOT_GAP 或者退缩成间隙锁 LOCK_MODE = X,GAP
那我们来看几个例子:
现在表里有 1, 3 ,5 三条数据:
比如我想要一个退缩成间隙锁的,我们可以这样:
begin; -- 等值查询 不存在的情况下 select * from t_demo where id = 2 for UPDATE;
比如我想要一个退缩成记录锁的,你先想想,可以这样:
begin; -- 等值查询 存在的情况 select * from t_demo where id = 3 for UPDATE;
比如我不要退缩的,我要看一个临键锁,可以这样:
begin; -- 范围查询 select * from t_demo where id >= 3 for UPDATE;
3.4 死锁
趁热我们再体验体验数据库中的死锁,因为只要有锁,基本就有死锁的问题产生,官网的示例,然后我这里我写一个别的,比如我们有的时候会基于数据库的悲观锁方式来做微服务或者多服务的一个协同,比如我的这张表 t_demo 理解为消息表,我们要消费消息或者发送消息做幂等,可能会先查一下这个消息有没有发送过呢:
还是一样,我表里目前有三条数据:
现在我有两个线程,两个事务,消息id = 7777 的要进行操作,这时候,两个事务并发进行查询:
A事务查询:
begin; -- 查询 select * from t_demo where id = 7777 for UPDATE;
B事务查询:
begin; -- 查询 select * from t_demo where id = 7777 for UPDATE;
当前的锁信息:
可以看到A事务、B事务都对表加了一个 IX 的意向锁(意向锁都不冲突)和正无穷的临键锁(正无穷的临键锁都不冲突),所以两个事务都没阻塞,都没查到,两个事务都同时认为没有消费过消息,然后业务执行完,都会执行 insert 一条数据,表示处理过消息了,
begin; -- 查询 select * from t_demo where id = 7777 for UPDATE; -- 执行插入 insert into t_demo(id, name) VALUES(7777, '7777');
假如 A 事务执行的快,A先 insert , 我们这时候会发现 A 事务阻塞了,再看看锁信息:
可以看到会有一条 正无穷的X型的插入意向锁,那么它为什么会阻塞呢?因为 A 事务要加 X,而这时候 B 事务也持有正无穷的临键锁,所以 A 事务加不上锁,只能等待其它事务都释放掉,自己才能加,所以阻塞。阻塞时间过长的话, MySQL 默认会有锁超时时间的管理,超时了,会有提示噢:
我们在 A 阻塞的时候,事务B也要进行插入,这时候,事务B 就会报错,出现死锁(事务A 插入等着事务B 释放锁,事务B 插入也等着事务A 释放锁),然后尝试重启事务了,而事务A 插入成功:
锁信息已经没有 事务B了,事务B回滚了。
可以看到 MySQL 对于死锁的把控:
(1)死锁检测
(2)锁超时时间控制
另外的一个细节困惑:我们发现当锁申请超时的时候,会 try restarting transaction 重启事务,比如事务A 先 insert 的时候,这时候事务A还存活也没有回滚,而当事务B 因为死锁也是提示信息 try restarting transaction 重启事务,但是事务 B 这时候已经在 data_locks看不到了,已经回滚了,那么同样都是重启事务,怎么一个回滚一个还继续存活呢?
首先说超时的情况下:
在MySQL中,当一个事务尝试获取锁但等待超时(通常是因为其他事务持有该锁并且没有释放),默认情况下,该事务并不会因为锁申请超时而自动回滚。相反,它会收到一个错误,表明它无法获取所需的锁。
在InnoDB存储引擎中,当一个事务因为锁等待超时而失败时,它会收到一个错误码(通常是
ER_LOCK_WAIT_TIMEOUT
),并且该事务仍然保持活动状态。这意味着事务中的所有更改(即尚未提交的更改)仍然保留在数据库中,直到事务被明确地回滚或提交。要处理这种情况,应用程序需要捕获这个错误,并决定如何处理。通常,应用程序可以选择以下几种方式之一:
重试:应用程序可以尝试重新执行导致锁等待的操作。这可能在某些情况下是合适的,特别是当认为锁很快就会被释放时。
回滚:应用程序可以选择回滚事务,释放所有未提交的更改。这可以通过执行
ROLLBACK
命令来完成。提交:如果应用程序认为即使无法获取某些锁,事务中的其他更改仍然是有价值的,它可以选择提交事务。然而,这可能会导致数据不一致,因为部分操作可能已经成功而其他操作因为锁等待而失败。
忽略错误:在某些情况下,应用程序可能选择忽略锁等待超时的错误,并继续执行其他操作。这通常不是一个好的做法,因为它可能导致数据不一致或其他难以调试的问题。
正确的处理方式取决于应用程序的业务逻辑和其对数据一致性的要求。一般来说,当事务因为锁等待超时而失败时,最佳做法是回滚事务,以确保数据的一致性。
请注意,InnoDB存储引擎提供了
innodb_lock_wait_timeout
配置参数,用于设置事务等待锁的最长时间。如果事务在这个时间内未能获取锁,它将收到一个错误并可以继续执行其他操作。默认的超时值可能因MySQL版本和配置的不同而有所差异。SHOW VARIABLES LIKE '%innodb%'
再说出现死锁的情况下:
当在MySQL中出现死锁(deadlock)时,InnoDB存储引擎会主动检测到死锁并介入处理。InnoDB的死锁检测机制是为了防止两个或多个事务相互等待对方释放资源,导致它们都无法继续执行的情况。
具体来说,当InnoDB检测到死锁时,它会选择一个“受害者”事务进行回滚,以解除死锁状态。这个选择通常是基于一些启发式算法,比如选择插入、更新或删除行数最少的事务进行回滚,这样可以最大限度地减少回滚操作对系统性能的影响。
InnoDB通过查看
INFORMATION_SCHEMA.INNODB_TRX
表来确定哪个事务是“受害者”。该表包含了当前正在运行的所有事务的信息,包括它们正在等待的锁和其他相关信息。在死锁被检测并处理之后,被回滚的事务会释放它持有的所有锁,从而使得其他事务可以继续执行。这样可以确保数据库系统能够继续正常运行,而不会因为死锁而陷入停滞状态。
需要注意的是,虽然死锁检测和处理是InnoDB存储引擎的一部分,但并不是所有的数据库系统都支持这种机制。在某些其他数据库系统中,死锁可能会导致所有相关事务都被回滚,或者可能需要管理员手动介入来解决死锁问题。
4 小结
好啦,关于数据库锁的一些前置知识体验就到这里了,然后具体什么情况下会加什么锁,可以参考我的这篇,有理解不对的地方欢迎指正哈。