数据库系统原理
事务
四大特性—ACID
- 原子性(Atomicity)
- 事务不可再分,要么执行成功、要么失败回滚。回滚操作采用混滚日志(Undo Log)的方式实现。
- 一致性(Consistency)
- 事务执行前后数据库保持一致性状态,所有事务对同一个数据的读取结果都是相同的。
- 隔离性(Isolation)
- 某个事务所作的修改在提交以前对其他事务不可见
- 持久性(Durability)
- 事务一旦提交,所作的修改将永远保存到数据库中。即使系统发生崩溃,也可以通过重做日志(Redo Log)的方式恢复。
ACID的关系
- 事务的持久化是为了应对系统崩溃造成的数据丢失
- 只有保证了事务的一致性,才能保证执行结果的正确性
- 在非并发状态下,事务间天然保证隔离性,因此只需要保证事务的原子性即可保证一致性
- 在并发状态下,需要严格保证事务的原子性、隔离性。

并发一致性问题—并发状态下如何保证隔离性
并发状态下会出现的一致性问题
- 丢失修改—并发修改
- 事务A所作的修改被并发事务B的修改覆盖
- 脏读—修改时读
- 事务A读到了并发事务B未提交的修改
- 不可重复读—读时修改
- 事务A读取数据期间并发事务B对数据进行了修改,导致事务A读取前后数据不同
- 幻读
- 事务A读取某个范围的数据期间并发事务B插入的新数据,导致事务A前后读取的该范围数据不同
事务加锁/封锁
-
解决问题 前三种问题(丢失修改、脏读、不可重复读)是由于并发事务对同一数据进行修改导致的,可以通过对事务加锁的方式解决
-
封锁粒度—行锁、表锁(MySQL服务器只支持表锁,只有存储引擎入InnoDB等实现了表锁)
-
封锁原则—满足要求的情况下锁定尽可能少的数据,但更小的锁粒度也会带来更大的锁开销
-
封锁类型—读写锁、意向锁
- 读写锁——写锁/互斥锁/X锁、读锁/共享锁/S锁
- 加了S锁则只能再加S锁,且只能读取数据而不能修改
- 加了X锁则不能再加任何所,只有持有锁的事务能够进行读取、修改
- 意向锁——再X/S锁的基础上增加了意向表锁IX/IS, 表明想要对表加X/S锁(并不是真正加锁)
- 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁
- 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁
- 事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁
- 任意 IS/IX 锁之间都是兼容的,因为它们只表示想要对表加锁,而不是真正加锁
- 这里兼容关系针对的是表级锁,而表级的 IX 锁和行级的 X 锁兼容,两个事务可以对两个数据行加 X 锁。(事务 T1 想要对数据行 R1 加 X 锁,事务 T2 想要对同一个表的数据行 R2 加 X 锁,两个事务都需要对该表加 IX 锁,但是 IX 锁是兼容的,并且 IX 锁与行级的 X 锁也是兼容的,因此两个事务都能加锁成功,对同一个表中的两个数据行做修改。)
- 读写锁——写锁/互斥锁/X锁、读锁/共享锁/S锁
-
封锁协议
-
三级封锁协议
-
一级封锁(解决丢失修改)
事务修改时加X锁,直到事务结束释放 -
二级封锁(解决脏读)
一级封锁基础上(修改时加X锁),读取时必须加S锁,直到事务结束立即释放 -
三级封锁(解决不可重复读)
二级封锁基础上,读取时必须加S锁
-
-
两段锁协议
- 加锁、释放分两个阶段进行(先全部加锁、再全部释放)
- 串行化调度 并发事务执行结果与串行执行结果一致
-
隔离等级
- 事务的隔离级别
-
读未提交(READ UNCOMMITTED)
事务中的修改,即使没有提交,对其它事务也是可见的。 -
读提交(READ COMMITTED)
一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。 -
可重复读(REPEATABLE READ)
保证在同一个事务中多次读取同一数据的结果是一样的。 -
可串行化(SERIALIZABLE)
强制事务串行执行,这样多个事务互不干扰,不会出现并发一致性问题。
该隔离级别需要加锁实现,因为要使用加锁机制保证同一时间只有一个事务执行,也就是保证事务串行执行。
-
多版本并发控制MVCC——实现隔离等级(读提交、可重复读)的方式
- 写操作更新最新版本的快照,读操作读旧版本快照,一个事务只能读取已提交的快照
- MVCC 中事务的修改操作(DELETE、INSERT、UPDATE)会为数据行新增一个版本快照
- 版本号:
- 系统版本号SYS_ID: 是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增
- 事务版本号TRX_ID: 事务开始时的系统版本号
- Undo日志:
- 每一行记录拥有一系列快照
- 快照中包含对该行数据的操作、版本号(对该行数据进行操作的事务版本号)、该行记录的关键字
- 同一行记录的快照使用回滚指针(ROLL_PTR)串联, 从新的版本号(大)指向旧版本号(小)
- ReadView:
- 记录了当前系统内所有未提交事务的列表
- 读快照的规则:
- 查找要读取记录的最新事务号TRX_ID
- 与ReadView中最小事务号TRX_ID_MIN和最大事务号TRX_ID_MAX比较,若TRX_ID < TRX_ID_MIN则可以读取
- 若TRX_ID > TRX_ID_MAX则无法读取
- 若TRX_ID > TRX_ID_MIN 且 TRX_ID < TRX_ID_MIN则需要参考隔离级别
- 读提交: 如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。
- 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。
- 快照读和当前读
- 读(Select)快照
- 写操作永远使用加锁的方式获取最新数据
Next-Key Locks
- RecordLocks: 锁定一个记录上的索引,而不是记录本身。如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
- GapLocks: 锁定索引之间的间隙,但是不包含索引本身
- Next-KeyLocks: 它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙, 它锁定一个前开后闭区间