在我撰写 High-Performance Java Persistence training 一书时,我逐步认识到让读者明白关系型数据库工作原理会是了解如何构建高性能的 Java 持久化存储的重要基石。不过关系型数据库中的事务相关的重要概念:原子性、持久性以及检查点等等也是相当绕人。而本文中我希望以相对高屋建瓴的方式来解释关系型数据库内部工作原理,也会涉及一些数据库实现细节。
Data Pages
访问磁盘中的数据往往速度较慢,换言之,内存中数据的访问速度还是远快于 SSD 中的数据访问速度。基于这个考量,基本上所有数据库引擎都尽可能地避免访问磁盘数据。并且无论数据库表还是数据库索引都被划分为了固定大小的数据页(譬如 8 KB)。当我们需要读取表或者索引中的数据时,关系型数据库会将磁盘中的数据页映射入存储缓冲区。当我们需要修改数据时,关系型数据库首先会修改内存页中的数据,然后利用 fsync) 这样的同步工具将改变同步回磁盘中。
Undo log
由于同时可能由多个事务并发地对内存中的数据进行修改,因此关系型数据库往往需要依赖于某个并发控制机制(2PL 或者 MVCC)来保证数据一致性。因此,当某个事务需要去更改数据表中某一行时,未提交的改变会被写入到内存数据中,而之前的数据会被追加写入到 undo log 文件中。
Oracle 或者 MySQL 中使用了所谓 undo log 数据结构,而SQL Server 中则是使用 transaction log 完成此项工作。PostgreSQL 并没有 undo log,不过其内建支持所谓多版本的表数据,即同一行的数据可能同时存在多个版本。总而言之,任何关系型数据库都采用的类似的数据结构都是为了允许回滚以及数据的原子性。
如果当前运行的事务发生了回滚,undo log 会被用于重建事务起始阶段时候的内存页。
Redo Log
某个事务提交之后,内存中的改变就需要同步到磁盘中。不过并不是所有的事务提交都会立刻触发同步,过高频次的同步反而会对应用性能造成损伤。不过根据 ACID 原则,提交之后的事务必须要保证持久性,也就是即使此时数据库引擎宕机了,提交之后的更改也应该被持久化存储下来。这里关系型数据库就是依靠 redo log 来达成这一点,它是一个仅允许追加写入的基于磁盘的数据结构,它会记录所有尚未执行同步的事务操作。相较于一次性写入固定数目的数据页到磁盘中,顺序地写入到 redo log 会比随机访问快上很多。因此,关于事务的 ACID 特性的保证与应用性能之间也就达成了较好的平衡。该数据结构在 Oracle 与 MySQL 中就是叫 redo log,而 SQL Server 中则是由 transaction log 执行,在 PostgreSQL 中则是使用 Write-Ahead Log( WAL )。
下面我们继续回到上面的那个问题,应该在何时将内存中的数据写入到磁盘中。关系型数据库系统往往使用检查点来同步内存的脏数据页与磁盘中的对应部分。为了避免 IO 阻塞,同步过程往往需要等待较长的时间才能完成。因此,关系型数据库需要保证即使在所有内存脏页同步到磁盘之前引擎就崩溃的时候不会发生数据丢失。同样地,在每次数据库重启的时候,数据库引擎会基于 redo log 重构那些***一次成功的检查点以来所有的内存数据页。
总结
上面我们简要讨论的这些原则与考虑都是为了保证基于磁盘的存储的较高吞吐量的同时保证数据一致性。其中,undo lo 主要用于提供原子性(允许回滚),而 redo log 则是保证磁盘页的不可变性。
【本文是51CTO专栏作者“张梓雄 ”的原创文章,如需转载请通过51CTO与作者联系】