MsSql

关注公众号 jb51net

关闭
首页 > 数据库 > MsSql > sql 数据库锁

SQL 数据库锁超清晰总结

作者:basketball616

本文详细总结SQL数据库锁机制,涵盖行锁、表锁、意向锁、更新锁等多种锁类型,解析它们的应用场景与实现方式,帮助读者优化并发性能,避免死锁,感兴趣的朋友一起看看吧

SQL 数据库锁超清晰总结

一、按锁粒度分(最核心分类)

1. 行锁(Row Lock)

2. 表锁(Table Lock)

3. 页锁(Page Lock)

4. 全局锁

二、按锁功能分(面试高频)

1. 共享锁 / 读锁(Shared Lock,S锁)

SELECT * FROM 表 LOCK IN SHARE MODE;

2. 排他锁 / 写锁(Exclusive Lock,X锁)

SELECT * FROM 表 FOR UPDATE;

3. 意向锁

表级锁,完全由系统自动管理,为了协调行级锁和表级锁。事务想给某行加锁前,会先在表级加个意向锁,声明“我想做某类操作”。

这样,当有事务想锁整张表时,只需检查表的意向锁,就知道有没有行被锁住,而不用一行行检查。

意向锁是和行锁搭配工作的,我们以事务 T1 执行为例:

-- 事务 T1
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;

它的内部加锁顺序是:

  1. 在 users 表上,申请并加上意向排他锁(IX)。
  2. 加表锁成功后,再在 id = 1 的行记录上,加上排他锁(X)。

如果此时事务 T2 想锁定整张表:

-- 事务 T2
LOCK TABLES users WRITE;
  1. T2 的请求需要 users 表上的排他锁。
  2. 系统检查发现,T1 已在表上持有 IX 锁。
  3. IX 和 X 锁冲突,因此 T2 的请求会进入等待,直到 T1 提交或回滚。

4. 更新锁

用于解决“先读后写”操作(如UPDATE的查找阶段)中的死锁问题。SQL Server 中常用,它允许共享读,但只允许一个事务获得更新锁,并最终升级为排他锁。
典型的 UPDATE 流程是两步:先找到行,再修改。如果最初用共享锁来“找行”,就埋下了死锁隐患。
死锁推演:

  1. 事务A SELECT … FOR SHARE 找到数据,持有该行的共享锁 (S)。
  2. 事务B 也 SELECT … FOR SHARE 同一行,共享锁兼容,也成功持有S锁。
  3. 事务A 执行 UPDATE,想把自己的S锁升级为排他锁 (X),但必须等事务B释放S锁。
  4. 事务B 也执行 UPDATE,同样想升级为X锁,但必须等事务A释放S锁。

双方互相等待,死锁就发生了。
更新锁(U锁)正是为了打破这个循环而设计的。

更新锁的核心规则

它的工作规则很简单:

  1. 与共享锁兼容:U锁和S锁可以共存,满足最初的“读”需求。
  2. 与自身互斥:一个资源上只能有一个U锁。这从源头上阻止了多个事务同时持有U锁并等待升级。
  3. 可直接升级为排他锁:U锁持有者可以将其直接升级为X锁,而无需先释放。

1. 安全的“先读后写”

这是最经典、也是最正确的用法。通过在 SELECT 阶段就加上 (UPDLOCK),确保后续更新安全无死锁。

BEGIN TRANSACTION;
-- 查询时立刻对该行加上更新锁(U)
SELECT * FROM Products WITH (UPDLOCK) 
WHERE ProductID = 1;
-- 判断逻辑,比如检查库存...
-- IF ...
-- 后续更新,此时U锁将无缝升级为排他锁(X)
UPDATE Products 
SET Stock = Stock - 1 
WHERE ProductID = 1;
COMMIT TRANSACTION;

此时,如果另一个事务也执行同样的 WITH (UPDLOCK) 查询,它的U锁请求会因为U锁互斥而立刻等待,这就避免了之前共享锁升级造成的死锁环。

2. 避免丢失更新

直接用 WITH (UPDLOCK) 做读取-修改-写回,也是实现悲观锁、防止并发丢失更新的一种手段。

-- 事务A
BEGIN TRANSACTION;
-- 读出当前值并锁定
SELECT @current_value = Balance FROM Accounts WITH (UPDLOCK) WHERE AccountID = 100;
-- 计算新值
SET @new_value = @current_value + 500;
-- 基于锁的保护进行更新
UPDATE Accounts SET Balance = @new_value WHERE AccountID = 100;
COMMIT TRANSACTION;

3. 在可序列化隔离级别下,用更新锁防止幻读

在需要范围查询并可能后续插入的场景下,可将 UPDLOCK 和 SERIALIZABLE 结合使用。

BEGIN TRANSACTION;
-- 检查订单是否存在,对查询范围施加更新锁和范围锁
IF NOT EXISTS (
    SELECT 1 FROM Orders WITH (UPDLOCK, SERIALIZABLE) 
    WHERE OrderDate = '2023-01-01' AND CustomerID = 5
)
BEGIN
    -- 如果不存在则插入
    INSERT INTO Orders (OrderDate, CustomerID) VALUES ('2023-01-01', 5);
END
COMMIT TRANSACTION;

与 SELECT … FOR UPDATE 的对比

总之,在 SQL Server 中,通过 WITH (UPDLOCK) 这个提示,你就能显式控制更新锁,它是实现安全“先读后写”操作的关键工具。

5. 自增锁

特指 MySQL 里 AUTO_INCREMENT 列的锁,保证自增 ID 唯一。它有多种锁模式(传统、连续、交错),可由 innodb_autoinc_lock_mode 参数控制。

自增锁(AUTO-INC Lock)是一种特殊的表级锁,专门用于保护 AUTO_INCREMENT 列的并发赋值。它和意向锁一样,完全由数据库自动管理,无法通过 SQL 显式调用。我们能控制的是它的行为模式

核心目标:保证主键唯一,而非连续

首先要明确,自增锁的核心任务是保证自增 ID 的唯一性并不保证连续性。出现回滚或冲突时,已分配的 ID 会被浪费,这是设计取舍。

三种工作模式

自增锁的行为由 MySQL 参数 innodb_autoinc_lock_mode 控制,有 0、1、2 三种。我们结合 INSERT 的几种类型来看,它们的核心区别在于锁的粒度和释放时机

模式行为特点主要影响
0-传统模式所有 INSERT 都加表级自增锁,语句执行完才释放。并发最低,主从复制最安全 (基于语句复制时 ID 一定连续)。
1-连续模式 (默认)简单插入:用轻量级互斥量,拿到所需 ID 就释放,不用等语句结束。
批量插入:同传统模式,加表级锁直到语句结束。
性能与安全的平衡缺点:二进制日志用 STATEMENT 格式时,批量插入的复制不安全,必须用 ROW 格式。
2-交错模式所有插入都立即释放锁,ID 分配是所有事务交错的。并发最高。缺点:任何基于 STATEMENT 的复制都不安全,且 ID 可能不连续。

核心使用方法:配置与排查

日常开发中,你的“使用方法”主要是这三点:

1. 根据场景设置模式
在配置文件 my.cnf 中设置:

[mysqld]
# 使用默认的连续模式,适合大多数场景
innodb_autoinc_lock_mode = 1
# 若主从复制用的是 STATEMENT 格式,可能需要更安全的传统模式
# innodb_autoinc_lock_mode = 0
# 若全用 ROW 格式复制且追求极高插入并发,可考虑交错模式
# innodb_autoinc_lock_mode = 2

2. 排查锁等待
自增锁在表级冲突,现象是大量 INSERT 卡在 “AUTO-INC lock waiting”。

-- 查看正在等待自增锁的线程
SELECT * FROM performance_schema.metadata_locks 
WHERE OBJECT_TYPE = 'TABLE' AND LOCK_TYPE = 'AUTO-INC';

结合 SHOW ENGINE INNODB STATUS\G 就能看到哪个事务长时间持有自增锁不释放。

3. 优化批量插入
在默认模式 1 下,要避免让简单的单行插入,被大的、不确定行数的批量插入阻塞。

-- 这种插入行数不确定,会持有表级自增锁直到结束,阻塞所有插入
INSERT INTO t (data) SELECT data FROM huge_table;
-- 如果业务允许,可手动分批提交,或考虑用程序先在外部取完数据再插入。

三种模式场景总结

切换到模式 2 前,要确保 replication 配置中 binlog_format 设置为 ROW,否则主从数据可能不一致。

三、按实现方式分

1. 乐观锁(Optimistic Lock)

与后面讲的记录锁、间隙锁等由数据库内核自动管理的悲观锁不同,乐观锁是一种应用层级的并发控制策略

它的核心思想是:假定冲突很少发生,操作时不加锁,只在最终提交更新时检查数据是否被修改过。 如果被改了,就回滚重试。

乐观锁的核心实现:版本号或时间戳

乐观锁不依赖 FOR UPDATE,而是在你的业务表里加一个字段,最常用的是 version

1. 表结构设计

在你的业务表中,必须有一个用于版本校验的字段。

-- 常用的版本号字段
ALTER TABLE accounts ADD COLUMN version INT NOT NULL DEFAULT 0;
-- 或者用时间戳
ALTER TABLE accounts ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;

2. 更新的标准 SQL 写法

逻辑是:读出数据时带出版本号,更新时在 WHERE 里校验这个版本号,同时把版本号加 1。

-- 1. 查询余额,同时获取当前版本号
SELECT balance, version FROM accounts WHERE account_id = 100;
-- 假设结果:balance = 500, version = 3
-- 2. 应用程序计算新余额
-- new_balance = 500 - 100 = 400
-- 3. 执行带版本号校验的更新
UPDATE accounts 
SET balance = 400, version = version + 1 
WHERE account_id = 100 AND version = 3;

核心判断:更新后的受影响行数。

完整的使用模式(含重试)

将查询、计算、更新、重试整合起来,就是乐观锁的标准实现。

# 伪代码示例
max_retry = 3
retry_count = 0
while retry_count < max_retry:
    # 1. 查询当前数据及版本
    cursor.execute("SELECT balance, version FROM accounts WHERE account_id = 100")
    row = cursor.fetchone()
    if not row:
        print("账户不存在")
        break
    current_balance = row['balance']
    current_version = row['version']
    # 2. 业务逻辑计算
    new_balance = current_balance - 100
    # 3. 带版本号条件的更新
    cursor.execute(
        "UPDATE accounts SET balance = %s, version = version + 1 "
        "WHERE account_id = %s AND version = %s",
        (new_balance, 100, current_version)
    )
    conn.commit()
    # 4. 检查结果
    if cursor.rowcount > 0:
        print("更新成功!")
        break
    else:
        retry_count += 1
        print(f"版本冲突,正在进行第 {retry_count} 次重试...")

乐观锁的适用与不适用场景

最适合的场景:

不适合的场景:

乐观锁与悲观锁的对比

特性乐观锁 (Optimistic)悲观锁 (Pessimistic)
假设冲突很少发生冲突很可能发生
实现应用层通过 version 字段校验数据库内核的行锁/表锁 (FOR UPDATE)
数据锁定时机只在提交更新时校验,全程无锁读取时就锁定数据
资源开销消耗 CPU 做重试,无锁等待消耗数据库锁和连接资源,事务可能长时间等待
最适场景读多写少,Web 应用写并发高,后台批处理
死锁风险(没有锁等待)

需要我接着讲讲如何在后端应用中,用 AOP 或拦截器封装这个重试逻辑吗?

2. 悲观锁(Pessimistic Lock)

与乐观锁不同,悲观锁的核心思想是:假定冲突一定会发生,所以在读取数据的瞬间就将其锁定,直到事务结束才释放

它由数据库内核实现,是之前聊过的行锁、表锁、临键锁等的具体应用。

使用方式:显式锁定读

悲观锁主要通过 SELECT ... FOR UPDATESELECT ... FOR SHARE 实现。它们必须在事务中使用,否则读完后锁立即释放,毫无意义。

语句加的锁其他事务还能做什么典型用途
SELECT ... FOR UPDATE排他锁 (X锁)只能读快照版本,不能加任何锁(S/X)。修改和 FOR UPDATE 都会被阻塞。锁定一行,准备后续修改。
SELECT ... FOR SHARE
(旧版:LOCK IN SHARE MODE)
共享锁 (S锁)可以读,也可以加 S 锁,但不能修改(会被阻塞)。保护数据在事务期间不被改动,但允许别人读。

标准使用流程

1. 经典“锁定-修改”

这是最常用的场景:先锁住数据,确保其间无人更改,再做更新。

START TRANSACTION;
-- 1. 对即将操作的数据加排他锁
SELECT stock FROM products WHERE id = 100 FOR UPDATE;
-- 假设 stock = 50
-- 2. 在应用层安全地做业务判断
-- if stock < 10 → ROLLBACK
-- 3. 执行更新
UPDATE products SET stock = stock - 10 WHERE id = 100;
COMMIT;

在事务提交前,任何其他想通过 SELECT ... FOR UPDATE 或修改这行的事务都会被阻塞。

2. 安全的“读取-插入”

当需要检查后再决定是否插入时,悲观锁是防止竞态最直接的方法。

START TRANSACTION;
-- 1. 尝试锁定还未存在的记录。若记录不存在,InnoDB会加间隙锁/临键锁
SELECT * FROM unique_emails WHERE email = 'test@example.com' FOR UPDATE;
-- 2. 如果没查到,则安全插入
INSERT INTO unique_emails (email, created_at) VALUES ('test@example.com', NOW());
COMMIT;

这个操作能保证,在检查和插入之间,不会有其他事务插入相同的值。

悲观锁的完整特点

决策参考:乐观锁 vs. 悲观锁

你可以根据这个对比来选择:

对比维度悲观锁乐观锁
冲突概率
并发模式读多写多,强数据一致性读多写少
事务模式短事务,无用户交互等待适用于长对话,如Web应用
实现成本完全由数据库负责需应用层额外实现版本号校验和重试
性能瓶颈集中在数据库锁和连接资源冲突多时,CPU浪费在重试上
死锁风险存在不存在

常见的应用场景

悲观锁是把利剑,用好了能清晰可靠地解决并发问题,但前提是事务设计必须短小精悍,索引条件精准。

四、按算法分(InnoDB 特有)

1. 记录锁(Record Lock)

锁定索引中的一条精确记录

WHERE id=1

记录锁(Record Lock)是 InnoDB 中最基础的锁之一,它直接锁定索引中的一条具体记录,用来防止其他事务修改或删除这条数据

和前面的意向锁、自增锁不同,记录锁是我们在日常写 SQL 时最能直接控制和感受到的锁

核心概念:锁的是索引记录

一个关键点是:记录锁总是锁定索引记录,而不是直接锁数据行。

InnoDB 的表是基于聚簇索引组织的,数据就存在叶子节点。所以,即使你 WHERE 条件里没有用到索引列,它也会退化为扫全表,并在扫描到的每一行聚簇索引记录上都加上锁。

记录锁的精确定义

如何使用记录锁?

你通过特定的 SQL 语句,在特定的隔离级别下,就能精确触发记录锁。

1. 显式加锁读取

这是最精确的控制方式,通过在 SELECT 语句后加上 FOR UPDATEFOR SHARE,可以锁定查询命中的索引记录。

SELECT ... FOR UPDATE
对扫描到的索引记录加上排他记录锁(X Lock)。这意味着在你的事务提交前,其他事务既不能给这些行加 X 锁(不能修改或删除),也不能加 S 锁(不能执行 SELECT ... FOR SHARE),但普通的非锁定读不受影响。

-- 事务 A
START TRANSACTION;
-- 假设 id 是主键。对 id=10 的主键索引记录加上 X 锁。
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 在提交前,其他事务无法 UPDATE/DELETE 这行,也无法 SELECT ... FOR UPDATE/SHARE 这行

SELECT ... FOR SHARE(MySQL 8.0+,之前叫 LOCK IN SHARE MODE
对扫描到的索引记录加上共享记录锁(S Lock)。它允许其他事务也加 S 锁,但会阻止加 X 锁。

-- 事务 A
START TRANSACTION;
-- 对 id=10 的主键索引记录加上 S 锁
SELECT * FROM users WHERE id = 10 FOR SHARE;
-- 其他事务可以继续读这行(加 S 锁),但不能修改它(加 X 锁会等待)
2. 隐式加锁:写操作

UPDATEDELETE 语句会自动对被修改/删除的索引记录加上排他记录锁(X Lock)

修改时

-- 事务 A
UPDATE users SET name = 'new_name' WHERE id = 10;
-- 会自动对 id=10 的主键索引记录加 X 锁。

删除时

-- 事务 A
DELETE FROM users WHERE id = 10;
-- 同样会对 id=10 的主键索引记录加 X 锁。

实践要点:索引的影响

锁是对索引记录加的。如果 WHERE 条件没有用到索引,就会导致锁表。

假设 users 表的 city 列没有索引,你在 RC 隔离级别下执行:

-- 事务 A
START TRANSACTION;
DELETE FROM users WHERE city = 'Beijing';
  1. 存储引擎行为:优化器会选择全表扫描。它会扫描整个主键索引。
  2. 加锁过程:InnoDB 会对扫描到的所有主键索引记录都加上 X 锁。
  3. 实际结果:即使某行的 city 不是 ‘Beijing’,在被扫描到时也会被锁。效果上等同于锁定了整张表,极大影响并发。
  4. 优化:只需给 city 列加上索引,DELETE 就能精确定位,只锁住相关的索引记录。

由上可知,在使用记录锁删改查带索引的列的一大好处就是避免全局锁表,造成其他事务无法进行

隔离级别的关键影响

记录锁的行为严重依赖隔离级别:

实践总结

你的目标在 RC 隔离级别下的操作锁的效果
强锁定一行,防止被修改SELECT ... FOR UPDATE该行的索引记录被加 X 锁,其他事务的写操作、FOR UPDATE/FOR SHARE 都会被阻塞。
弱锁定一行,允许别人读,但不许修改SELECT ... FOR SHARE该行的索引记录被加 S 锁,其他事务的写操作会被阻塞。
修改/删除数据(自动)UPDATE ... / DELETE ...自动对被操作行的索引记录加 X 锁。
避免锁表使用精确命中索引的 WHERE 条件只有符合条件的索引记录被锁,并发度最高。

2. 间隙锁(Gap Lock)

和记录锁锁定具体记录不同,间隙锁锁定的是索引记录之间的间隙(一个开区间),用来防止其他事务在这个间隙里插入新记录,是解决“幻读”问题的核心武器

间隙锁同样由 InnoDB 自动施加,你不能显式地“加一个间隙锁”,但可以通过隔离级别特定的查询条件来触发它。

核心规则:只在可重复读(RR)下生效

这是最重要的一点。间隙锁只在隔离级别设置为 REPEATABLE READ 时才工作。如果你用的是 READ COMMITTED,即便执行同样的 SELECT ... FOR UPDATE,InnoDB 也只会加记录锁,不会加间隙锁。

间隙锁的精确行为

如何“使用”间隙锁?

你不能直接写 LOCK GAP,但可以通过以下方式精确触发。

1. 通过锁定不存在的记录触发

当你查询一条不存在的记录并加上锁,就会产生间隙锁,锁住该记录本应落入的间隙。

-- 会话 A(隔离级别为 RR)
BEGIN;
-- 表中只有 id=5 和 id=10 两条记录
-- 尝试锁定 id=7(不存在),会触发间隙锁,锁住 (5, 10) 这个区间
SELECT * FROM users WHERE id = 7 FOR UPDATE;
-- 会话 B
BEGIN;
-- 会被阻塞,因为 id=6 落在被锁住的 (5, 10) 区间内
INSERT INTO users (id, name) VALUES (6, 'blocked');
2. 通过范围查询的边界触发

当范围查询 WHERE id > X 的终点“正无穷”没有记录时,也会触发间隙锁。

-- 会话 A
BEGIN;
-- 表中最大 id=10,那么 id>10 的区间是 (10, +∞)
-- 执行此查询会锁住 (10, +∞) 这个间隙
SELECT * FROM users WHERE id > 10 FOR UPDATE;
-- 会话 B
-- 会被阻塞,因为 id=15 落在 (10, +∞) 内
INSERT INTO users (id, name) VALUES (15, 'blocked');
3. 通过组合触发“临键锁”

更常见的情况是,你锁定了存在的行,InnoDB 会默认加上临键锁,其间的间隙部分就是由间隙锁提供的。

-- 会话 A
BEGIN;
-- 表中存在 id=10,查询 id <= 10 的两条记录(假设 5 和 10)
-- 可能会对 (5, 10] 和 (10, +∞) 加锁,其中 (5,10) 和 (10,+∞) 都是间隙锁
SELECT * FROM users WHERE id <= 10 FOR UPDATE;

线上排查与观察

在执行 SELECT ... FOR UPDATE 前后,可以通过 performance_schema 观察锁情况:

-- 查看当前事务持有的锁
SELECT 
    lock_type, lock_mode, lock_status, lock_data
FROM 
    performance_schema.data_locks
WHERE 
    engine_transaction_id = (SELECT trx_id FROM information_schema.innodb_trx WHERE trx_mysql_thread_id = CONNECTION_ID());

你会在 lock_mode 列中看到 X,GAP(纯粹间隙锁)或 X(临键锁,包含了间隙和记录),lock_data 则显示锁定的边界值。

一个常见的影响与应对

间隙锁的主要副作用是导致并发插入性能下降,容易引发死锁。比如两个事务互相持有对方要插入位置的间隙锁,然后都试图插入数据,就会产生死锁。

应对思路:

间隙锁是 InnoDB 在 RR 级别下保证数据一致性的基石,理解它的触发条件对排查死锁和性能抖动至关重要。需要我继续讲它最常组合的“临键锁”吗?
范围之间的空隙,防止幻读.比如有记录 1 和 10,间隙锁会锁住 (1,10),防止插入 id=5 的行。只在可重复读隔离级别下生效。

WHERE id BETWEEN 1 AND 10

3. 临键锁(Next-Key Lock)

临键锁是间隙锁和记录锁的组合,也是 InnoDB 在可重复读(RR)隔离级别下,用来防止幻读的核心默认锁。

和间隙锁一样,你无法直接“加一个临键锁”,但当你执行 SELECT ... FOR UPDATE 这类锁定读时,它就会被自动触发

为什么需要临键锁?

记录锁只能保护存在的行,间隙锁只能保护未来的行。如果只用一个,都无法彻底防幻读:

临键锁通过**“当前记录 + 它之前的间隙”**的组合,完美覆盖了这两种情况。

临键锁的精确定义

默认加锁规则:所有区间都是临键锁

在 RR 级别下,你执行一个范围查询的 SELECT ... FOR UPDATE,InnoDB 的默认行为就是给扫描到的所有区间加上临键锁。

假设表中有 id 索引,记录值为 5, 10, 15,可能的临键锁区间如下:

如何“使用”(触发)临键锁?

你通过查询的范围和条件,精确控制它加哪些区间的临键锁。

1. 范围查询触发

这是最标准的触发方式。

-- 假设表中有 id=5, 10, 15 三行
-- 会话 A
BEGIN;
-- 范围扫描,会锁住命中的所有临键锁区间
SELECT * FROM users WHERE id BETWEEN 8 AND 12 FOR UPDATE;

此时,id=10 被命中,锁会加到 (5, 10](10, 15] 这两个区间。效果是:

2. 等值查询触发(不存在时降级为间隙锁)

如果等值查询命中了记录,加的也是临键锁;如果没命中,则退化为间隙锁。

-- 会话 A:锁定存在的记录 id=10
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 会加临键锁 (5, 10],锁住 id=10 的记录,以及它前面的间隙 (5,10)
-- 会话 A:锁定不存在的记录 id=7
SELECT * FROM users WHERE id = 7 FOR UPDATE;
-- 记录不存在,退化为间隙锁 (5, 10),不锁任何记录。
3. 唯一索引等值查询的降级

这是最重要的优化。当查询条件是唯一索引,且精确命中一条记录时,InnoDB 会认为不再需要间隙锁来防幻读,临键锁会自动降级为单纯的记录锁

-- 假设 id 是主键
-- 会话 A
BEGIN;
-- 唯一索引 + 等值命中,只加记录锁,锁住 id=10
SELECT * FROM users WHERE id = 10 FOR UPDATE;
-- 会话 B(不会被阻塞)
INSERT INTO users (id, name) VALUES (9, 'ok');
-- 因为 (5,10) 的间隙锁被省略了,插入 id=9 是允许的。

实践中的直接影响与排查

临键锁的“左开右闭”设计,是很多锁冲突的根源。

一个经典死锁场景:
两个事务分别锁定对方的间隙并等待插入。

  1. 事务 ASELECT * FROM users WHERE id = 10 FOR UPDATE; 持有 (5, 10] 临键锁。
  2. 事务 BSELECT * FROM users WHERE id = 15 FOR UPDATE; 持有 (10, 15] 临键锁。
  3. 事务 AINSERT INTO users (id) VALUES (12); 想要 (10, 15) 的插入意向锁,被事务 B 的临键锁阻塞。
  4. 事务 BINSERT INTO users (id) VALUES (7); 想要 (5, 10) 的插入意向锁,被事务 A 的临键锁阻塞。

排查时,在 SHOW ENGINE INNODB STATUS 的输出中,你会看到 lock_mode X 后面缺少 GAP 字样,通常表示临键锁。 它直接锁定了记录本身。

临键锁是 RR 隔离级别默认行为,也是我们日常写锁定读时真正打交道的锁。理解它的触发和降级条件,是写好高并发 SQL 和排查死锁的基础。

五、最常出现的面试/工作问题总结

1. 什么情况下行锁会变表锁?

2. UPDATE 不加 WHERE 会加什么锁?

表锁!
全表锁定,严重阻塞业务。

3. 共享锁和排他锁的关系?

4. 乐观锁和悲观锁怎么选?

总结

到此这篇关于SQL 数据库锁总结的文章就介绍到这了,更多相关sql 数据库锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文