注:本文章引自终于把分布式事务讲明白了!
事务的ACID
了解数据库的同学应该都知道数据库中一个非常重要的概念就是“事务”,而我们通常描述“事务”是什么的时候一定是离不开ACID这个词儿,ACID是原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)这四个特性的简称。针对事务的ACID的实现,数据库都有对应的通常的实现方式,可以通过如下表格了解。
属性 | 含义 | 数据库实现方式 |
---|---|---|
原子性A | 指一个事务中的操作要么全部执行,要么全部不执行。 | WAL,分布式中:两阶段提交协议 |
一致性C | 指数据库要保证事务执行一定是从一个一致的状态转移到另一个一致的状态 | 支持了A、I、D也就支持了C |
隔离性I | 多个事务并发执行,对每个事务来说不会感知到系统中有其他事务在同时执行 | MVCC多版本并发控制、两阶段加锁(2PL)、乐观并发控制(OCC) |
持久性D | 一个事务提交后对数据库的改变是持久的 | WAL+存储管理 |
下图是一个disk-oriented数据库的架构。除了disk-oriented,还有main-memory数据库,即主存数据库。
通过上图可以看出,数据库中有几个主要的部分:
- 资源管理器:即图中的索引/文件/记录管理器,它是负责管理各种数据库资源的。
- 缓冲区管理器:它是负责管理缓冲区的。缓冲区一般包括两大类:存放数据页的buffer pool和存储日志文件的log buffer。
- 存储管理器:负责从存储里面获取数据页。
- 事务管理器:如果需要执行事务,就需要通过事务管理器来协调调度、跟踪事务的各个执行阶段和状态。通过资源管理器和日志恢复模块,保证事务的原子 性和持久性。并发控制模块通过锁管理器等模块来保证事务的隔离性。
关于数据库存储介质,主要分为以下三类:
- 易失性存储器,包括CPU的寄存器,cache以及主存等,它们的特点是当断电之后上数据就会丢失。
- 非易失性存储器,包括Disk、SSD、NVM等,它们的特点是断对数据不会产生影响。
- 稳定存储器,理论上是不可能被保证的,因为物理存储介质都有被损坏的可能,所以只是一个概念。
下图描述不同存储介质的特点,可以发现,越靠近CPU存储速度越快,容量越小,单位成本越高;越远离CPU存储访问越慢,容量越大,单位成本越低。不同硬件的特点导致访问数据耗时的不均衡性,在数据库中一般会尽量把经常访问的数据缓冲在一个buffer pool中进行操作,而不是直接去操作磁盘上的数据文件。
缓冲区Buffer Pool
数据库中的数据文件是以block/page组成,每个页面大小为32K或64K,页面中存放用户的数据。数据库在启动实例时,buffer pool在内存中开辟一大块空间,按page组织和管理,通常buffer pool会和log buffer以及其他的一些内存区共同构成数据库的SGA(全局缓冲区)。
缓冲区管理器和存储管理器之间主要有以下四种操作:
- Input(page):从磁盘里读page到buffer中进行操作
- Output(page):page更新完毕,把buffer的page写回到磁盘
- Pin(page):如果page被pin信,将不允许写回磁盘,pin是防止正在修改的数据页被刷到磁盘,加共享锁或揍他锁是为了并发控制。
- Unpin(page):操作和Pin相反
缓冲区管理策略
对于缓冲区的管理,有4类管理策略:
- Force - 事务提交时,所修改的页面强制刷新到磁盘中
- No-Force - 事务提交时,所修改的页面不需要强制刷新到磁盘中
- Steal - 允许Buffer Pool中未提交事务修改的脏页刷到磁盘中
- No-Steal - 不允许Buffer Pool中未提交事务修改的脏页刷到磁盘中
为了保证事务的ACID特性,按理应该使用Force + No-Steal的管理策略。但是这样会带来一些问题:
- Force策略的问题 - 每次提交事务要把buffer pool中的脏页刷写到磁盘,会造成磁盘频繁的随机写操作,导致性能下降。
- No-Steal策略的问题 - 如果不允许未提交事务脏页刷盘,会导致系统并发量不高,假如有一些事务修改大量数据将buffer pool几乎占满但一直未提交,其他事务就无法申请空闲页来取数据。
所以一般情况下对于数据库来说不会采用Force + No-Steal的策略,因此这样会导致数据库几乎不可用。通常会采用No-Force + Steal的策略,可以达到较好的性能,但是这两种策略会有数据一致性的问题,针对一致性的问题,我们通常日志的方式来解决。
使用日志保证数据一致性
针对No-Force和Steal的问题,我们分别用Redo log和Undo log来解决。
- Redo log - 当事务提交时,不需要强制把脏页刷到磁盘,但为了保证一致性,先把redo log写入日志文件中,redo log记录修改数据对象的新值(After Image,AFIM)
- Undo log - 事务没提交,也可以将脏页刷到磁盘,但了为保证一致性,先把undo log写入日志文件中,undo log记录修改数据对象的旧值(Before Imange,BFIM)
针对buffer pool的4种管理策略,我们可以得到4种不同的组合,对于不同的组合,它们需要的数据一致性保证的方案也是不同的,我们可以用下面表格来观察:
Force | No-Force | |
---|---|---|
Steal | Undo/No-Redo | Undo+Redo,性能最快,但恢复时间最慢 |
No-Steal | No-Undo/No-Redo,性能最快,但恢复时间最快 | Redo/No-Undo |
关于redo log、undo log和不同策略的关系,我们通过下图来看一个示例:
通过上图我们也可以很容易的发现,Undo log记录的是旧值,当发生故障恢复时从后往前做undo操作将数据变更回滚掉;而Redo log记录的是新值,当发生故障恢复时从前往后做redo操作将数据变更重做一次。
通过日志机制,将对数据文件的随机写操作变为对日志文件的顺序写操作,因为对日志的操作是append的方式,如果buffer满了或需要commit事务时,才将log buffer刷到日志文件里面,这个过程是一个顺序操作,因此可以极大提升数据库的性能。
主流数据库在Undo上的区别
对比Oracle、MySQL和PostgreSQL,我们了解到PostgreSQL里面是没有Undo这个概念的,而Oracle和MySQL(通过InnoDB存储引擎实现)是有的。其主要的原因是PostgreSQL中采用了MVCC多版本并发控制技术,在PG中更新一条记录并不是in-place update,而是重新创建一条记录。由于存储空间中已经有这条记录的旧值,所以不需要额外通过undo log还记录旧值。当扫描到一条记录时,可以通过可见性判断来决定这条记录是否对当前的事务可见。因此PostgreSQL以及基于PG的相关数据库产品如Greenplum并没有undo log,事务回滚也不需要undo操作。
但是其实MySQL和Oracle同样也是采用MVCC,为什么他们又需要undo log呢?
这是因为版本的存储方式(Version Storage)不同。在PostgreSQL中,任何对一个tuple的修改都在主表上创建一个新的tuple进行修改,再通过一个指针指向新的tuple。在MySQL和Oracle中的Version Storage是另外一种机制:Delta Storage。他们是把差异变化记录到Delta Storage中形成一个链,这种差异存储被称为rollback segment,即回滚段。关于Version Storage,可以参考《Yingjun Wu, An Empirical Evaluation of In-Memory Multi-Version Concurrency Control, VLDB 2017》