本地事务
以下都是基本原理,请不要与 MySQL 挂钩!MySQL 的基本思想肯定是这样,但肯定不是完全照搬。
原子性和持久性
按照事务提交时点为界,划分为 FORCE 和 STEAL 两类情况:
- FORCE:指的是事务的实际写入发生在提交之后。当事务提交后,要求数据必须同时完成写入则称为 FORCE,如果不强制数据必须同时完成写入则称为 NO-FORCE。现实中绝大多数数据库采用的都是 NO-FORCE 策略,因为只要有了日志,变动数据随时可以持久化,从优化磁盘 I/O 性能考虑,没有必要强制数据写入立即进行。
- STEAL:在事务提交前,允许数据提前写入则称为 STEAL,不允许则称为 NO-STEAL。从优化磁盘 I/O 性能考虑,允许数据提前写入,有利于利用空闲 I/O 资源,也有利于节省数据库缓存区的内存。
在数据库中,因为写入磁盘这个操作不是原子操作,所以可能出现崩溃的情况。为了保证事务的原子性和持久性,通常通过日志(顺序追加,这是最高效的写入方式)的方式,分为两种方法:
- Commit Logging:允许 NO-FORCE,但是不允许 STEAL。
- Write-Ahead Logging:允许 NO-FORCE,也允许 STEAL。
通过日志实现事务的原子性和持久性是当今的主流方案,但并不是唯一的选择。除了日志外,还有一种称之为 Shadow Paging(影子分页)来实现,SqLite v3 就是这种机制。
Shadow Paging 在数据写入磁盘时,不会直接修改原来的数据,而是将原来的数据复制一份副本,保留原数据只修改副本。事务成功提交后,最后一步是去修改数据的引用指针,将引用从原数据改为新复制出来修改后的副本(这个修改指针被认为是原子操作)。但是 Shadow Paging 并发能力不强,所以应用不多。
Commit Logging
Commit Logging 中只有一种日志,就是 redo 日志。对数据的操作都会记录在 redo 日志中,事务的提交就是在日志中写入代表提交成功的提交记录(Commit Record)。在数据库看到提交记录(Commit Record)后,才根据日志内容对数据进行真正的修改,修改完成后向日志中加入一条结束记录(End Record)表示事务已经成功完成持久化。
一旦写入了 Commit Record,即使在写入磁盘之前崩溃了,重启后也能根据日志恢复,保证了持久性。如果在日志没有 Commit Record 时就发生了崩溃,则整个任务就是失败的,因为没有真正写入磁盘,所以就好像发生了回滚一样,保证了原子性。
Commit Logging 的原理非常清晰,如 OceanBase 就是采用了这种方法。但是 Commit Logging 的一个缺陷就是:所有对数据的真实修改必须发生在提交之后,在提交之前即使磁盘 I/O 是空闲的也不允许写入数据,并且占用了大量的内存缓冲区。
因为 Commit Logging 只有 redo log,所以不允许 STEAL,否则无法回滚(回滚需要 undo log)。
Write-Ahead Logging
Write-Ahead Logging 是改进的方案,所谓 Write-Ahead(提前写入)指的就是在提交之前写入数据。Write-Ahead Logging 在 redo log 的基础上,引入了 undo log 用于回滚。当变动数据写入磁盘前,必须先记录 Undo Log,注明修改了哪个位置的数据、从什么值改成什么值等等。以便在事务回滚或者崩溃恢复时根据 undo log 对提前写入的数据变动进行擦除。
在崩溃恢复时,根据 redo log 对没有写入磁盘但是已经提交的数据进行持久化,根据 undo log 对没有提交但是已经写入磁盘的数据进行回滚。
redo log 保证事务的持久性,而 undo log 保证事务的原子性。
隔离性
锁
实现隔离性最直观的方式,就是加锁。现代数据库均提供以下三种锁:
- 写锁:对一行数据加写锁,写锁与其他锁都互斥。
- 读锁:对一行数据加读锁,读锁与读锁不互斥,与写锁互斥。
- 范围锁:对一个范围加锁。
注意,范围锁不等于对多行数据加锁,因为数据与数据之间有间隙(这就是为什么 MySQL 中有间隙锁)。
隔离性用隔离级别来体现,有以下几种隔离级别(这几种隔离级别都解决了脏读问题):
- 可串行化(Serializable):可串行化是最高的隔离级别,也是并发程度最低的隔离级别。可串行化中相当于全局加锁,不存在幻读、不可重复读、脏读问题。
- 可重复读(Repeatable Read):可重复读中,对于事务中涉及到的数据会加上读锁和写锁,但不会加范围锁。不加范围锁导致的问题,就是会出现幻读问题。
- 读已提交(Read Commited):在读已提交中,写锁会持续到事务结束,读锁在查询完成后会马上释放。读锁在查询结束后马上释放的带来的问题,就是会导致不可重复读。
- 读未提交(Read Uncommitted):读未提交中,写锁会持续到事务结束,但是完全不会加读锁。不加读锁带来的问题就是脏读,可能会读取到其他事务还未提交的数据。
在 MySQL 中的默认隔离级别未可重复读,但它在只读事务中可以完全避免幻读问题。
MVCC
MVCC 多版本并发控制是一种读优化策略,MVCC 的基本思路是对数据库的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本共存,以此达到读取时可以完全不加锁的目的。
- 如果隔离级别是可重复读:读取小于等于当前事务 ID 的最大版本。
- 如果隔离级别是读已提交:读取最新版本。
读未提交直接修改原始数据即可,不需要版本。