事务与并发控制
事务与并发控制
事务
事务是由一些列DML操作组成,这些操作要么全做,要么全不做。它从第一个DML操作开始,到rollback 、commit或者DDL结束。拥有4个特性(ACID):
- 原子性:(操作是原子的)要么全做,要么全不做。事务是最小的执行单位。
- 一致性:(数据是一致的)事务发生后数据是一致的。例如银行转账,不会存在A账户转出,但B账户没有收到的情况。
- 隔离性:(执行是隔离的)任一事务的更新操作直到其成功提交的整个过程对其他事务都是不可见的,不同事务之间是隔离的,互不干涉。
- 持久性:(改变是持久的)一个事务提交之后,它对数据库数据的改变是持久的。及时数据库发生故障也不应该对其有任何影响
并发控制
事务是并发控制的前提条件,并发控制就是控制不同的事务并发执行,提高系统效率,但是并发控制中存在三个问题:
并发控制存在的问题

丢失更新
事务1读取A数据时,另外事务2也访问了A数据,如果在事务1中修改了A数据后,事务2也修改了A数据,那么导致事务1修改的数据丢失的情况称为丢失更新
不可重复读
事务1读取A数据时,事务2修改了数据A,此时事务1再次读取A数据发现2次读取到的数据不一样的情况
脏读
事务1读取A变量,修改A数据但未提交,事务2读取A变量后,事务1回滚导致事务2读到的A数据是不对的数据,是脏数据。这种现象称为脏读,也是读“脏”数据。
封锁协议
解决并发控制存在问题的方法是封锁协议,目标是防止三种特定的数据不一致性。锁有2种锁:
- X锁是排它锁(写锁):若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他事务都不能再对A加任何类型的锁,直到T释放A上的锁。
- S锁是共享锁(读锁):若事务T对数据对象A加上S锁,则只允许T读取A,但不能修改A,其他事务只能再对A加S锁(也即能读不能修改),直到T释放A上的S锁
封锁协议共分为三级封锁协议,如下:
一级封锁协议
事务在修改数据R之前必须先对其加X锁,直到事务结束才释放。可解决丢失更新问题。
二级封锁协议
一级封锁协议的基础上加上事务T在读取数据R之前必须先对其加S锁,读完后即可释放S锁。可解决丢失更新、读脏数据问题。
三级封锁协议
一级封锁协议的基础上加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。可解决丢失更新、读脏数据、数据重复读问题
为什么有三级封锁协议还需要二级封锁协议
二级封锁协议不是被三级取代的过时技术,而是在性能与一致性之间的一种重要权衡选择。
- 三级封锁协议提供了更强的一致性保证
- 二级封锁协议在保证基本数据正确性(防止脏读)的前提下,提供了更好的性能
因此,数据库系统通常会同时支持多个隔离级别,让用户根据具体业务需求来选择使用哪种"封锁协议思想",而不是强制使用最强的一致性级别。
首先,三级封锁协议是层层递进的关系:
- 一级:防止丢失修改
- 二级:在一级基础上+防止脏读
- 三级:在二级基础上+防止不可重复读
为什么二级封锁协议仍然在用有如下4点原因。
1. 并发度与性能的权衡
二级封锁协议允许在读完后立即释放S锁,这意味着:
- 更高的并发度:其他事务可以更快地获得锁
- 更好的性能:锁持有时间更短,系统吞吐量更高
三级封锁协议要求S锁持有到事务结束:
- 并发度较低:锁被长时间持有
- 性能开销更大:可能造成更多的阻塞
2. 对应不同的隔离级别
在实际数据库系统中,不同的封锁协议对应不同的隔离级别:
| 封锁协议 | 对应隔离级别 | 读音 | 特定 |
|---|---|---|---|
| 一级封锁协议 | READ UNCOMMITTED(read uncommitted) | 允许脏读、允许不可重复读 | |
| 二级封锁协议 | READ COMMITTED(read committed) | 防止脏读、允许不可重复读 | |
| 三级封锁协议 | REPEATABLE READ(repeatable read) | /rɪˈpiːtəbl/ - 可重复的; | 防止脏读和不可重复读 |
3. 应用场景不同
适合使用二级封锁协议(READ COMMITTED)的场景:
- 大多数OLTP(在线事务处理)系统的默认设置
- 统计报表查询(不要求重复读结果一致)
- 实时数据展示系统
- 对性能要求高于一致性要求的场景
适合使用三级封锁协议(REPEATABLE READ)的场景:
- 银行交易系统
- 库存管理系统
- 需要保证数据一致性的关键业务
4. 实际数据库的实现
以主流数据库为例:
- Oracle默认:READ COMMITTED(相当于二级封锁协议思想)
- MySQL InnoDB默认:REPEATABLE READ(相当于三级封锁协议思想)
- PostgreSQL默认:READ COMMITTED
这说明不同的数据库根据其设计目标和应用场景选择了不同的默认级别。
两段锁协议
是保证可串行化的充分条件
两段锁协议的规则非常简单粗暴:
- 扩展阶段:事务只能不断地申请新的锁,但不能释放任何锁。
- 收缩阶段:事务只能不断地释放已经持有的锁,但不能申请任何新的锁。
核心思想: 将事务的生命周期严格划分为两个阶段,锁的申请和释放不能交错进行。
为什么它能保证可串行化?
因为2PL有效地将并发事务的执行在时间上“固定”了下来。一旦一个事务开始释放锁,就意味着它不会再请求任何新资源,它的“需求”已经固定,不会因为后续的操作再去和别的事务产生新的冲突。这保证了事务的交叉执行顺序等价于一个串行的顺序。
重要事实: 遵守封锁协议的事务,不一定满足两段锁协议。最典型的例子就是二级封锁协议:它要求读完后立即释放S锁,然后在事务后面可能又会去申请X锁。这就出现了 申请S锁 -> 释放S锁 -> 申请X锁 的局面,锁的申请和释放是交错的,不满足2PL。因此,一个遵守二级封锁协议的事务调度,可能是不可串行化的。
封锁协议与两段锁协议的区别与关系
区别
他们是两种不同的协议,它们的目标和层次不同:
- 目标不同:
- 三级封锁协议:目标是防止三种特定的数据不一致性。它是一个“问题清单”式的解决方案。
- 两段锁协议:目标是保证可串行化。这是一个更根本、更通用的正确性标准。可串行化本身就已经蕴含了不会出现丢失修改、脏读和不可重复读。
- 层次关系:
- 你可以把2PL看作是一个强大的框架。在这个框架下,你可以实施不同的锁规则(比如三级协议)。
- 一个系统如果采用了2PL框架,并且在此基础上,规定读锁(S锁)在事务结束后才释放(即融入了三级封锁协议的思想),那么它既能保证可串行化(得益于2PL),又能高效地避免三种数据异常(得益于三级协议)。
- 充分 vs 必要:
- 2PL是可串行化的充分条件(遵守2PL,一定是可串行化的)。
- 三级封锁协议不是可串行化的充分条件(仅遵守三级协议,不一定是可串行化的,如二级封锁协议的例子)。
关系
现代数据库管理系统通常将两者结合使用:
- 它们以两段锁协议作为并发控制的核心框架,以确保所有并发事务调度的正确性(可串行化)。
- 在这个框架下,它们具体实现的锁规则(例如,读写锁的类型、锁的粒度、锁的持有时间)则借鉴了三级封锁协议的思想,以在保证正确性的前提下,尽可能地提高并发度。
“两段锁协议提供了正确性的基础保障,封锁协议及其衍生的规则在这个保障之上解决了更具体的并发问题并优化了性能”,它们是相辅相成的。
数据库是如何使用这两者并让它们交互
核心架构:2PL作为框架,三级协议作为规则
两段锁协议 (2PL)
↓ (作为并发控制的骨架)
扩展阶段 + 收缩阶段
↓ (在这个框架内填充具体规则)
三级封锁协议的思想
↓ (最终实现)
不同的事务隔离级别1. 锁管理器的基础设施
数据库首先建立一个锁管理器,负责:
- 锁的申请和释放
- 死锁检测和处理
- 锁兼容性检查
2. 2PL框架的强制执行
数据库在事务处理中强制实施2PL规则:
-- 事务开始
BEGIN TRANSACTION;
-- 扩展阶段:只能加锁,不能释放
SELECT * FROM accounts WHERE id = 1; -- 自动加S锁
UPDATE accounts SET balance = 100 WHERE id = 1; -- 自动加X锁
-- 一旦开始释放锁,就进入收缩阶段,不能再申请新锁
COMMIT; -- 在提交时释放所有锁3. 三级协议规则的集成
在2PL框架内,数据库根据设置的隔离级别,应用不同的锁持有策略:
READ UNCOMMITTED (近似一级协议)
# 在2PL框架内:
def read_uncommitted():
# 扩展阶段:写数据时加X锁,但不加S锁
# 允许读取未提交数据
# 收缩阶段:事务结束时释放X锁READ COMMITTED (二级协议思想)
def read_committed():
# 在2PL框架内,但特殊处理S锁:
# 扩展阶段:
# - 读数据时加S锁,但读完立即释放(违反纯2PL,但数据库特殊处理)
# - 写数据时加X锁,持有到事务结束
# 收缩阶段:释放剩余锁REPEATABLE READ (三级协议思想)
def repeatable_read():
# 严格遵守2PL:
# 扩展阶段:读数据加S锁,写数据加X锁
# 收缩阶段:事务结束时一次性释放所有锁
# 这样S锁和X锁都持有到事务结束,防止不可重复读实际交互过程示例
让我们通过一个具体例子来看两者如何协作:
-- 事务开始 - 进入2PL扩展阶段
BEGIN TRANSACTION;
-- 查询1:数据库根据隔离级别加锁
-- 如果隔离级别是READ COMMITTED:加S锁,查询完立即释放
-- 如果隔离级别是REPEATABLE READ:加S锁,持有到事务结束
SELECT balance FROM accounts WHERE id = 1;
-- 更新操作:无论什么隔离级别,都加X锁并持有到事务结束
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
-- 查询2:再次读取
-- READ COMMITTED:可能重新加S锁,可能读到其他事务的修改
-- REPEATABLE READ:使用之前持有的S锁,保证读到相同数据
SELECT balance FROM accounts WHERE id = 1;
-- 提交 - 进入2PL收缩阶段,释放所有持有的锁
COMMIT;完