排它锁与共享锁详解 - 详解
2026-01-17 20:58 tlnshuju 阅读(0) 评论(0) 收藏 举报排它锁与共享锁详解
1. 基本概念
共享锁(Shared Lock,S锁)
-- 显式加共享锁
SELECT * FROM table WHERE id = 1 LOCK IN SHARE MODE;
排它锁(Exclusive Lock,X锁)
-- 显式加排它锁
SELECT * FROM table WHERE id = 1 FOR UPDATE;
2. 锁的兼容性矩阵
| 当前锁状态 | 共享锁(S) | 排它锁(X) | 无锁 |
|---|---|---|---|
| 共享锁(S) | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 |
| 排它锁(X) | ❌ 冲突 | ❌ 冲突 | ✅ 兼容 |
| 无锁 | ✅ 兼容 | ✅ 兼容 | ✅ 兼容 |
3. 锁的作用与使用场景
共享锁(S锁)使用场景
-- 场景1:读取数据并确保不被修改
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 1001 LOCK IN SHARE MODE;
-- 其他事务可以加共享锁读取,但不能修改
-- 确保在事务期间余额数据不被更改
COMMIT;
-- 场景2:检查引用完整性
START TRANSACTION;
-- 检查是否有订单引用此产品
SELECT COUNT(*) FROM orders WHERE product_id = 500 LOCK IN SHARE MODE;
-- 如果返回0,可以安全删除产品
DELETE FROM products WHERE id = 500;
COMMIT;
排它锁(X锁)使用场景
-- 场景1:更新操作(自动加排它锁)
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1001;
-- 其他事务不能读取(取决于隔离级别)或修改此行
COMMIT;
-- 场景2:悲观锁控制并发
START TRANSACTION;
SELECT * FROM inventory WHERE product_id = 2001 FOR UPDATE;
-- 检查库存并更新
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 2001;
COMMIT;
-- 场景3:防止幻读
START TRANSACTION;
SELECT * FROM orders WHERE user_id = 1001 AND status = 'pending' FOR UPDATE;
-- 在REPEATABLE READ隔离级别下,防止其他事务插入新的pending订单
INSERT INTO orders (...) VALUES (...);
COMMIT;
4. 锁的实现模型
InnoDB锁的内存结构
// 简化的锁结构表示
struct lock_t {
trx_t* trx; // 持有锁的事务
lock_rec_t* rec_lock; // 记录锁
lock_table_t* table_lock;// 表锁
uint32_t type_mode; // 锁类型和模式
// ... 其他字段
};
struct lock_rec_t {
space_id_t space; // 表空间ID
page_no_t page_no; // 页号
uint32_t n_bits; // 锁位图大小
byte* bits; // 锁位图,记录哪些记录被锁住
};
锁管理的核心流程
5. 具体实现机制
记录锁(Record Lock)
-- 对单条记录加锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 在id=1的记录上加排它锁
间隙锁(Gap Lock)
-- 防止幻读,锁定一个范围
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;
-- 锁定age在20-30之间的间隙,防止其他事务插入
临键锁(Next-Key Lock)
-- 记录锁 + 间隙锁的组合
SELECT * FROM users WHERE id > 100 FOR UPDATE;
-- 锁定id>100的所有现有记录和间隙
6. 实际案例分析
银行转账场景
-- 事务1:转账操作
START TRANSACTION;
-- 对转出账户加排它锁
SELECT * FROM accounts WHERE account_no = 'A001' FOR UPDATE;
-- 对转入账户加排它锁
SELECT * FROM accounts WHERE account_no = 'B002' FOR UPDATE;
-- 执行转账
UPDATE accounts SET balance = balance - 100 WHERE account_no = 'A001';
UPDATE accounts SET balance = balance + 100 WHERE account_no = 'B002';
COMMIT;
库存扣减场景
-- 高并发库存管理
START TRANSACTION;
-- 使用排它锁确保库存准确
SELECT quantity FROM inventory WHERE product_id = 1001 FOR UPDATE;
-- 检查库存
IF quantity >= order_quantity THEN
UPDATE inventory SET quantity = quantity - order_quantity
WHERE product_id = 1001;
COMMIT;
ELSE
ROLLBACK;
-- 库存不足处理
END IF;
7. 锁的监控与诊断
查看当前锁信息
-- 查看InnoDB锁状态
SHOW ENGINE INNODB STATUS\G
-- 查看锁信息部分
-- 查看当前锁等待
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 查看进程和锁信息
SHOW PROCESSLIST;
锁超时配置
-- 设置锁等待超时时间(秒)
SET SESSION innodb_lock_wait_timeout = 50;
SET GLOBAL innodb_lock_wait_timeout = 50;
-- 查看当前配置
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
8. 死锁处理
死锁产生场景
-- 事务1
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 锁住id=1
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 等待id=2的锁
-- 事务2(同时执行)
START TRANSACTION;
UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- 锁住id=2
UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- 等待id=1的锁
-- 死锁发生!
死锁检测与处理
-- InnoDB自动检测死锁并回滚代价较小的事务
-- 查看最近死锁信息
SHOW ENGINE INNODB STATUS\G
-- 在LATEST DETECTED DEADLOCK部分查看详情
-- 错误处理代码示例(应用程序层面)
try:
cursor.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
cursor.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
connection.commit()
except mysql.connector.errors.DatabaseError as e:
if 'Deadlock' in str(e):
# 死锁发生,重试逻辑
time.sleep(0.1)
retry_transaction()
else:
raise
9. 最佳实践
锁优化建议
-- 1. 尽量使用索引,减少锁范围
-- 慢:全表扫描,锁住所有记录
SELECT * FROM users WHERE name LIKE '%john%' FOR UPDATE;
-- 快:使用索引,只锁相关记录
SELECT * FROM users WHERE id = 1001 FOR UPDATE;
-- 2. 保持事务简短
START TRANSACTION;
-- 尽快完成数据操作
UPDATE ...;
DELETE ...;
COMMIT; -- 立即提交释放锁
-- 3. 按固定顺序访问资源,避免死锁
-- 所有事务都按id升序访问账户
UPDATE accounts SET ... WHERE id = 1;
UPDATE accounts SET ... WHERE id = 2;
-- 4. 使用较低的隔离级别(如READ COMMITTED)减少锁竞争
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
锁与性能平衡
-- 读多写少场景:考虑使用乐观锁
SELECT version, data FROM table WHERE id = 1;
-- 应用程序处理业务逻辑
UPDATE table SET data = new_data, version = version + 1
WHERE id = 1 AND version = old_version;
-- 如果影响行数为0,说明版本已变化,需要重试
-- 写多场景:使用悲观锁但控制粒度
-- 使用行级锁而不是表锁
SELECT * FROM table WHERE id = 1 FOR UPDATE;
-- 而不是:LOCK TABLES table WRITE;
10. 总结
共享锁:
- 用于读取操作,允许多个事务同时读取
- 阻止其他事务获取排它锁
- 适合读多写少的并发读取场景
排它锁:
- 用于写入操作,确保数据一致性
- 阻止其他事务获取共享锁或排它锁
- 适合数据更新、删除等修改操作
关键要点:
- 理解锁兼容性是设计并发系统的关键
- 合理选择锁粒度(行锁 vs 表锁)
- 监控和优化锁等待时间
- 设计事务以避免死锁
- 根据业务场景选择合适的锁策略
正确使用锁机制可以确保数据一致性,同时最大化系统并发性能。