数据库锁机制与并发控制
描述:数据库锁机制是保证数据一致性和事务隔离性的核心技术。当多个事务同时访问数据库时,通过锁可以防止并发操作导致的数据不一致问题,如丢失更新、脏读、不可重复读等。理解锁的类型、粒度以及死锁的产生与解决,是数据库领域的核心知识点。
知识讲解:
第一步:为什么需要锁?—— 并发问题
想象一下,你和朋友同时编辑一个在线共享文档的同一行文字。如果没有控制机制,一个人刚输入的内容可能瞬间被另一个人的操作覆盖。数据库中也存在类似问题,主要体现在以下三个方面:
- 脏读:事务A读取了事务B尚未提交的修改。如果事务B后来回滚了,那么事务A读到的就是无效的“脏”数据。
- 不可重复读:事务A内多次读取同一数据。在两次读取之间,事务B修改并提交了该数据,导致事务A两次读取的结果不一致。
- 幻读:事务A根据条件查询出一批数据。此时事务B插入或删除了一些符合该条件的记录并提交。当事务A再次以相同条件查询时,发现多出了一些“幽灵”行或少了些行。
锁就是为了解决这些问题而存在的。
第二步:锁的基本类型
锁可以按照不同的维度分类,最基础的是按“权限”划分:
-
共享锁(S锁,读锁)
- 行为:当一个事务对数据加共享锁后,其他事务可以继续加共享锁来读取该数据,但不能加排他锁来修改它。
- 类比:就像很多人可以同时打开同一个PDF文件阅读,但只要有人在读,谁也不能去修改这个文件的内容。
- 目的:保证在读取过程中,数据不会被其他事务修改,从而解决“脏读”问题。
-
排他锁(X锁,写锁)
- 行为:当一个事务对数据加排他锁后,其他事务既不能再加共享锁读取,也不能加排他锁修改。
- 类比:就像你独占了文档的编辑权限,在你编辑保存之前,其他人既不能看也不能改。
- 目的:保证在修改数据时,不会有其他事务来读取或修改同一数据,从而保证修改的原子性和一致性。
兼容性规则:可以简单记为“读读兼容,读写/写写互斥”。
第三步:锁的粒度
锁可以应用在不同大小的数据单元上,这就是锁的粒度。粒度越小,并发性越好,但管理锁的开销越大。
- 行级锁:锁住表中的一行记录。粒度最小,并发性最高,是主流关系型数据库(如MySQL的InnoDB、PostgreSQL)的默认或常用级别。
- 页级锁:锁住一页数据(数据库存储的基本单位,通常包含多行)。粒度介于行锁和表锁之间。
- 表级锁:锁住整张表。粒度最大,实现简单,开销小,但并发性能最差。例如,MySQL的MyISAM引擎就使用表级锁。
选择策略:数据库系统通常会自动选择锁粒度,并在开销和并发性之间做权衡。
第四步:封锁协议与隔离级别
仅仅有锁还不够,还需要规定事务何时加锁、何时释放锁。这套规则就是“封锁协议”。SQL标准通过定义不同的事务隔离级别,来对应不同的封锁协议严格程度,从而解决不同的并发问题。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 锁策略简述 |
|---|---|---|---|---|
| 读未提交 | ❌ 可能 | ❌ 可能 | ❌ 可能 | 写数据时加短暂的排他锁(事务结束释放),读不加锁。 |
| 读已提交 | ✅ 避免 | ❌ 可能 | ❌ 可能 | 写数据加排他锁(事务结束释放);读数据时加共享锁,读完立即释放。 |
| 可重复读 | ✅ 避免 | ✅ 避免 | ❌ 可能 | 写数据加排他锁(事务结束释放);读数据时加共享锁,直到事务结束才释放。 |
| 可串行化 | ✅ 避免 | ✅ 避免 | ✅ 避免 | 最严格。可能在范围查询时加“间隙锁”来防止幻读。 |
关键点:锁的持有时间至关重要。“读已提交”级别下,共享锁读完就放,所以其他事务可以在你两次读取之间修改数据,导致“不可重复读”。而“可重复读”级别下,共享锁会一直持有到事务结束,从而保证了在事务内多次读取结果一致。
第五步:死锁与解决
当多个事务循环等待对方持有的锁时,就会发生死锁。
-
场景模拟:
- 事务A持有锁1,并请求锁2。
- 事务B持有锁2,并请求锁1。
- 此时,事务A在等B,事务B在等A,双方都无法继续,形成死锁。
-
数据库的解决方案:
- 预防:在事务开始时,一次性申请所有可能需要的锁,或者规定一个统一的加锁顺序。但这种方法会降低系统吞吐量。
- 检测与解除(主流方案):数据库允许死锁发生,但会定期检测。一旦检测到死锁,它会选择一个“牺牲者”事务(通常是根据回滚代价最小等策略),将其回滚并释放其持有的所有锁,从而让其他事务可以继续执行。被回滚的事务会收到错误信息,需要应用程序重新发起。
总结:
数据库锁机制是一个精巧的系统,它通过共享锁和排他锁这两种基本工具,作用在行、表等不同粒度的数据上,并遵循不同的封锁协议(对应不同隔离级别) 来在数据一致性和系统并发性能之间取得平衡。同时,通过死锁检测和回滚机制来处理不可避免的资源竞争问题,确保了数据库在高并发场景下的稳定运行。