数据库锁机制与并发控制
字数 1876 2025-11-02 09:29:26
数据库锁机制与并发控制
描述
数据库锁机制是并发控制的核心技术,用于协调多个事务同时访问共享数据时的冲突问题。当多个事务并发执行时,可能会引发脏读、不可重复读、幻读等问题。锁机制通过为数据资源加锁,限制其他事务的访问方式,从而确保数据的一致性和隔离性。理解锁的类型、兼容性以及死锁是掌握该知识点的关键。
解题过程
我们将从并发问题入手,逐步讲解锁如何解决这些问题。
第一步:理解并发操作可能引发的问题
在多个事务未经控制地并发执行时,会出现以下典型问题:
- 脏读:事务A读取了事务B已修改但未提交的数据。如果事务B后来回滚,那么事务A读到的就是无效的“脏”数据。
- 不可重复读:事务A内多次读取同一数据。在两次读取的间隔,事务B修改并提交了该数据,导致事务A两次读取的结果不一致。
- 幻读:事务A根据条件查询一批数据。在查询过程中,事务B插入或删除了符合该条件的记录并提交,导致事务A再次查询时,结果集的行数发生了变化,如同出现了“幻觉”。
第二步:认识锁的基本类型——共享锁与排他锁
锁是解决上述问题的基本工具。最核心的两种锁是:
- 共享锁(S锁,读锁):
- 作用:用于读取操作。一个事务对数据对象加了S锁后,其他事务可以继续加S锁进行读取,但不能加X锁进行修改。
- 类比:就像多人同时打开同一份文档阅读,大家都能看,但不能有任何人去修改它。
- 排他锁(X锁,写锁):
- 作用:用于写入操作(增、删、改)。一个事务对数据对象加了X锁后,其他事务既不能对其加S锁(读取),也不能加X锁(修改)。
- 类比:就像一个人锁上门独自在房间里修改文档,在此期间,其他人既不能进来看,也不能进来改。
锁的兼容性矩阵如下:
| 当前已有的锁 | 请求共享锁(S) | 请求排他锁(X) |
|---|---|---|
| 无锁 | 允许 | 允许 |
| 共享锁(S) | 允许 | 拒绝 |
| 排他锁(X) | 拒绝 | 拒绝 |
第三步:锁是如何解决并发问题的
现在,我们看锁如何应用于事务来解决第一步中的问题。
- 解决脏读:事务B在修改数据前必须先获得X锁。在事务B提交之前,这个X锁一直持有。此时事务A尝试读取该数据(请求S锁),但由于X锁与S锁不兼容,请求会被阻塞,直到事务B提交或回滚后释放X锁。这样,事务A就读不到未提交的数据。
- 解决不可重复读:事务A在第一次读取数据后,并不立即释放S锁,而是持续持有直到事务结束。这样,在事务A执行期间,事务B尝试修改该数据(请求X锁)会被阻塞,因为S锁和X锁不兼容。这就保证了事务A在期间多次读取该数据时,结果是一致的。
- 解决幻读:解决幻读需要一种更“宽”的锁,称为范围锁。事务A在查询时(例如
WHERE age > 20),不仅会为表中已有的、满足age > 20的记录加锁,还会为整个age > 20这个“范围”加锁。这个范围锁会阻止其他事务在这个范围内插入新的记录(例如INSERT ... age=25),从而防止了幻读的发生。
第四步:理解锁的粒度
锁可以施加在不同大小的数据单元上,这就是锁的粒度。
- 行级锁:锁住一行记录。粒度最小,并发度最高,但管理开销最大。
- 页级锁:锁住一页数据(数据库存储的基本单位,通常包含多行)。粒度与开销介于行锁和表锁之间。
- 表级锁:锁住整张表。粒度最大,实现简单,开销小,但并发度非常低。
数据库系统会根据操作需要自动选择最合适的锁粒度。
第五步:认识死锁及其解决策略
当多个事务互相等待对方释放锁时,就会进入死锁状态。
-
示例:
- 事务A锁住了行1,请求锁行2。
- 事务B锁住了行2,请求锁行1。
- 此时,事务A等待事务B释放行2,事务B等待事务A释放行1。双方无限期等待,形成死锁。
-
解决策略:
- 预防:规定所有事务必须按统一的顺序申请锁,破坏“循环等待”条件。
- 检测与恢复(数据库常用):系统维护一个“等待图”来检测是否存在循环等待(死锁)。一旦检测到死锁,会选择一个“牺牲者”事务(通常选择撤销代价最小的那个),将其回滚并释放其持有的所有锁,从而让其他事务得以继续执行。
总结
数据库锁机制是一个通过共享锁(S)和排他锁(X)来控制并发访问的系统。它通过锁的兼容性规则和不同的加锁策略,在不同隔离级别下解决了脏读、不可重复读和幻读等问题。同时,需要注意锁的粒度权衡和死锁的应对策略。理解这套机制,是设计和优化高并发数据库应用的基础。