适用于 MySQL InnoDB、PostgreSQL 等支持行级锁的 MVCC 引擎
重点以 MySQL InnoDB 为例说明
一、基本概念
| 锁类型 | 全称 | 别名 | 作用 |
|---|---|---|---|
| S 锁 | Shared Lock | 共享锁、读锁 | 允许多个事务同时读同一数据 |
| X 锁 | Exclusive Lock | 排他锁、写锁 | 确保只有一个事务能修改数据 |
二、加锁方式(MySQL InnoDB)
| 操作 | 加的锁 | 说明 |
|---|---|---|
SELECT ... LOCK IN SHARE MODE |
S 锁 | 显式加共享锁(MySQL 特有语法) |
SELECT ... FOR UPDATE |
X 锁 | 显式加排他锁(通用) |
UPDATE / DELETE |
X 锁 | 自动对匹配行加 X 锁 |
普通 SELECT |
无锁 | 使用 MVCC 快照读,不加任何锁 |
💡 PostgreSQL 中对应语法为
SELECT ... FOR SHARE(S 锁)和SELECT ... FOR UPDATE(X 锁)
三、锁兼容性矩阵(关键!)
| 当前持有 → 请求 ← | S 锁 | X 锁 |
|---|---|---|
| S 锁 | ✅ 兼容 | ❌ 冲突 |
| X 锁 | ❌ 冲突 | ❌ 冲突 |
✅ 含义:
- 多个事务可同时持有 S 锁(并发读)
- X 锁是独占的:不能与其他任何锁共存
- S 与 X 互斥:有 S 锁时不能加 X 锁,反之亦然
四、详细行为对比 + 示例
场景设定
1 -- 表结构
2 CREATE TABLE products (
3 id INT PRIMARY KEY,
4 stock INT
5 );
6 INSERT INTO products VALUES (1001, 10);
1. X 锁(排他锁)—— FOR UPDATE
🔒 加锁语句
SELECT * FROM products WHERE id = 1001 FOR UPDATE;
🧪 并发行为测试
| 会话A(持 X 锁) | 会话B 尝试操作 | 结果 |
|---|---|---|
START TRANSACTION; SELECT ... FOR UPDATE; |
SELECT stock FROM products WHERE id = 1001; |
✅ 立即返回(普通读,MVCC) |
SELECT ... FOR UPDATE; |
⏳ 阻塞 | |
SELECT ... LOCK IN SHARE MODE; |
⏳ 阻塞 | |
UPDATE products SET stock = 5 WHERE id = 1001; |
⏳ 阻塞 | |
UPDATE products SET stock = 5 WHERE id = 1002; |
✅ 立即成功(不同行) |
✅ 适用场景
- 电商扣库存
- 银行转账前锁定账户
- “读完就改”的悲观锁流程
💡 示例(Spring Boot)
1 @Transactional
2 public boolean deductStock(Long id) {
3 // 加 X 锁
4 Integer stock = jdbcTemplate.queryForObject(
5 "SELECT stock FROM products WHERE id = ? FOR UPDATE",
6 Integer.class, id
7 );
8
9 if (stock > 0) {
10 jdbcTemplate.update("UPDATE products SET stock = stock - 1 WHERE id = ?", id);
11 return true;
12 }
13 return false;
14}
2. S 锁(共享锁)—— LOCK IN SHARE MODE
🔒 加锁语句
SELECT * FROM products WHERE id = 1001 LOCK IN SHARE MODE;
🧪 并发行为测试
| 会话 A(持 S 锁) | 会话 B 尝试操作 | 结果 |
|---|---|---|
START TRANSACTION; SELECT ... LOCK IN SHARE MODE; |
SELECT stock FROM products WHERE id = 1001; |
✅ 立即返回(普通读) |
SELECT ... LOCK IN SHARE MODE; |
✅ 立即返回(S 与 S 兼容) | |
SELECT ... FOR UPDATE; |
⏳ 阻塞 | |
UPDATE products SET stock = 5 WHERE id = 1001; |
⏳ 阻塞 | |
UPDATE products SET stock = 5 WHERE id = 1002; |
✅ 立即成功(不同行) |
如果有一个加了
@Transactional并对某行加了FOR UPDATE锁的事务(方法 A)正在执行,
另一个没有事务(或独立事务)的方法 B 直接执行UPDATE操作同一行,
那么方法 B 会不会被阻塞?要不要等方法 A 执行完?
无论方法 B 是否加了 @Transactional,只要它要修改被 X 锁保护的行,就会被阻塞。
✅ 适用场景
- 生成一致性报表(防止底层数据被修改)
- 引用完整性检查(如“删除用户前确认无订单”)
- 多步骤只读事务需防中间修改
💡 示例:安全删除用户
1 -- 检查是否有未完成订单(加 S 锁防新增/修改)
2 SELECT 1 FROM orders WHERE user_id = 123 LOCK IN SHARE MODE;
3
4 -- 若无结果,再删除用户(此时订单表仍被 S 锁保护)
5 DELETE FROM users WHERE id = 123;
五、关键注意事项
1. 必须在事务中使用
🔒 锁的生命周期 = 事务的生命周期
→ 只有在整个事务提交(或回滚)后,锁才会释放。
1 // ❌ 错误:无 @Transactional → 锁立即释放
2 public void bad() {
3 repo.findWithLock(id); // FOR UPDATE 执行完就 COMMIT,锁失效!
// 此时锁已释放!其他请求可以同时进入
if (stock > 0) {
// 2. 更新(无锁!)
update("UPDATE products SET stock = stock - 1 WHERE id = ?", id);
}
4 }
5
6 // ✅ 正确
7 @Transactional
8 public void good() {
9 repo.findWithLock(id); // 锁持续到方法结束(事务提交)
10 }
2. 锁粒度依赖索引
- ✅
WHERE id = ?(主键)→ 只锁一行 - ❌
WHERE name = ?(无索引)→ 可能锁全表(因全表扫描)
🔍 用
EXPLAIN验证是否走索引!
3. 普通 SELECT 永远不阻塞
- 无论 S 锁还是 X 锁,普通
SELECT都通过 MVCC 读历史版本,不加锁、不等待
4. 死锁风险
- 多事务交叉加锁顺序不一致 → 可能死锁
- 数据库会自动回滚一个事务(如 MySQL 报
Deadlock found) - 应用需捕获异常并重试
六、对比总结表
| 特性 | S 锁(共享锁) | X 锁(排他锁) |
|---|---|---|
| 加锁语法 | SELECT ... LOCK IN SHARE MODE |
SELECT ... FOR UPDATE |
| 是否允许多个并发 | ✅ 是 | ❌ 否 |
| 阻止其他事务读? | ❌(普通读不受影响) | ❌(普通读不受影响) |
| 阻止其他事务写? | ✅ 是 | ✅ 是 |
| 阻止其他事务加 S 锁? | ❌ 否 | ✅ 是 |
| 典型用途 | 读时防改(报表、校验) | 读后续改(扣库存、转账) |
| 死锁风险 | 较低 | 较高 |
七、最佳实践建议
- 优先使用 X 锁(
FOR UPDATE):逻辑更简单,避免 S-X 转换复杂性 - 确保 WHERE 条件有索引 → 避免意外锁表
- 事务尽量短 → 减少锁持有时间
- 普通查询不要加锁 → 用 MVCC 快照读即可
- 处理死锁异常 → 实现重试机制
✅ 记住一句话:
X 锁 = “我读完要改,谁都别动”。
普通 SELECT 永远自由通行,靠 MVCC 保证一致性。
给个饭钱?
- Post link: http://sovzn.github.io/2026/01/20/%E6%95%B0%E6%8D%AE%E5%BA%93%E8%A1%8C%E9%94%81/
- Copyright Notice: All articles in this blog are licensed under unless otherwise stated.



若没有本文 Issue,您可以使用 Comment 模版新建。
GitHub Issues