MySQL 中的锁你真的了解吗?从行锁到死锁,一篇彻底讲透!
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
在 Spring Boot 项目中,你是否遇到过这些问题:
- 用户下单时,库存扣成负数?
- 两个线程同时修改同一条记录,结果数据错乱?
- 程序突然卡住,日志显示“等待锁”?
这些问题的背后,往往都是 MySQL 锁机制没搞清楚!
今天我们就用 Java + Spring Boot + MySQL(InnoDB),手把手带你搞懂:
- MySQL 到底有哪些锁?
- 什么情况下会加锁?
- 如何避免死锁?
- 如何用代码正确处理并发?
一、为什么需要锁?—— 并发场景下的数据一致性
📌 场景:电商扣库存
// 伪代码
public void reduceStock(Long productId) {
Product product = productMapper.selectById(productId);
if (product.getStock() > 0) {
product.setStock(product.getStock() - 1);
productMapper.updateById(product); // 危险!
}
}
问题:
如果两个用户同时下单,都读到 stock=1,然后各自减 1 → 最终 stock=-1!
✅ 解决方案:加锁,确保“读-判断-写”是原子操作。
而 MySQL 的锁,就是实现这种原子性的底层保障。
二、MySQL 锁的分类(InnoDB 引擎)
InnoDB 支持 行级锁,这是它比 MyISAM(只支持表锁)更适合高并发的关键!
🔒 1. 按粒度分
| 类型 | 说明 | 并发性 |
|---|---|---|
| 表锁(Table Lock) | 锁整张表(MyISAM 默认) | ❌ 差 |
| 行锁(Row Lock) | 只锁特定行(InnoDB 默认) | ✅ 高 |
| 间隙锁(Gap Lock) | 锁索引之间的“间隙” | 防幻读 |
| 临键锁(Next-Key Lock) | 行锁 + 间隙锁 | InnoDB 默认 |
💡 InnoDB 在 RC(Read Committed) 和 RR(Repeatable Read) 隔离级别下,锁行为不同!
🔒 2. 按类型分
| 锁类型 | 作用 | 典型场景 |
|---|---|---|
| 共享锁(S Lock) | 允许多个事务读同一行 | SELECT ... LOCK IN SHARE MODE(已废弃) |
| 排他锁(X Lock) | 禁止其他事务读写 | UPDATE / DELETE / SELECT ... FOR UPDATE |
| 意向锁(Intention Lock) | 表级锁,表示“想对某行加锁” | 自动加,无需关心 |
✅ 重点掌握:排他锁(X Lock)和间隙锁!
三、实战:Spring Boot 中如何触发 MySQL 行锁?
场景:安全扣库存
✅ 正确做法:使用 SELECT ... FOR UPDATE
@Service
@Transactional
public class OrderService {
@Autowired
private ProductMapper productMapper;
public boolean createOrder(Long userId, Long productId) {
// 1. 加排他锁查询库存
Product product = productMapper.selectForUpdate(productId);
if (product == null || product.getStock() <= 0) {
throw new RuntimeException("库存不足");
}
// 2. 扣库存(此时别人无法修改这行)
product.setStock(product.getStock() - 1);
productMapper.updateById(product);
// 3. 创建订单...
return true;
}
}
对应的 Mapper:
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
@Select("SELECT * FROM product WHERE id = #{id} FOR UPDATE")
Product selectForUpdate(@Param("id") Long id);
}
🔑 关键:
FOR UPDATE会在查询的行上加 排他锁(X Lock),直到事务提交才释放。
四、不同隔离级别下的锁行为(重点!)
MySQL 默认隔离级别是 RR(Repeatable Read),但很多公司用 RC(Read Committed)。
| 隔离级别 | SELECT ... FOR UPDATE 是否加间隙锁? |
幻读风险 |
|---|---|---|
| RC(读已提交) | ❌ 不加间隙锁,只锁具体行 | ✅ 有幻读 |
| RR(可重复读) | ✅ 加临键锁(行+间隙) | ❌ 无幻读 |
🌰 举例:防止“幻读”
表 user(id PK, name),当前有 id=1,3,5
执行:
SELECT * FROM user WHERE id > 2 AND id < 6 FOR UPDATE;
- 在 RR 下:不仅锁住 id=3,5,还锁住 (2,3)、(3,5)、(5,+∞) 的间隙,防止别人插入 id=4!
- 在 RC 下:只锁 id=3,5,别人仍可插入 id=4 → 出现幻读。
💡 如果你用的是 RC + 唯一索引等值查询,InnoDB 会退化为记录锁,不加间隙锁。
五、死锁(Deadlock)是怎么发生的?
📌 经典死锁场景
| 时间 | 事务 A | 事务 B |
|---|---|---|
| T1 | UPDATE product SET stock=stock-1 WHERE id=1; |
|
| T2 | UPDATE product SET stock=stock-1 WHERE id=2; |
|
| T3 | UPDATE product SET stock=stock-1 WHERE id=2;(等待 B 释放) |
|
| T4 | UPDATE product SET stock=stock-1 WHERE id=1;(等待 A 释放) |
💥 结果:互相等待,死锁!
MySQL 会自动检测死锁,并回滚其中一个事务(通常是 undo log 少的那个)。
🔍 如何查看死锁日志?
SHOW ENGINE INNODB STATUS;
在 LATEST DETECTED DEADLOCK 部分能看到详细信息。
六、如何避免死锁?(开发建议)
✅ 1. 按固定顺序访问资源
- 总是先锁 id 小的,再锁 id 大的
- 避免交叉加锁
✅ 2. 减少事务持有锁的时间
- 事务尽量短
- 不要在事务中做 RPC 调用、sleep 等耗时操作
✅ 3. 加锁前先排序
// 错误:随机顺序
List<Long> ids = Arrays.asList(3L, 1L, 2L);
// 正确:先排序
ids.sort(Long::compareTo);
for (Long id : ids) {
productMapper.selectForUpdate(id); // 按顺序加锁
}
✅ 4. 设置超时时间
# application.yml
spring:
datasource:
hikari:
connection-timeout: 3000
# 或在 SQL 中
或在 MySQL 层设置:
SET innodb_lock_wait_timeout = 10; -- 等待锁超时 10 秒
七、反例 vs 正例对比
❌ 反例:无锁扣库存(高并发必出错)
// 危险!没有加锁
Product p = productMapper.selectById(1001);
if (p.getStock() > 0) {
p.setStock(p.getStock() - 1);
productMapper.updateById(p);
}
✅ 正例:FOR UPDATE + 事务
@Transactional
public void safeReduceStock(Long productId) {
Product p = productMapper.selectForUpdate(productId);
if (p.getStock() <= 0) throw new RuntimeException("无库存");
p.setStock(p.getStock() - 1);
productMapper.updateById(p);
}
八、高级技巧:乐观锁 vs 悲观锁
| 方案 | 原理 | 适用场景 |
|---|---|---|
悲观锁(FOR UPDATE) |
先加锁,再操作 | 写多读少,冲突频繁 |
| 乐观锁(version 字段) | 更新时检查 version | 读多写少,冲突少 |
乐观锁示例:
ALTER TABLE product ADD COLUMN version INT DEFAULT 0;
// 更新时带 version 条件
int rows = productMapper.updateByVersion(
productId,
newStock,
oldVersion
);
if (rows == 0) {
throw new RuntimeException("并发修改,请重试");
}
💡 建议:
- 库存、余额等强一致性场景 → 用 悲观锁
- 商品信息、点赞数等 → 用 乐观锁
九、总结:MySQL 锁核心要点
| 问题 | 答案 |
|---|---|
| InnoDB 默认锁粒度? | 行锁 |
UPDATE 会加什么锁? |
排他锁(X Lock) |
| RR 隔离级别会加间隙锁吗? | 会!(防幻读) |
| 如何避免死锁? | 固定顺序 + 短事务 + 超时 |
| 高并发扣库存用什么锁? | SELECT ... FOR UPDATE(悲观锁) |
记住:锁是双刃剑——不用会数据错乱,滥用会性能下降甚至死锁!
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
更多推荐

所有评论(0)