当前位置: 首页 > news >正文

MySQL锁机制详解

文章目录

    • 一、锁的基本概念
      • 1.1 什么是锁
      • 1.2 锁的必要性
    • 二、锁的分类
      • 2.1 按锁粒度分类
        • 锁粒度对比
      • 2.2 全局锁(Global Lock)
        • 加锁命令
        • 使用场景
      • 2.3 表级锁
        • 类型1:表锁(Table Lock)
        • 类型2:元数据锁(MDL - Meta Data Lock)
        • 类型3:意向锁(Intention Lock)
      • 2.4 按锁模式分类
        • 共享锁(S Lock / 读锁)
        • 排他锁(X Lock / 写锁)
    • 三、InnoDB行锁详解
      • 3.1 行锁的类型
      • 3.2 记录锁(Record Lock)
      • 3.3 间隙锁(Gap Lock)
      • 3.4 临键锁(Next-Key Lock)
      • 3.5 行锁的加锁规则
        • 规则1:基本加锁原则
        • 规则2:索引失效时的加锁
      • 3.6 实战示例:理解加锁范围
    • 四、死锁问题
      • 4.1 什么是死锁
      • 4.2 死锁演示
      • 4.3 死锁检测
      • 4.4 死锁的产生条件
      • 4.5 避免死锁的方法
        • 方法1:按固定顺序访问资源
        • 方法2:一次性获取所有锁
        • 方法3:设置锁超时时间
        • 方法4:使用乐观锁
        • 方法5:降低事务隔离级别
        • 方法6:缩小事务范围
    • 五、锁等待和超时
      • 5.1 查看锁等待
      • 5.2 查看锁信息
      • 5.3 杀掉阻塞进程
    • 六、乐观锁与悲观锁
      • 6.1 悲观锁(Pessimistic Lock)
      • 6.2 乐观锁(Optimistic Lock)
      • 6.3 乐观锁 vs 悲观锁
    • 七、实战案例
      • 7.1 案例1:电商秒杀
      • 7.2 案例2:分布式锁
    • 八、锁优化建议
      • 8.1 减少锁冲突
      • 8.2 选择合适的隔离级别
      • 8.3 使用合适的索引
      • 8.4 监控锁等待
    • 总结
      • 核心要点


一、锁的基本概念

1.1 什么是锁

锁(Lock) 是数据库用来管理并发访问的机制,防止多个事务同时修改同一数据导致数据不一致。

核心作用

  • 保证数据一致性
  • 实现事务隔离
  • 控制并发访问

1.2 锁的必要性

没有锁的问题

时间线   事务A                    事务B
T1      读取:库存=10
T2                               读取:库存=10
T3      扣减:库存=9
T4                               扣减:库存=9
T5      写入:库存=9
T6                               写入:库存=9结果:两个订单,但库存只扣减了1(超卖!)

有锁的保护

时间线   事务A                    事务B
T1      加锁 + 读取:库存=10
T2                               等待锁...
T3      扣减:库存=9
T4                               等待锁...
T5      写入 + 释放锁
T6                               加锁 + 读取:库存=9
T7                               扣减:库存=8
T8                               写入 + 释放锁结果:正确扣减库存

二、锁的分类

2.1 按锁粒度分类

全局锁(Global Lock)↓
表级锁(Table Lock)↓
页级锁(Page Lock)- BDB引擎↓
行级锁(Row Lock)- InnoDB
锁粒度对比
锁类型粒度开销并发度死锁使用引擎
全局锁整个数据库最小最低所有
表锁整张表MyISAM、InnoDB
行锁单行记录可能InnoDB

2.2 全局锁(Global Lock)

定义:锁定整个数据库实例,使数据库变为只读状态。

加锁命令
-- 加全局读锁(FTWRL:Flush Tables With Read Lock)
FLUSH TABLES WITH READ LOCK;-- 此时:
-- ✅ 允许:SELECT查询
-- ❌ 禁止:UPDATE、INSERT、DELETE、DDL-- 释放锁
UNLOCK TABLES;
使用场景

场景1:全库逻辑备份

# 1. 加全局锁
mysql> FLUSH TABLES WITH READ LOCK;# 2. 备份数据
mysqldump -uroot -p --all-databases > backup.sql# 3. 释放锁
mysql> UNLOCK TABLES;

问题:整个数据库不可写,业务停摆!

更好的方案

# 使用--single-transaction参数(InnoDB)
mysqldump -uroot -p --single-transaction --all-databases > backup.sql# 原理:在可重复读隔离级别下,通过MVCC获取一致性视图
# 优势:不阻塞写操作

2.3 表级锁

类型1:表锁(Table Lock)

读锁(共享锁)

-- 加读锁
LOCK TABLES users READ;-- 当前会话:
-- ✅ 可以读users表
-- ❌ 不能写users表
-- ❌ 不能访问其他表-- 其他会话:
-- ✅ 可以读users表
-- ❌ 不能写users表(阻塞,等待锁释放)-- 释放锁
UNLOCK TABLES;

写锁(排他锁)

-- 加写锁
LOCK TABLES users WRITE;-- 当前会话:
-- ✅ 可以读写users表
-- ❌ 不能访问其他表-- 其他会话:
-- ❌ 不能读users表(阻塞)
-- ❌ 不能写users表(阻塞)-- 释放锁
UNLOCK TABLES;
类型2:元数据锁(MDL - Meta Data Lock)

自动加锁(MySQL 5.5+):

-- 事务开始时自动加MDL
START TRANSACTION;
SELECT * FROM users;  -- 自动加MDL读锁-- 其他会话尝试修改表结构
ALTER TABLE users ADD COLUMN age INT;  -- 阻塞!等待MDL写锁COMMIT;  -- 释放MDL锁

MDL锁的作用

  • 保护表结构不被并发修改
  • 防止读取到不一致的数据

查看MDL锁

-- MySQL 5.7+
SELECT * FROM performance_schema.metadata_locks;-- MySQL 8.0+
SELECT OBJECT_TYPE,OBJECT_SCHEMA,OBJECT_NAME,LOCK_TYPE,LOCK_STATUS,OWNER_THREAD_ID
FROM performance_schema.metadata_locks;
类型3:意向锁(Intention Lock)

定义:表级锁,表示事务准备在某行上加共享锁或排他锁。

意向共享锁(IS Lock):事务准备加行级共享锁
意向排他锁(IX Lock):事务准备加行级排他锁

作用:提升加表锁的效率。

示例

-- 事务A
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 自动加:IX锁(表级)+ X锁(行级)-- 事务B
LOCK TABLES users READ;
-- 检查意向锁,发现IX锁冲突,直接阻塞
-- 无需逐行检查是否有行锁

兼容性矩阵

ISIXSX
IS
IX
S
X

2.4 按锁模式分类

共享锁(S Lock / 读锁)

定义:多个事务可以同时持有共享锁,但不能修改数据。

-- 加共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 或(MySQL 8.0.1+)
SELECT * FROM users WHERE id = 1 FOR SHARE;-- 效果:
-- ✅ 其他事务可以读
-- ✅ 其他事务可以加共享锁
-- ❌ 其他事务不能加排他锁(阻塞)
排他锁(X Lock / 写锁)

定义:只有一个事务可以持有排他锁,其他事务不能读写。

-- 加排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;-- 效果:
-- ❌ 其他事务不能加共享锁(阻塞)
-- ❌ 其他事务不能加排他锁(阻塞)
-- ✅ 普通SELECT可以读(快照读,不加锁)

兼容性矩阵

共享锁(S)排他锁(X)
共享锁(S)✅ 兼容❌ 冲突
排他锁(X)❌ 冲突❌ 冲突

三、InnoDB行锁详解

3.1 行锁的类型

InnoDB支持3种行锁:

1. 记录锁(Record Lock)- 锁定单行记录2. 间隙锁(Gap Lock)- 锁定索引记录之间的间隙3. 临键锁(Next-Key Lock)- 记录锁 + 间隙锁- 锁定记录及其前面的间隙

3.2 记录锁(Record Lock)

定义:锁定索引记录本身。

示例

-- 准备数据
CREATE TABLE users (id INT PRIMARY KEY,name VARCHAR(50),age INT,KEY idx_age (age)
);INSERT INTO users VALUES 
(1, '张三', 20),
(5, '李四', 25),
(10, '王五', 30);-- 事务A:锁定id=5的记录
START TRANSACTION;
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- 加记录锁:锁定id=5这一行-- 事务B
SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- ✅ 不冲突,可以锁定
SELECT * FROM users WHERE id = 5 FOR UPDATE;  -- ❌ 冲突,阻塞
SELECT * FROM users WHERE id = 10 FOR UPDATE; -- ✅ 不冲突,可以锁定

查看锁信息

-- 查看当前锁
SELECT * FROM performance_schema.data_locks;-- 查看锁等待
SELECT * FROM performance_schema.data_lock_waits;

3.3 间隙锁(Gap Lock)

定义:锁定索引记录之间的间隙,防止其他事务在间隙中插入数据。

作用:解决幻读问题(可重复读隔离级别)。

示例

-- 数据:id = 1, 5, 10-- 事务A
START TRANSACTION;
SELECT * FROM users WHERE id > 5 AND id < 10 FOR UPDATE;
-- 加间隙锁:锁定(5, 10)这个间隙-- 事务B
INSERT INTO users VALUES (6, '赵六', 28);  -- ❌ 阻塞(在间隙中)
INSERT INTO users VALUES (7, '陈七', 29);  -- ❌ 阻塞(在间隙中)
INSERT INTO users VALUES (4, '孙八', 22);  -- ✅ 成功(不在间隙中)
INSERT INTO users VALUES (11, '周九', 35); -- ✅ 成功(不在间隙中)

间隙示意图

索引值:  1      5      10
间隙:  (-∞,1) (1,5) (5,10) (10,+∞)锁定 id > 5 AND id < 10:
锁定间隙 (5, 10)

注意

  • 间隙锁只在可重复读隔离级别下生效
  • 读已提交隔离级别下没有间隙锁

3.4 临键锁(Next-Key Lock)

定义:记录锁 + 间隙锁的组合,锁定记录及其前面的间隙。

默认行为:InnoDB默认使用临键锁。

示例

-- 数据:id = 1, 5, 10-- 事务A
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- 加临键锁:锁定 (1, 5] 
-- 即:间隙(1, 5) + 记录5-- 事务B
INSERT INTO users VALUES (2, '李四', 22);  -- ❌ 阻塞(在间隙中)
INSERT INTO users VALUES (4, '王五', 24);  -- ❌ 阻塞(在间隙中)
UPDATE users SET name = '李四二' WHERE id = 5;  -- ❌ 阻塞(锁定记录)
INSERT INTO users VALUES (6, '赵六', 26);  -- ✅ 成功(不在锁定范围)

临键锁范围

索引值:  1      5      10SELECT ... WHERE id = 5 FOR UPDATE
锁定:(1, 5]SELECT ... WHERE id <= 5 FOR UPDATE
锁定:(-∞, 1] + (1, 5]SELECT ... WHERE id > 5 FOR UPDATE
锁定:(5, 10] + (10, +∞)

3.5 行锁的加锁规则

规则1:基本加锁原则
-- 1. 唯一索引等值查询
--    - 记录存在:加记录锁
--    - 记录不存在:加间隙锁SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- id存在:记录锁(5)
-- id不存在:间隙锁-- 2. 唯一索引范围查询
--    - 加临键锁SELECT * FROM users WHERE id > 5 FOR UPDATE;
-- 临键锁:(5, 10], (10, +∞)-- 3. 非唯一索引等值查询
--    - 加临键锁 + 间隙锁SELECT * FROM users WHERE age = 25 FOR UPDATE;
-- 假设age=25的记录id=5
-- 临键锁:age索引上的(20, 25]
-- 间隙锁:age索引上的(25, 30)
-- 记录锁:主键id=5-- 4. 非唯一索引范围查询
--    - 加临键锁SELECT * FROM users WHERE age > 25 FOR UPDATE;
-- 临键锁:(25, 30], (30, +∞)
规则2:索引失效时的加锁
-- 没有索引或索引失效:全表扫描 + 表锁
SELECT * FROM users WHERE name = '张三' FOR UPDATE;
-- name没有索引
-- 结果:锁定整个表(所有行)

危险:索引失效导致锁升级为表锁,严重影响并发!


3.6 实战示例:理解加锁范围

-- 准备数据
CREATE TABLE t (id INT PRIMARY KEY,c INT,d INT,KEY idx_c (c)
);INSERT INTO t VALUES 
(0, 0, 0),
(5, 5, 5),
(10, 10, 10),
(15, 15, 15),
(20, 20, 20),
(25, 25, 25);

场景1:主键等值查询

-- 事务A
SELECT * FROM t WHERE id = 10 FOR UPDATE;
-- 加锁:记录锁(id=10)-- 事务B
UPDATE t SET d = d + 1 WHERE id = 5;   -- ✅ 成功
UPDATE t SET d = d + 1 WHERE id = 10;  -- ❌ 阻塞
UPDATE t SET d = d + 1 WHERE id = 15;  -- ✅ 成功

场景2:主键范围查询

-- 事务A
SELECT * FROM t WHERE id >= 10 AND id < 15 FOR UPDATE;
-- 加锁:
-- - 临键锁:(5, 10]
-- - 临键锁:(10, 15]
-- - 间隙锁:(15, 20) - 但15不包含,实际是(10, 15)-- 事务B
INSERT INTO t VALUES (8, 8, 8);   -- ❌ 阻塞(在(5,10]中)
INSERT INTO t VALUES (12, 12, 12); -- ❌ 阻塞(在(10,15)中)
INSERT INTO t VALUES (16, 16, 16); -- ✅ 成功

场景3:非唯一索引查询

-- 事务A
SELECT * FROM t WHERE c = 10 FOR UPDATE;
-- 加锁:
-- - c索引上:临键锁(5, 10],间隙锁(10, 15)
-- - 主键上:记录锁(id=10)-- 事务B
INSERT INTO t VALUES (8, 8, 8);   -- ❌ 阻塞(c=8在间隙中)
INSERT INTO t VALUES (12, 12, 12); -- ❌ 阻塞(c=12在间隙中)
UPDATE t SET d = d + 1 WHERE id = 10;  -- ❌ 阻塞(主键锁)
UPDATE t SET d = d + 1 WHERE c = 5;    -- ✅ 成功

四、死锁问题

4.1 什么是死锁

定义:两个或多个事务相互等待对方持有的锁,形成循环等待。

经典示例

时间线   事务A                        事务B
T1      START TRANSACTION;
T2                                   START TRANSACTION;
T3      UPDATE t SET c=1 WHERE id=1;-- 持有id=1的锁
T4                                   UPDATE t SET c=2 WHERE id=2;-- 持有id=2的锁
T5      UPDATE t SET c=3 WHERE id=2;-- 等待id=2的锁 ←
T6                                   UPDATE t SET c=4 WHERE id=1;-- 等待id=1的锁 ←死锁!循环等待

4.2 死锁演示

-- 准备数据
CREATE TABLE accounts (id INT PRIMARY KEY,balance DECIMAL(10,2)
);INSERT INTO accounts VALUES (1, 1000), (2, 500);-- 终端1(事务A)
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 持有id=1的锁-- 终端2(事务B)
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 持有id=2的锁-- 终端1
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 等待id=2的锁...-- 终端2
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 等待id=1的锁...-- MySQL检测到死锁,自动回滚其中一个事务
-- 错误:ERROR 1213 (40001): Deadlock found when trying to get lock

4.3 死锁检测

-- 查看死锁信息
SHOW ENGINE INNODB STATUS\G-- 输出示例:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2024-01-15 10:30:45*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 10, OS thread handle 140123456, query id 100 localhost root updating
UPDATE accounts SET balance = balance + 100 WHERE id = 2*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 3 n bits 72 index PRIMARY of table `test`.`accounts` trx id 12345 lock_mode X locks rec but not gap waiting*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 8 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 11, OS thread handle 140123457, query id 101 localhost root updating
UPDATE accounts SET balance = balance - 100 WHERE id = 1*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 0 page no 3 n bits 72 index PRIMARY of table `test`.`accounts` trx id 12346 lock_mode X locks rec but not gap*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 3 n bits 72 index PRIMARY of table `test`.`accounts` trx id 12346 lock_mode X locks rec but not gap waiting*** WE ROLL BACK TRANSACTION (2)

4.4 死锁的产生条件

四个必要条件

  1. 互斥条件:资源不能被共享
  2. 持有并等待:持有资源的同时等待其他资源
  3. 不可剥夺:资源不能被强制释放
  4. 循环等待:形成资源请求的环路

打破任一条件即可避免死锁


4.5 避免死锁的方法

方法1:按固定顺序访问资源
-- ❌ 错误:不同顺序访问
-- 事务A:先锁id=1,再锁id=2
-- 事务B:先锁id=2,再锁id=1  ← 死锁!-- ✅ 正确:统一顺序访问
-- 所有事务都按id升序访问
-- 事务A:先锁id=1,再锁id=2
-- 事务B:先锁id=1,再锁id=2  ← 不会死锁,B等待A释放id=1

代码实现

public void transfer(Long fromId, Long toId, BigDecimal amount) {// 按ID大小排序,统一访问顺序Long firstId = Math.min(fromId, toId);Long secondId = Math.max(fromId, toId);// 先锁较小的IDAccount first = accountMapper.selectByIdForUpdate(firstId);// 再锁较大的IDAccount second = accountMapper.selectByIdForUpdate(secondId);// 执行转账逻辑if (fromId.equals(firstId)) {first.setBalance(first.getBalance().subtract(amount));second.setBalance(second.getBalance().add(amount));} else {first.setBalance(first.getBalance().add(amount));second.setBalance(second.getBalance().subtract(amount));}accountMapper.update(first);accountMapper.update(second);
}
方法2:一次性获取所有锁
-- 一次性锁定所有需要的资源
START TRANSACTION;
SELECT * FROM accounts WHERE id IN (1, 2) FOR UPDATE;
-- 同时锁定id=1和id=2-- 执行业务逻辑
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;COMMIT;
方法3:设置锁超时时间
-- 查看锁等待超时时间
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- 默认50秒-- 设置超时时间
SET SESSION innodb_lock_wait_timeout = 5;  -- 5秒超时-- 超时后自动回滚,抛出异常
-- ERROR 1205 (HY000): Lock wait timeout exceeded
方法4:使用乐观锁
-- 不使用悲观锁,改用版本号机制
UPDATE accounts 
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = #{currentVersion};-- 更新失败则重试
方法5:降低事务隔离级别
-- 从可重复读降低到读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;-- 减少间隙锁,降低死锁概率
方法6:缩小事务范围
// ❌ 不好:事务范围过大
@Transactional
public void complexOperation() {// 大量业务逻辑// ...// 数据库操作accountMapper.update(...);// 更多业务逻辑// ...
}// ✅ 更好:缩小事务范围
public void complexOperation() {// 业务逻辑// ...// 只在必要时开启事务transactionTemplate.execute(status -> {accountMapper.update(...);return null;});// 更多业务逻辑// ...
}

五、锁等待和超时

5.1 查看锁等待

-- 方法1:SHOW PROCESSLIST
SHOW FULL PROCESSLIST;-- 方法2:information_schema
SELECT r.trx_id AS waiting_trx_id,r.trx_mysql_thread_id AS waiting_thread,r.trx_query AS waiting_query,b.trx_id AS blocking_trx_id,b.trx_mysql_thread_id AS blocking_thread,b.trx_query AS blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;-- 方法3:sys库(推荐)
SELECT * FROM sys.innodb_lock_waits;

5.2 查看锁信息

-- MySQL 8.0+
SELECT ENGINE_LOCK_ID,ENGINE_TRANSACTION_ID,OBJECT_SCHEMA,OBJECT_NAME,INDEX_NAME,LOCK_TYPE,LOCK_MODE,LOCK_STATUS,LOCK_DATA
FROM performance_schema.data_locks;-- 查看锁等待
SELECT REQUESTING_ENGINE_LOCK_ID,BLOCKING_ENGINE_LOCK_ID,REQUESTING_THREAD_ID,BLOCKING_THREAD_ID
FROM performance_schema.data_lock_waits;

5.3 杀掉阻塞进程

-- 1. 找到阻塞的进程ID
SELECT * FROM sys.innodb_lock_waits;-- 2. 杀掉进程
KILL 12345;  -- 12345是进程ID-- 或者杀掉查询
KILL QUERY 12345;

六、乐观锁与悲观锁

6.1 悲观锁(Pessimistic Lock)

思想:假设会发生并发冲突,每次操作都加锁。

实现:数据库锁机制(FOR UPDATE)

-- 悲观锁示例:库存扣减
START TRANSACTION;-- 1. 查询并锁定
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 其他事务无法修改,必须等待-- 2. 检查库存
IF stock > 0 THEN-- 3. 扣减库存UPDATE products SET stock = stock - 1 WHERE id = 1;
END IF;COMMIT;

优点

  • ✅ 保证数据强一致性
  • ✅ 适合写多读少的场景
  • ✅ 避免了重试

缺点

  • ❌ 并发性能低(阻塞)
  • ❌ 可能产生死锁
  • ❌ 锁持有时间长

6.2 乐观锁(Optimistic Lock)

思想:假设不会发生并发冲突,只在更新时检查数据是否被修改。

实现方式1:版本号

-- 1. 添加版本号字段
ALTER TABLE products ADD COLUMN version INT DEFAULT 0;-- 2. 查询商品
SELECT id, stock, version FROM products WHERE id = 1;
-- 假设:stock=10, version=5-- 3. 更新时检查版本号
UPDATE products 
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;-- 4. 检查影响行数
-- 如果为0,说明版本号已变化,更新失败
-- 如果为1,说明更新成功

实现方式2:时间戳

-- 1. 添加时间戳字段
ALTER TABLE products ADD COLUMN update_time TIMESTAMP;-- 2. 查询商品
SELECT id, stock, update_time FROM products WHERE id = 1;-- 3. 更新时检查时间戳
UPDATE products 
SET stock = stock - 1, update_time = NOW()
WHERE id = 1 AND update_time = #{previousUpdateTime};

Java实现

@Transactional(rollbackFor = Exception.class)
public boolean deductStock(Long productId, Integer quantity) {// 最大重试次数int maxRetry = 3;for (int i = 0; i < maxRetry; i++) {// 1. 查询商品Product product = productMapper.selectById(productId);if (product.getStock() < quantity) {throw new BusinessException("库存不足");}// 2. 乐观锁更新int updated = productMapper.updateStockWithVersion(productId,quantity,product.getVersion());if (updated > 0) {return true;  // 成功}// 失败,重试if (i < maxRetry - 1) {try {Thread.sleep(50);  // 短暂等待} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}throw new BusinessException("系统繁忙,请稍后重试");
}

优点

  • ✅ 并发性能高(无锁)
  • ✅ 不会死锁
  • ✅ 适合读多写少的场景

缺点

  • ❌ 可能需要重试
  • ❌ 不保证实时一致性
  • ❌ 高并发下重试次数多

6.3 乐观锁 vs 悲观锁

对比项悲观锁乐观锁
加锁时机读取时加锁更新时检查
实现方式数据库锁(FOR UPDATE)版本号/时间戳
并发性能低(阻塞)高(无锁)
适用场景写多读少读多写少
一致性强一致性最终一致性
死锁可能不会
重试不需要可能需要

选择建议

悲观锁:
- 写操作频繁
- 对一致性要求极高
- 冲突率高乐观锁:
- 读操作频繁
- 冲突率低
- 允许重试

七、实战案例

7.1 案例1:电商秒杀

需求:100个商品,10000人秒杀。

方案对比

方案1:悲观锁(不推荐)

@Transactional
public boolean seckill(Long productId, Long userId) {// 锁定商品Product product = productMapper.selectByIdForUpdate(productId);if (product.getStock() <= 0) {return false;}// 扣减库存productMapper.updateStock(productId, 1);// 创建订单orderMapper.insert(new Order(productId, userId));return true;
}

问题:数据库压力大,并发低(10000个请求排队)

方案2:乐观锁(改进)

@Transactional
public boolean seckill(Long productId, Long userId) {Product product = productMapper.selectById(productId);if (product.getStock() <= 0) {return false;}// 乐观锁扣减int updated = productMapper.updateStockWithVersion(productId, 1, product.getVersion());if (updated == 0) {return false;  // 失败,不重试}// 创建订单orderMapper.insert(new Order(productId, userId));return true;
}

优势:并发性能提升,但仍有数据库压力

方案3:Redis预扣库存(推荐)

public boolean seckill(Long productId, Long userId) {String stockKey = "seckill:stock:" + productId;// 1. Redis原子扣减Long stock = redisTemplate.opsForValue().decrement(stockKey);if (stock < 0) {// 库存不足,回滚redisTemplate.opsForValue().increment(stockKey);return false;}// 2. 异步创建订单(MQ)rabbitTemplate.convertAndSend("seckill.order",new OrderMessage(productId, userId));return true;
}// 消费者
@RabbitListener(queues = "seckill.order")
public void processOrder(OrderMessage message) {// 创建订单orderService.createOrder(message.getProductId(), message.getUserId());
}

7.2 案例2:分布式锁

场景:多个服务实例,保证同一时间只有一个实例执行任务。

Redis分布式锁

public void executeTask() {String lockKey = "task:lock";String lockValue = UUID.randomUUID().toString();// 1. 获取锁(SETNX + 过期时间)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS);if (!locked) {return;  // 获取锁失败}try {// 2. 执行任务doTask();} finally {// 3. 释放锁(Lua脚本保证原子性)String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"    return redis.call('del', KEYS[1]) " +"else " +"    return 0 " +"end";redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),Collections.singletonList(lockKey),lockValue);}
}

Redisson分布式锁(推荐)

@Autowired
private RedissonClient redisson;public void executeTask() {RLock lock = redisson.getLock("task:lock");try {// 尝试获取锁,最多等待10秒,锁自动过期时间30秒boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);if (locked) {// 执行任务doTask();}} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {// 释放锁if (lock.isHeldByCurrentThread()) {lock.unlock();}}
}

八、锁优化建议

8.1 减少锁冲突

1. 缩小锁范围

// ❌ 锁范围过大
@Transactional
public void process() {// 大量业务逻辑doSomething();// 数据库操作update();// 更多业务逻辑doOtherThing();
}// ✅ 缩小锁范围
public void process() {doSomething();// 只在必要时加锁transactionTemplate.execute(status -> {update();return null;});doOtherThing();
}

2. 减少锁持有时间

-- ❌ 锁持有时间长
START TRANSACTION;
SELECT * FROM products WHERE id = 1 FOR UPDATE;
-- 执行大量业务逻辑...
-- 调用外部API...
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;-- ✅ 减少锁持有时间
-- 先完成业务逻辑
-- 再开启事务
START TRANSACTION;
SELECT * FROM products WHERE id = 1 FOR UPDATE;
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;

3. 按顺序访问资源

// 统一访问顺序,避免死锁
public void transfer(Long from, Long to, BigDecimal amount) {Long firstId = Math.min(from, to);Long secondId = Math.max(from, to);lockAndUpdate(firstId);lockAndUpdate(secondId);
}

8.2 选择合适的隔离级别

-- 如果不需要可重复读,降低隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;-- 减少间隙锁,提升并发性能

8.3 使用合适的索引

-- ❌ 没有索引:全表锁
UPDATE products SET stock = stock - 1 WHERE name = 'iPhone';-- ✅ 有索引:行锁
CREATE INDEX idx_name ON products(name);
UPDATE products SET stock = stock - 1 WHERE name = 'iPhone';

8.4 监控锁等待

-- 定期检查锁等待
SELECT * FROM sys.innodb_lock_waits;-- 配置告警
-- 锁等待超过5秒,发送告警

总结

核心要点

1. 锁的分类:- 粒度:全局锁、表锁、行锁- 模式:共享锁、排他锁- 类型:记录锁、间隙锁、临键锁2. InnoDB行锁:- 记录锁:锁定记录- 间隙锁:锁定间隙(防幻读)- 临键锁:记录锁+间隙锁3. 死锁:- 四个条件:互斥、持有等待、不可剥夺、循环等待- 避免方法:统一顺序、一次获取、超时、乐观锁4. 乐观锁 vs 悲观锁:- 悲观锁:写多读少,强一致性- 乐观锁:读多写少,高并发5. 优化建议:- 缩小锁范围- 减少锁持有时间- 使用合适的索引- 选择合适的隔离级别

http://www.dtcms.com/a/512811.html

相关文章:

  • 用户网站模板厦门唯一官方网站
  • 介绍化工项目建设和招聘的网站做网站app要注册哪类商标
  • milvus容器restart不成功,但docker仍在running问题排查
  • 女的和男的做那个视频网站广西网站建设公司电话
  • 织梦网站首页模板更换重要新闻头条
  • ABAP 静态代码分析 - 语法分析
  • 【VPX315】基于 3U VPX 总线架构的 JFMQL100TAI + FT-M6678 智能信号处理平台
  • 建个什么网站赚钱大连发布: 大连发布
  • 南京网站建设工作室企业网站推广是不是必要的
  • 网站制作html代码网站开发工作量
  • Sprintf Boot 之 Nacos 配置中心实践(spring.config.import=optional:nacos:)
  • 东莞行业网站建设中国电子商务中心官网
  • 山东住房和城乡建设厅网站登陆怎么打广告宣传自己的产品
  • 网站建设公司营业执照图片免费图片在线制作
  • 参加科学大会(dijkstra(堆优化版))
  • 百度上如何做优化网站wordpress 目录权限设置
  • 涪陵建设工程信息网站除了红动中国还有哪些设计网站
  • JVM虚拟机入门到实战(持续更新中)
  • 苏州网站建设与网络营销网络销售的好处和意义
  • 免费wordpress主题分享seo网站建站公司的主页
  • 外贸网站宗旨网站建设包括哪些方面
  • 什么外设选择开漏,什么外设选择推挽?
  • HTML 标签及推荐嵌套结构
  • 优先算法专题十二——栈
  • Flare 少样本学习嵌入式agent
  • Windows 系统下 n8n 自动化工具的完整部署指南
  • 138ip地址查询网站php wordpress joom
  • 贴片电阻封装尺寸与功率等级对照表及选型指南:从0201到2512的全面解析
  • 基于单片机的智能灯光控制系统设计与实现(论文+源码)
  • TypeScript 高级类型工具:Partial, Required, Record 的妙用与陷阱