封锁协议
在运用 X 锁和 S 锁对数据对象加锁时,还需要约定一些规则 ,例如何时申请 X 锁或 S 锁、持锁时间、何时释放等。这些规则被称为封锁协议。对封锁方式规定不同的规则,就形成了各种不同的封锁协议。不同的封锁协议,在不同的程度上为并发操作的正确调度提供一定的保证。
一级封锁协议
事务在修改记录之前必须要添加 X 锁,直到事务结束(提交或者 ROLLBACK)才释放。一级封锁协议可以解决丢失修改。但是一级封锁协议在读取记录时不会加锁,所以会出现不可重复读和读取脏数据。
丢失修改:不加锁修改后,该记录又被其他事务修改。
读:不添加锁;
写:加 X 锁 -> 修改记录 -> 成功或失败 -> 释放 X 锁;
二级封锁协议
二级封锁协议在一级封锁协议的基础上,在事务读取记录之前添加 S 锁,在读取结束后释放 S 锁。因为事务读取之前添加了 S 锁,所以可以防止读取脏数据,但是由于事务读取结束就释放了 S 锁,所以不能保证可重复读。
- 读:读取前添加 S 锁 -> 读取记录 -> 读取结束后释放 S 锁;
- 写:同一级封锁协议;
三级封锁协议
和二级封锁协议类似,事务读取记录之前必须添加 S 锁,但是三级封锁协议在事务结束后才释放 S 锁,这进一步防止了不可重复读。
- 读:读取前添加 S 锁 -> 读取记录 -> 事务完成 -> 释放 S 锁;
- 写:同一级封锁协议;
不同隔离级别对应的封锁协议
RU(读未提交):遵循一级封锁协议;
RC(读已提交):遵循二级封锁协议;
RR(可重复读):遵循二级封锁协议,使用 MVCC 机制实现可重复读;
Serializable(串行化):遵循三级封锁协议;
一段锁协议
一段锁协议要求事务开始时一次性申请所有的锁,之后不再申请任何锁。如果其中的某个锁不可用,则整个申请就不成功,事务就不会执行。在事务的尾端,一次性释放所有的锁。
一次性锁协议不会产生死锁的问题,但是事务的并发度很低。
两段锁协议
MySQL 遵循的是两段锁协议,将整个事务分为加锁阶段和解锁阶段。
- 加锁阶段:该阶段的事务只能操作数据和加锁。任何读操作之前都需要申请获得 S 锁,任何写操作之前需要申请 X 锁。如果加锁不成功,则事务等待,直到加锁成功后继续执行。
- 解锁阶段:当事务释放第一个锁,就进入了解锁阶段。在该阶段,事务只能操作数据和解锁,但是不能再加锁。
因为两段锁协议保证事务可以在结尾之前解锁,所以事务可以具有较高的并发度,并且保证事务的并发调度是串行化。但是两段锁协议的缺点是没有解决死锁的问题,因为在加锁阶段没有顺序要求,如果两个事务分别申请了 A、B 锁,接着又申请了对方的锁,此时进入死锁状态。