数据库事务--原子性和持久性原理(2)
例子引入
对于原子性和持久性的解释可以看上一篇文章。
下面是一个简单的例子:
用户在一个网站上注册了信息,这个功能最终需要保存用户信息到数据库上。假如数据库没有任何保护措施。在程序将数据从内存搬运到对应磁盘位置时,断电了(crash),这就会产生脏数据了。如果真的这样发生了,那就相当于数据库没有一点保护措施。
问题解决
方案一
也是业内最常用的方案,保留中间状态,比如增加操作日志 commit log。在保存用户信息之前,先写入一个 commit log 中(这个磁盘操作可以顺序追加,比正常写快 10 多倍),写完之后,就可以提交该事务了。至于 commit log 搬运到对应的数据库位置,可以用定时任务刷新。
说明一下,这个方案为什么能解决问题。
第一种情况:commit log 写失败,当作事务失败,这部分数据丢弃了,也没有影响到正常数据
第二种情况:commit log 写成功,事务成功,数据库数据的迁移,无论是否 crash,都可以利用 commit log 的数据进行恢复操作,重试几次,总会成功
commit log 的意义就是为正式数据提交保留了一个数据备份和事务是否提交的标识。
方案缺陷:大数据量变更场景下存在性能问题。因为数据库数据必须在 commit log 的数据提交之后才能开始移动。而 commit log 一般保留的是数据的物理备份,而不是 sql 一样的逻辑备份,大数据量场景下保存的数据会比较多。影响了整体系统的吞吐量。
方案二
再引入一个 undo log,它与 commit log 不太一样,它是保存数据发生变更之前的原版本,记录下原数据本身,就可以放心的在事务提交之前对数据进行修改。一旦发生什么问题,都可以回滚到原始版本数据。
对比
MySQL经典三个文件的对比
| bin log | redo log | undo log | |
|---|---|---|---|
| 数据内容 | 保存执行的sql | 记录具体在哪个数据块修改的最新内容 | 记录与此次执行内容相反的操作,也是一个sql |
| 处理事务粒度 | 一个事务 | 以一个事务为粒度 | 以一条具体的数据为粒度 |
| 提供的能力 | 主从复制,数据崩溃恢复 | 事务 crash 后的恢复数据的能力,事务提交的标志是 redo log 有没有记录最后的 commit record,没有记录就当作事务失败,丢弃这个变更。无论怎么样,都不会影响实际的数据 | 提供行记录的历史版本,让记录 redo log 时可以放心的修改数据库数据,有数据的历史版本就不怕不能回滚 |
| 核心功能 | 主从复制 | 保障持久性,即 crash-safe,顺序读写加快读写速度 | 记录历史版本数据,也可用于 MVCC |
| 落盘时机 | redo日志落盘之前 | 事务提交之前落盘(因为redo log在内存有缓冲区,实际落盘时间可以配置的,默认是提交前落盘) | 落盘时机是在事务提交之前 |
引申一下
我们可能会看过bin log和redo log有一个二阶段提交。确保 binlog 和 redo log 在事务提交前保持一致。这个实际上和MySQL的持久化能力无关的,主要是为了保证MySQL的主库和从库的数据一致性。MySQL使用bin log做从库的数据同步,使用redo log做主库的崩溃恢复,两个日志内容保持一致才是可以保证主从的一致性,所以这个二阶段提交更多是一个分布式事务的操作。具体过程:
- 第一阶段:准备阶段(Prepare Phase)
当事务执行完毕,MySQL 准备提交时,它首先会将事务的修改记录写入 redo log,并将 redo log 标记为 PREPARED 状态。
此时,redo log 并不会立即提交,而是先记录事务的修改,但还未正式完成事务。
在此阶段,binlog 也会生成,并暂时存储在内存的缓存中。
目的:这一步确保如果系统在接下来崩溃,重启时 MySQL 可以通过 redo log 来判断这个事务是未完成的,不会丢失事务修改。
- 第二阶段:提交阶段(Commit Phase)
MySQL 首先将 binlog 刷入磁盘,确保二进制日志持久化。
然后将 redo log 标记为 COMMITTED 状态,并将其刷入磁盘。这时,事务正式完成并提交。
目的:确保当事务提交时,binlog 和 redo log 都已经持久化到磁盘,并且保持一致。如果此时系统崩溃,可以使用这两个日志来恢复事务。