问题
什么是排他锁和共享锁?
答案
核心概念
共享锁(Shared Lock, S锁):允许事务读取一行数据,多个事务可以同时持有同一资源的共享锁。 排他锁(Exclusive Lock, X锁):允许事务更新或删除一行数据,一旦事务持有排他锁,其他事务无法获取该资源的任何锁。
锁的兼容性矩阵
| 共享锁(S) | 排他锁(X) | |
|---|---|---|
| 共享锁(S) | ✓ 兼容 | ✗ 冲突 |
| 排他锁(X) | ✗ 冲突 | ✗ 冲突 |
含义:
- S锁与S锁兼容:多个事务可以同时读取同一数据
- S锁与X锁冲突:读锁阻止写,写锁阻止读
- X锁与X锁冲突:写操作之间互斥
SQL语法与加锁方式
1. 共享锁(S锁)
-- 显式加共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- MySQL 8.0新语法
SELECT * FROM users WHERE id = 1 FOR SHARE;
-- 普通SELECT不加锁(MVCC实现快照读)
SELECT * FROM users WHERE id = 1; -- 不加锁
应用场景:
- 确保读取期间数据不被修改
- 多个事务需要同时读取相同数据
- 检查外键约束时自动加S锁
2. 排他锁(X锁)
-- 显式加排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- DML语句自动加排他锁
UPDATE users SET name = 'Alice' WHERE id = 1; -- 自动加X锁
DELETE FROM users WHERE id = 1; -- 自动加X锁
INSERT INTO users VALUES (1, 'Alice'); -- 自动加X锁
应用场景:
- 数据修改操作(INSERT/UPDATE/DELETE)
- 读取后立即修改(避免并发修改冲突)
- 分布式锁实现(SELECT FOR UPDATE)
实际并发场景示例
场景1:共享锁的并发读
-- 事务A:加共享锁读取
BEGIN;
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE; -- balance = 1000
-- 持有S锁...
-- 事务B:可以同时读取
BEGIN;
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE; -- balance = 1000
-- 成功!两个事务同时持有S锁
-- 事务C:无法修改,等待
UPDATE account SET balance = 500 WHERE id = 1; -- 等待S锁释放
场景2:排他锁的互斥
-- 事务A:加排他锁
BEGIN;
SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 持有X锁
-- 事务B:无法读(LOCK IN SHARE MODE)
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE; -- 等待
-- 事务C:无法写
UPDATE account SET balance = 500 WHERE id = 1; -- 等待
-- 事务D:可以快照读(不加锁)
SELECT * FROM account WHERE id = 1; -- 成功(MVCC读取旧版本)
场景3:共享锁升级为排他锁(死锁风险)
-- 事务A
BEGIN;
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE; -- 持有S锁
-- 尝试升级为X锁
UPDATE account SET balance = 500 WHERE id = 1; -- 等待事务B释放S锁
-- 事务B(同时执行)
BEGIN;
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE; -- 持有S锁
-- 尝试升级为X锁
UPDATE account SET balance = 800 WHERE id = 1; -- 等待事务A释放S锁
-- 结果:死锁!InnoDB自动检测并回滚一个事务
实现原理(简化)
/**
* 锁管理器伪代码
*/
class LockManager {
Map<RowId, Set<Transaction>> sharedLocks = new HashMap<>();
Map<RowId, Transaction> exclusiveLocks = new HashMap<>();
// 申请共享锁
public boolean acquireSharedLock(RowId row, Transaction txn) {
// 检查是否有排他锁
if (exclusiveLocks.containsKey(row)) {
return false; // 等待排他锁释放
}
// 加入共享锁集合
sharedLocks.computeIfAbsent(row, k -> new HashSet<>()).add(txn);
return true;
}
// 申请排他锁
public boolean acquireExclusiveLock(RowId row, Transaction txn) {
// 检查是否有其他事务持有锁
if (exclusiveLocks.containsKey(row) ||
(sharedLocks.containsKey(row) && sharedLocks.get(row).size() > 0)) {
return false; // 等待所有锁释放
}
exclusiveLocks.put(row, txn);
return true;
}
}
分布式锁实现
-- 使用SELECT FOR UPDATE实现悲观锁
BEGIN;
-- 尝试获取分布式锁
SELECT * FROM distributed_lock WHERE resource_name = 'order_123' FOR UPDATE;
-- 执行业务逻辑
UPDATE orders SET status = 'processing' WHERE id = 123;
COMMIT; -- 释放锁
性能与优化
1. 优先使用MVCC快照读
-- 不推荐:加共享锁降低并发
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 推荐:利用MVCC,无锁读取
SELECT * FROM users WHERE id = 1; -- 读取一致性快照
2. 避免长事务持有锁
-- 错误:长事务持有锁时间过长
BEGIN;
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- 执行复杂业务逻辑(耗时10秒)...
COMMIT;
-- 正确:缩短锁持有时间
BEGIN;
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- 快速完成数据修改
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;
3. 减少锁冲突
-- 高并发场景:拆分热点行
-- 原表设计:单行记录总库存
UPDATE inventory SET total = total - 1 WHERE product_id = 100; -- 高并发冲突
-- 优化:分段库存
UPDATE inventory_shards SET amount = amount - 1
WHERE product_id = 100 AND shard_id = (user_id % 10); -- 减少冲突
不同隔离级别下的行为
| 隔离级别 | SELECT | LOCK IN SHARE MODE | FOR UPDATE |
|---|---|---|---|
| READ UNCOMMITTED | 不加锁 | S锁 | X锁 |
| READ COMMITTED | 不加锁 | S锁(读完即释放) | X锁 |
| REPEATABLE READ | 不加锁 | S锁(事务结束释放) | X锁+Gap锁 |
| SERIALIZABLE | 自动加S锁 | S锁 | X锁 |
答题总结
共享锁和排他锁是实现数据库并发控制的基础机制:
- S锁允许并发读,适用于多个事务同时查询同一数据
- X锁保证独占写,确保数据修改的原子性和一致性
- 关键规则:读读不互斥,读写/写写互斥
面试要点:
- 理解锁的兼容性矩阵
- 区分显式加锁(LOCK IN SHARE MODE/FOR UPDATE)和自动加锁(DML)
- 掌握MVCC快照读不加锁的特性
- 警惕S锁升级X锁可能导致的死锁问题
在实际开发中,应尽量使用MVCC快照读代替共享锁,用乐观锁代替排他锁,以提高系统并发性能。