MySQL 锁机制全面解析

目录

  • [1. MySQL的锁类型](#1. MySQL的锁类型)
    • [1.1 全局锁](#1.1 全局锁)
    • [1.2 表锁](#1.2 表锁)
    • [1.3 行锁](#1.3 行锁)
    • [1.4 共享锁(读锁)](#1.4 共享锁(读锁))
    • [1.5 排它锁(写锁)](#1.5 排它锁(写锁))
    • [1.6 死锁](#1.6 死锁)
  • [2 乐观锁和悲观锁](#2 乐观锁和悲观锁)
    • [2.1 乐观锁](#2.1 乐观锁)
    • [2.2 悲观锁](#2.2 悲观锁)
  • [3 意向锁](#3 意向锁)
  • [4 间隙锁](#4 间隙锁)
  • [5 临键锁](#5 临键锁)
  • [6 插入意向锁](#6 插入意向锁)
  • [7. 事务隔离级别对锁的影响](#7. 事务隔离级别对锁的影响)
    • [6.1 读未提交(Read Uncommitted)](#6.1 读未提交(Read Uncommitted))
    • [6.2 读已提交(Read Committed)](#6.2 读已提交(Read Committed))
    • [6.3 可重复读(Repeatable Read)](#6.3 可重复读(Repeatable Read))
    • [6.4 串行化(Serializable)](#6.4 串行化(Serializable))
    • [6.5 为什么mvcc无法防止幻读](#6.5 为什么mvcc无法防止幻读)

1. MySQL的锁类型

MySQL按锁的粒度分主要分为全局锁、表锁和行锁。锁类型分为共享锁和排它锁。

1.1 全局锁

全局锁是一种锁定机制,它可以对整个数据库或特定的资源进行锁定。

全局锁的作用是确保在特定的操作期间,防止其他并发操作对受保护的资源进行修改。这有助于维护数据的一致性完整性。用于全库备份、大规模数据迁移。

列举:

开启全局锁:FLUSH TABLES WITH READ LOCK(这是一种全局读锁,用于在执行一些全局操作时阻止其他写入操作。)

开启数据备份:mysqldump -uroot -p 数据库名 > /home/backup.sql

解除全局锁:unlock tables

1.2 表锁

MyISAM 存储引擎默认表锁、InnoDB 存储引擎默认行锁

在MySQL中,对MyISAM表的读操作,会自动加上读表锁,对MyISAM表的写操作,会自动加上写表锁。

InnoDB引擎在必要情况下会使用表锁,但主要是使用行锁来实现多版本并发控制(MVCC) ,它能提供更好的并发性能和更少的锁冲突。

innoDB存储引擎下行锁升级为表锁场景:

  • 查询涉及到索引范围扫描
typescript 复制代码
 例如: SELECT * FROM orders WHERE order_id BETWEEN 100 AND 200
 虽然带有 BETWEEN 表达式的 SELECT 查询通常会使用行级锁,但是数量较大时会升级为表锁,因为InnoDB 认为多个行锁会占用过多资源。
  • 一整张表进行删除
typescript 复制代码
DELETE FROM orders
  • 整张表进行更新
typescript 复制代码
UPDATE orders SET status = 'processed'
  • 为整张表添加或者删除索引时,会对整张表加锁
typescript 复制代码
ALTER TABLE Orders ADD INDEX (Column); -- 加锁整张表完成添加索引操作
  • 某些情况下,MySQL优化器会选择全表扫描,此时对全表加锁。例如,如果在 WHERE 或 JOIN 的 ON

    子句中使用了列的函数表达式,那么 InnoDB 存储引擎不能使用行锁实现高效的索引扫描,会退化为表级锁定。

  • 使用 LOCK TABLES 命令显式加表锁。

typescript 复制代码
LOCK TABLES Orders WRITE; -- 显式锁定整张表
INSERT INTO Orders(ID, Total) VALUES (1, 100);
UNLOCK TABLES;

1.3 行锁

InnoDB存储引擎加锁 默认是行锁,例如 对某行数据添加共享锁、排它锁,都称为行锁

  • SELECT ... FOR UPDATE:这种查询会对选定的行添加一个排他锁(×锁),这意味着其他事务不能修改这些行,也不能对这些行添加共享锁。
  • SELECT ... LOCK IN SHARE MODE:这种查询会对选定的行添加一个共享锁(S锁) ,这意味着其他事务不能修改这些行,但可以对这些行添加共享锁。
  • INSERT:插入操作会对新添加的行添加一个排他锁(X锁)。
  • UPDATE:更新操作会对被更新的行添加一个排他锁(×锁) 。
  • DELETE:删除操作会对被删除的行添加一个排他锁(×锁) 。

注:同一个事务中,被加上行锁的数据可以被访问

.锁升级:如果一个事务试图锁定的行过多,InnoDB可能会将锁从行级升级为表级,这就可能导致更多的锁冲突。

1.4 共享锁(读锁)

共享锁优点是允许一个资源被多个事务读取,但不能被任何事务写入。

typescript 复制代码
START TRANSACTION;
SELECT * FROM table_name WHERE id = 10 LOCK IN SHARE MODE;

这个查询会在'table_name'的'id=10'这一行上放置一个共享锁。起效后,任意数量的其他事务可以对此行设置共享锁并读取,但无法获取排它锁进行修改,直到你结束事务为止。

1.5 排它锁(写锁)

排它锁允许事务独占某个资源。其他事务无法进行读取和写入。

typescript 复制代码
START TRANSACTION;
SELECT * FROM table_name WHERE id = 20 FOR UPDATE;

这个查询会在'table_name'的'id=20'这一行上放置一个排他锁。起效后,同一时间只有这个事务可以读取和修改这一行,所有其他尝试获取这行的锁(共享锁或排他锁)的操作都会被阻塞,直到你结束事务为止。

注:同一事务 中不同程度的锁可被升级,例如,先给某一行设置共享锁,然后在需要对此行进行修改时升级为排他锁。但是,一旦一个锁被设置,就无法在相同事务中降级。(在事务结束前,无法将排他锁降级为共享锁)。

1.6 死锁

在 MySQL 数据库中,死锁是指两个或多个事务相互等待对方所持有的资源,从而导致它们无法继续执行的情况。当发生死锁时,这些事务将会无限期地相互等待,除非通过干预来解决死锁。

下面是一些可能导致死锁的情况:

  • 事务顺序交叉:当多个事务同时访问数据时,如果它们的操作序列交叉且相互依赖,可能会导致死锁。例如,事务 A 锁定资源 X,并等待锁定资源Y,而事务 B 正好相反,这将导致两个事务相互等待对方的锁而发生死锁。
  • 索引顺序不一致:当多个事务以不同的顺序访问相同的资源时,可能会引发死锁。例如,事务 A 首先锁定资源 X,再锁定资源 Y;而事务 B先锁定资源 Y,再锁定资源 X。这种情况下,如果事务 A 和事务 B 同时执行,就有可能发生死锁。
  • 长时间事务:如果一个事务持有锁的时间过长,其他事务可能会因为等待所需资源被阻塞,从而导致死锁。长时间运行的事务可能会导致锁等待链条的增长,增加死锁的风险。
  • 锁粒度过高:当锁的粒度过高时,可能会增加发生死锁的可能性。例如,如果对整个表进行锁定而不是仅锁定需要的行或记录,就会增加死锁的概率。
  • 并行执行:当多个并发事务同时执行时,由于竞争相同资源而可能导致死锁。如果没有正确处理并发访问,例如使用适当的锁定机制和事务隔离级别,就会增加死锁的风险。

为了处理或避免死锁,可以采取以下几种方法:

  • 设置适当的事务隔离级别,确保事务之间的隔离性。
  • 尽量缩小事务持有锁的时间,减少锁冲突的可能性。
  • 使用合适的索引,以避免全表扫描或不必要的锁定。
  • 对事务执行顺序进行优化,以减少死锁的可能性。
  • 监控和检测死锁,并及时处理解决。

2 乐观锁和悲观锁

2.1 乐观锁

乐观地认为并发访问不会造成数据冲突,只在更新时检查是否有冲突。乐观锁和CAS的关系可以用"乐观锁是一种思想,CAS是一种具体的实现"来理解。

当使用CAS操作修改数据时,如果版本号不匹配或者其他线程已经修改了要操作的数据,CAS会返回失败。这时候,程序可以再次尝试CAS操作,也就是进行自旋重试,直到CAS操作成功。

因此,CAS操作已经内置了自旋重试的机制,避免了使用额外的自旋锁。

适用场景 :适用于并发较低(高并发场景每次修改了去对比,还不如让加锁阻塞排队执行)、读多写少的场景,相信数据多数情况下不会发生冲突,只在更新时进行检查,以减少对共享资源的争用。

java中常见悲观锁实现:可以使用java.util.concurrent.atomic包中的原子类,比如AtomicInteger、AtomicLong等,来实现CAS操作。

mysql实现乐观锁:版本号、时间戳

2.2 悲观锁

悲观地认为并发访问会造成数据冲突,因此在访问共享资源之前就会进行加锁,确保同一时刻只有一个线程能够访问。

适用场景 :适用于高并发写多的场景,通过加锁保护共享资源,确保并发访问时不会造成数据不一致性。

java中常见悲观锁实现:synchronized 关键字、ReentrantLock(可重入锁)

mysql中实现悲观锁SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE

3 意向锁

意向锁是表锁,用于表明某个事务有意锁定某个表中的某些行或区间。主要用于协调行锁和表锁的关系,以优化InnoDB的加锁策略。意向锁的主要目的是为了提高并发性能并减少锁冲突。

作用 :当有事务A有行锁时, MySQL会自动为该表添加意向锁,事务B如果想申请整个表的写锁,那么不需要遍历每一行判断是否存在行锁,而直接判断是否存在意向锁,增强性能。

意向共享锁:给行数据添加共享锁(读锁)时,会自动给表添加意向共享锁。例: SELECT ... LOCK IN SHARE MODE

意向排它锁:给行数据添加排它锁(写锁)时,会自动给表添加意向排它锁。例: SELECT ... FOR UPDATE

两个事务间锁的兼容性:

事务A/事务B 意向共享锁(IS) 意向排他锁(IX)
共享锁(S) 兼容 互斥
排它锁(X) 互斥 互斥

4 间隙锁

间隙锁是MySQL InnoDB存储引擎RR隔离级别引入的一种解决幻读的锁机制 。它锁定的不是具体的行记录,而是两个索引之间一个左开右开的区间 ,这样可以防止新的记录插入到该间隙,确保数据的一致性和事务的隔离性。间隙锁之间不互斥,但是间隙锁和插入语句互斥

间隙锁的存在,主要是为了解决幻读问题。通常间隙锁只存在于可重复读(RR)隔离级别下,在RC隔离级别下不常见。

特殊情况参考:MySQL · 引擎特性 · InnoDB unique check 的问题

产生间隙锁方式:普通索引锁定多列唯一索引唯一索引锁定多行记录

表结构如下,其中id是主键索引,age是普通索引

age对应的间隙锁就是(-,20),(20,22),(22,27),(27,28),(28,+)

举例 1:

// 事务 A,则会主动给(22,27)这个区间加间隙锁。(22,27]会存在临键锁
START TRANSACTION;
SELECT * FROM `user` where  age =23 for update;

同时当age=22时,会给id>2的加上间隙锁,因为age是普通索引,他上面的锁还会上升到对应的主键上,仅age=22时会存在主键锁。

推荐参考:InnoDB 锁定的综合(和动画)指南

可以用:SELECT * FROM performance_schema.data_locks; 查看加锁情况,但不会显示间隙锁

第一个锁是意向锁,第二个锁是排它锁

rr级别下间隙锁引起的死锁:1 降低隔离级别到rc,2 分布式锁

5 临键锁

临键锁是由间隙锁记录锁(行锁)组成的一种特殊锁,是一个左开右闭的索引区间。也是为了解决幻读问题,只存在于非唯一索引

6 插入意向锁

插入意向锁是一种特殊的间隙锁,表示事务想要在某个间隙中插入新记录。多个事务可以同时对一个间隙持有插入意向锁,但只有当间隙内没有任何其他锁(包括临键锁和间隙锁)时,事务才能成功插入新记录。只在insert语句时存在

作用:提高并发插入性能。

特征:

  • 插入意向锁之间不排斥、可以同时对一个区间进行插入。
  • 插入意向锁和间隙锁、临键锁排斥

举例:

// 事务1,会给age(22,27)加上插入意向锁
start TRANSACTION;
insert into user(age,name) values(23,'465')

// 事务2
insert into user(age,name) values(24,'465')

因此事务2能执行成功,因为插入意向锁就是为了解决并发插入问题。试想如果没有插入意向锁,事务1是不是加的就是间隙锁,那么事务2就不会成功了

7. 事务隔离级别对锁的影响

MySQL InnoDB 支持四种异步隔离级别:READ UNCOMMITTED(读未提交)、READ COMMITTED(读已提交)、REPEATABLE READ(可重复读)和 SERIALIZABLE(串行化)。不同的隔离级别用不同的方法处理并发事务,以满足您的特定需求。

  • READ UNCOMMITTED:在此隔离级别下,事务可以读取未提交的数据。
  • READ COMMITTED:此隔离级别下,只能读取已经提交的数据。
  • REPEATABLE READ:在此隔离级别下,同一事务中的多次读取可以看到同一份数据。这是 MySQL 默认的隔离级别。
  • SERIALIZABLE:这是最高的事务隔离级别,它要求所有事务顺序执行。

如果需要了解mysql事务和隔离级别,可以参考:MySQL事务的四个特征(ACID)以及隔离级别

6.1 读未提交(Read Uncommitted)

该隔离级别允许事务读取未提交的数据,但在修改数据时仍然会加锁,防止其他事务同时对同一条数据进行修改,从而避免"脏写"。事务可以读取未提交的数据(即,脏读)。select 不加锁,update delete insert 会加行锁(排它锁)

typescript 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT * FROM Orders WHERE CustomerID = 1; -- 可以读取到未提交的数据

6.2 读已提交(Read Committed)

只能读取已经提交的数据读操作不会使用锁,写操作则会加上行锁 ,并且只有在事务提交后才释放。select 不加锁,update delete insert 会加行锁(排它锁)

typescript 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE Orders SET Total = Total + 100 WHERE CustomerID = 1; -- 对匹配的行加上排他锁

6.3 可重复读(Repeatable Read)

这是 MySQL 默认的隔离级别。在此隔离级别中,同一事务中的多次读取可以看到同一份数据。读操作(不使用 FOR UPDATE 或 LOCK IN SHARE MODE 句法)不会使用锁,写操作会加上行锁,并在事务结束后释放。select 不加锁,update delete insert 会加行锁(排它锁)

typescript 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
INSERT INTO Orders(CustomerID, Total) VALUES (1, 100); -- 加上排他锁,其他事务无法修改这一行,直到本事务结束

那么可重复读是如何解决多次读取的是同一份数据呢?,这是因为可重复读的实现主要依赖于 MySQL 的多版本并发控制(MVCC)。当一个事务开始时,数据库的当前状态会被记录下来,随后在这个事务中的所有读操作,看到的都是这个存储的版本,而不是最新的数据。

比如:

typescript 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM Orders WHERE CustomerID = 1;
-- 假设在此时,另一个事务修改了 CustomerID 为 1 的这条数据的值并且提交了
SELECT * FROM Orders WHERE CustomerID = 1; -- 在同一事务中,尽管另一个事务已经修改了数据,但你仍然能看到第一次查询的结果。因为你看到的是事务开始时的那个版本的数据。
COMMIT;

6.4 串行化(Serializable)

在串行化(Serializable)隔离级别下,事务会在读取的时候也设置锁, 并且会按顺序排队,类似于队列,即并发多个当前行的查询会排序一个一个执行。select 加行锁(共享锁),update delete insert 会加行锁(排它锁)

串行化级别可以解决幻读的问题。"幻读" 是指一个事务在读取了几行数据后,另一个并发事务插入了一些符合其查询条件的新行,如果前一个事务再次读取,会发现一些"从未存在"的新行,这些新行被称为"幻读"。

串行化通过锁住整张表,阻止其他事务插入新的行,从而防止了幻读的发生。

在串行化中,读数据会加上共享锁,增删改会加上排它锁。至于是否从行锁上升到表锁在于是否需要全表查询。同时除了锁定,串行化还遵循一个重要的规则------所有的操作都必须按照某种确定的顺序执行,即事务必须按照它们的提交顺序来串行执行,以确保数据的一致性和事务的隔离性。在这种级别下,即使是并发的事务,也会被数据库系统排队,按照提交顺序一个接一个地处理。这就是"串行化"一词的来源。

typescript 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
DELETE FROM Orders WHERE CustomerID = 1; -- 对匹配的行加上排他锁,并阻塞其他所有事务

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
SELECT COUNT(*) FROM Orders WHERE Total > 100; --假设返回10
-- 另一个事务插入了一个Total > 100的新订单并且提交
SELECT COUNT(*) FROM Orders WHERE Total > 100; --串行化下,此次查询会阻塞,等待插入新订单的事务完成。事务完成后,新的查询结果会返回新插入的行,即返回11
COMMIT;

6.5 为什么mvcc无法防止幻读

"幻读"是指在一个事务内读取某范围的记录时,另一个事务在此范围内插入了新的记录,当前事务在此范围内再次读取时,会发现一些之前不存在的记录,这就是"幻读"。

即使使用了 MVCC(多版本并发控制)以在同一事务内保持数据的一致性,通过多版本并发控制(MVCC)可以创建查询时点的数据快照,使得在一个事务内多次读取数据时始终能够得到一致的结果。"幻读"问题仍然可能发生,因为 MVCC 主要解决的是"非重复读"的问题,也就是防止一个事务读取到在此期间其他事务提交的数据。

然而,"幻读"是由其它事务提交了新的数据引起的。仅仅依靠 MVCC 是无法解决"幻读"问题的,因为在事务开始后,新提交的数据仍然可能影响到同一事务后续的查询结果。

例如:

typescript 复制代码
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;

-- 初始读取
SELECT * FROM Orders WHERE Total > 1000;

-- 此时,另一个事务插入了一个 Total > 1000 的记录并提交

-- 再次读取
SELECT * FROM Orders WHERE Total > 1000;  -- 这时会发现一个新的记录,这就是幻读

COMMIT;

为了解决幻读问题,需要升级到串行化(SERIALIZABLE)隔离级别。该隔离级别会在读取的数据范围上放置共享锁,防止其他事务插入新的行,从而解决了幻读问题。但这种方式的代价是并发性能的下降。

InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁(也就是where后的条件)

相关推荐
新知图书1 小时前
MySQL用户授权、收回权限与查看权限
数据库·mysql·安全
文城5211 小时前
Mysql存储过程(学习自用)
数据库·学习·mysql
沉默的煎蛋1 小时前
MyBatis 注解开发详解
java·数据库·mysql·算法·mybatis
C语言扫地僧1 小时前
MySQL 事务及MVCC机制详解
数据库·mysql
小镇cxy1 小时前
MySQL事物,MVCC机制
数据库·mysql
雾里看山2 小时前
【MySQL】 库的操作
android·数据库·笔记·mysql
꧁瀟洒辵1恛꧂3 小时前
从新手到高手的蜕变:MySQL 视图进阶全攻略
数据库·mysql
doubt。15 小时前
【BUUCTF】[RCTF2015]EasySQL1
网络·数据库·笔记·mysql·安全·web安全
小辛学西嘎嘎15 小时前
MVCC在MySQL中实现无锁的原理
数据库·mysql
咩咩大主教19 小时前
Go语言通过Casbin配合MySQL和Gorm实现RBAC访问控制模型
mysql·golang·鉴权·go语言·rbac·abac·casbin