有多个原因让数据库不得不回滚事务:
- 因为用户取消
- 因为服务器或网络故障
- 因为事务破坏了数据库完整性(比如一个列有唯一性约束而事务添加了重复值)
- 因为死锁
这怎么可能呢?为了回答这个问题,我们需要了解日志里保存的信息 。
日志
事务的每一个操作(增/删/改)产生一条日志,由如下内容组成:
- LSN:一个唯一的日志序列号(Log Sequence Number) 。LSN是按时间顺序分配的 *,这意味着如果操作 A 先于操作 B,log A 的 LSN 要比 log B 的 LSN 小 。
- TransID:产生操作的事务ID 。
- PageID:被修改的数据在磁盘上的位置 。磁盘数据的最小单位是页,所以数据的位置就是它所处页的位置 。
- PrevLSN:同一个事务产生的上一条日志记录的链接 。
- UNDO:取消本次操作的方法 。
比如,如果操作是一次更新,UNDO将或者保存元素更新前的值/状态(物理UNDO),或者回到原来状态的反向操作(逻辑UNDO) ** 。 - REDO:重复本次操作的方法 。同样的,有 2 种方法:或者保存操作后的元素值/状态,或者保存操作本身以便重复 。
- …:(供您参考,一个 ARIES 日志还有 2 个字段:UndoNxtLSN 和 Type) 。
*LSN的分配其实更复杂,因为它关系到日志存储的方式 。但道理是相同的 。
** ARIES 只使用逻辑UNDO,因为处理物理UNDO太过混乱了 。
注:据我所知,只有 PostgreSQL 没有使用UNDO,而是用一个垃圾回收服务来删除旧版本的数据 。这个跟 PostgreSQL 对数据版本控制的实现有关 。
为了更好的说明这一点,这有一个简单的日志记录演示图,是由查询 “UPDATE FROM PERSON SET AGE = 18;” 产生的,我们假设这个查询是事务18执行的 。
文章插图
每条日志都有一个唯一的LSN,链接在一起的日志属于同一个事务 。日志按照时间顺序链接(链接列表的最后一条日志是最后一个操作产生的) 。
日志缓冲区
为了防止写日志成为主要的瓶颈,数据库使用了日志缓冲区 。
文章插图
当查询执行器要求做一次修改:
- 1) 缓存管理器将修改存入自己的缓冲区;
- 2) 日志管理器将相关的日志存入自己的缓冲区;
- 3) 到了这一步,查询执行器认为操作完成了(因此可以请求做另一次修改);
- 4) 接着(不久以后)日志管理器把日志写入事务日志,什么时候写日志由某算法来决定 。
- 5) 接着(不久以后)缓存管理器把修改写入磁盘,什么时候写盘由某算法来决定 。
STEAL 和 FORCE 策略
出于性能方面的原因,第 5 步有可能在提交之后完成,因为一旦发生崩溃,还有可能用REDO日志恢复事务 。这叫做 NO-FORCE策略 。
数据库可以选择FORCE策略(比如第 5 步在提交之前必须完成)来降低恢复时的负载 。
另一个问题是,要选择数据是一步步的写入(STEAL策略),还是缓冲管理器需要等待提交命令来一次性全部写入(NO-STEAL策略) 。选择STEAL还是NO-STEAL取决于你想要什么:快速写入但是从 UNDO 日志恢复缓慢,还是快速恢复 。
总结一下这些策略对恢复的影响:
- STEAL/NO-FORCE 需要 UNDO 和 REDO: 性能高,但是日志和恢复过程更复杂 (比如 ARIES) 。多数数据库选择这个策略 。注:这是我从多个学术论文和教程里看到的,但并没有看到官方文档里显式说明这一点 。
- STEAL/ FORCE 只需要 UNDO.
- NO-STEAL/NO-FORCE 只需要 REDO.
- NO-STEAL/FORCE 什么也不需要: 性能最差,而且需要巨大的内存 。
Ok,有了不错的日志,我们来用用它们!
假设新来的实习生让数据库崩溃了(首要规矩:永远是实习生的错 。),你重启了数据库,恢复过程开始了 。
ARIES从崩溃中恢复有三个阶段:
- 1) 分析阶段:恢复进程读取全部事务日志,来重建崩溃过程中所发生事情的时间线,决定哪个事务要回滚(所有未提交的事务都要回滚)、崩溃时哪些数据需要写盘 。
推荐阅读
- 如何挑选黑豆
- 不是夫妻合租房犯罪吗
- 医保断缴三个月余额清零?员工可自愿放弃社保?这些谣言别再信了
- 手串颗数大有讲究,千万别戴错了
- “禁止长时间停车”,到底指的是几分钟?交警:最后再说一遍
- 太热了!别再披头散发了,这4款发型够美够清凉
- 没钱还信用卡有什么解决办法
- 信用卡过期卡怎么处理
- 信用卡逾期被注销怎么还钱
- 如何找回抖音私聊记录