排它锁与共享锁详解
1. 基本概念
共享锁(Shared Lock,S锁)
-- 显式加共享锁SELECT*FROMtableWHEREid=1LOCKINSHAREMODE;排它锁(Exclusive Lock,X锁)
-- 显式加排它锁SELECT*FROMtableWHEREid=1FORUPDATE;2. 锁的兼容性矩阵
| 当前锁状态 | 共享锁(S) | 排它锁(X) | 无锁 |
|---|---|---|---|
| 共享锁(S) | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 |
| 排它锁(X) | ❌ 冲突 | ❌ 冲突 | ✅ 兼容 |
| 无锁 | ✅ 兼容 | ✅ 兼容 | ✅ 兼容 |
3. 锁的作用与使用场景
共享锁(S锁)使用场景
-- 场景1:读取数据并确保不被修改STARTTRANSACTION;SELECTbalanceFROMaccountsWHEREuser_id=1001LOCKINSHAREMODE;-- 其他事务可以加共享锁读取,但不能修改-- 确保在事务期间余额数据不被更改COMMIT;-- 场景2:检查引用完整性STARTTRANSACTION;-- 检查是否有订单引用此产品SELECTCOUNT(*)FROMordersWHEREproduct_id=500LOCKINSHAREMODE;-- 如果返回0,可以安全删除产品DELETEFROMproductsWHEREid=500;COMMIT;排它锁(X锁)使用场景
-- 场景1:更新操作(自动加排它锁)STARTTRANSACTION;UPDATEaccountsSETbalance=balance-100WHEREuser_id=1001;-- 其他事务不能读取(取决于隔离级别)或修改此行COMMIT;-- 场景2:悲观锁控制并发STARTTRANSACTION;SELECT*FROMinventoryWHEREproduct_id=2001FORUPDATE;-- 检查库存并更新UPDATEinventorySETquantity=quantity-1WHEREproduct_id=2001;COMMIT;-- 场景3:防止幻读STARTTRANSACTION;SELECT*FROMordersWHEREuser_id=1001ANDstatus='pending'FORUPDATE;-- 在REPEATABLE READ隔离级别下,防止其他事务插入新的pending订单INSERTINTOorders(...)VALUES(...);COMMIT;4. 锁的实现模型
InnoDB锁的内存结构
// 简化的锁结构表示structlock_t{trx_t*trx;// 持有锁的事务lock_rec_t*rec_lock;// 记录锁lock_table_t*table_lock;// 表锁uint32_ttype_mode;// 锁类型和模式// ... 其他字段};structlock_rec_t{space_id_tspace;// 表空间IDpage_no_tpage_no;// 页号uint32_tn_bits;// 锁位图大小byte*bits;// 锁位图,记录哪些记录被锁住};锁管理的核心流程
5. 具体实现机制
记录锁(Record Lock)
-- 对单条记录加锁SELECT*FROMusersWHEREid=1FORUPDATE;-- 在id=1的记录上加排它锁间隙锁(Gap Lock)
-- 防止幻读,锁定一个范围SELECT*FROMusersWHEREageBETWEEN20AND30FORUPDATE;-- 锁定age在20-30之间的间隙,防止其他事务插入临键锁(Next-Key Lock)
-- 记录锁 + 间隙锁的组合SELECT*FROMusersWHEREid>100FORUPDATE;-- 锁定id>100的所有现有记录和间隙6. 实际案例分析
银行转账场景
-- 事务1:转账操作STARTTRANSACTION;-- 对转出账户加排它锁SELECT*FROMaccountsWHEREaccount_no='A001'FORUPDATE;-- 对转入账户加排它锁SELECT*FROMaccountsWHEREaccount_no='B002'FORUPDATE;-- 执行转账UPDATEaccountsSETbalance=balance-100WHEREaccount_no='A001';UPDATEaccountsSETbalance=balance+100WHEREaccount_no='B002';COMMIT;库存扣减场景
-- 高并发库存管理STARTTRANSACTION;-- 使用排它锁确保库存准确SELECTquantityFROMinventoryWHEREproduct_id=1001FORUPDATE;-- 检查库存IFquantity>=order_quantityTHENUPDATEinventorySETquantity=quantity-order_quantityWHEREproduct_id=1001;COMMIT;ELSEROLLBACK;-- 库存不足处理ENDIF;7. 锁的监控与诊断
查看当前锁信息
-- 查看InnoDB锁状态SHOWENGINEINNODBSTATUS\G-- 查看锁信息部分-- 查看当前锁等待SELECT*FROMinformation_schema.INNODB_LOCKS;SELECT*FROMinformation_schema.INNODB_LOCK_WAITS;-- 查看进程和锁信息SHOWPROCESSLIST;锁超时配置
-- 设置锁等待超时时间(秒)SETSESSIONinnodb_lock_wait_timeout=50;SETGLOBALinnodb_lock_wait_timeout=50;-- 查看当前配置SHOWVARIABLESLIKE'innodb_lock_wait_timeout';8. 死锁处理
死锁产生场景
-- 事务1STARTTRANSACTION;UPDATEaccountsSETbalance=balance-100WHEREid=1;-- 锁住id=1UPDATEaccountsSETbalance=balance+100WHEREid=2;-- 等待id=2的锁-- 事务2(同时执行)STARTTRANSACTION;UPDATEaccountsSETbalance=balance-50WHEREid=2;-- 锁住id=2UPDATEaccountsSETbalance=balance+50WHEREid=1;-- 等待id=1的锁-- 死锁发生!死锁检测与处理
-- InnoDB自动检测死锁并回滚代价较小的事务-- 查看最近死锁信息SHOWENGINEINNODBSTATUS\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()exceptmysql.connector.errors.DatabaseErrorase:if'Deadlock'instr(e):# 死锁发生,重试逻辑time.sleep(0.1)retry_transaction()else: raise9. 最佳实践
锁优化建议
-- 1. 尽量使用索引,减少锁范围-- 慢:全表扫描,锁住所有记录SELECT*FROMusersWHEREnameLIKE'%john%'FORUPDATE;-- 快:使用索引,只锁相关记录SELECT*FROMusersWHEREid=1001FORUPDATE;-- 2. 保持事务简短STARTTRANSACTION;-- 尽快完成数据操作UPDATE...;DELETE...;COMMIT;-- 立即提交释放锁-- 3. 按固定顺序访问资源,避免死锁-- 所有事务都按id升序访问账户UPDATEaccountsSET...WHEREid=1;UPDATEaccountsSET...WHEREid=2;-- 4. 使用较低的隔离级别(如READ COMMITTED)减少锁竞争SETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;锁与性能平衡
-- 读多写少场景:考虑使用乐观锁SELECTversion,dataFROMtableWHEREid=1;-- 应用程序处理业务逻辑UPDATEtableSETdata=new_data,version=version+1WHEREid=1ANDversion=old_version;-- 如果影响行数为0,说明版本已变化,需要重试-- 写多场景:使用悲观锁但控制粒度-- 使用行级锁而不是表锁SELECT*FROMtableWHEREid=1FORUPDATE;-- 而不是:LOCK TABLES table WRITE;10. 总结
共享锁:
- 用于读取操作,允许多个事务同时读取
- 阻止其他事务获取排它锁
- 适合读多写少的并发读取场景
排它锁:
- 用于写入操作,确保数据一致性
- 阻止其他事务获取共享锁或排它锁
- 适合数据更新、删除等修改操作
关键要点:
- 理解锁兼容性是设计并发系统的关键
- 合理选择锁粒度(行锁 vs 表锁)
- 监控和优化锁等待时间
- 设计事务以避免死锁
- 根据业务场景选择合适的锁策略
正确使用锁机制可以确保数据一致性,同时最大化系统并发性能。