🏺 第16章 Oracle 数据备份与恢复
📄 一句话概括

完全恢复就是恢复到「失败那一刻」,一行不丢。它要求三件事: 归档模式、日志完整、备份可用。少一样就只能退化成不完全恢复。 命令是两步:RESTORE 把文件铺回去,RECOVER 把日志应用上去。 NOARCHIVELOG 模式下只能做第一步——所以它救不回备份之后的数据。

📝 本章和前面几章的关系

16.3 讲怎么备。这一篇讲怎么恢复——备份的价值只在这一刻体现。 注意这一篇和下一篇的分工:完全恢复往前滚到底,不完全恢复停在某个点。前者治「文件坏了」, 后者治「数据被误改了」。

一、完全恢复的前提:三个都要满足

① 库在归档模式。没有归档日志,就没法「把备份之后的重做补上」,只能恢复到备份点。 ② 日志完整。归档日志从备份点一直连续到失败点,中间不能缺。 缺一段就滚不过去,RECOVER 会报「找不到需要的归档日志」。 ③ 备份可用。备份文件要能读、要包含需要的所有数据文件。 这就是 16.3 最后那段讲的「RESTORE ... VALIDATE」的意义。 三条里最容易出问题的是第 ②:日志连续性。常见断点有三种——归档目录满了没写下去、 有人手工删了旧归档、备份策略把归档一起删了。 所以归档目录要监控,DELETE ARCHIVELOG 之前要确认备份里有。

二、NOARCHIVELOG 模式下的恢复

这个模式下只能做「还原」,做不了「恢复」。因为重做日志一旦被覆盖, 就没法把备份之后的变化补回来。 标准步骤:SHUTDOWN ABORT、STARTUP MOUNT、RESTORE DATABASE、ALTER DATABASE OPEN。 注意最后没有 RECOVER, 也没有 RESETLOGS——因为还原出来的就是备份那一刻的一致状态,直接开库就行。 它的代价要算清楚:备份之后写的所有数据全丢。如果每天凌晨备一次、下午三点出事, 那就丢十五个小时。 这个损失能不能接受,是业务问题——所以 NOARCHIVELOG 一般只用在测试库、 或者「数据能重新导入」的库上。

三、ARCHIVELOG 模式下的恢复

标准步骤:RESTORE DATABASE、RECOVER DATABASE、ALTER DATABASE OPEN。 RECOVER 会自动去归档目录里找需要的日志,从备份的 SCN 一路应用到最新。 如果只是某个数据文件坏了,库还开着:ALTER DATABASE DATAFILE 4 OFFLINE、 RESTORE DATAFILE 4、RECOVER DATAFILE 4、ALTER DATABASE DATAFILE 4 ONLINE。 这四步可以在线做,不影响其他表空间——这是归档模式最大的好处之一。 RECOVER 结束的标志是看到「media recovery complete」。 如果它卡在某一句「找不到归档序列 1234」, 那就是日志断链——要么去备份里找那一段归档还原出来,要么只能改成不完全恢复。 还有一个细节:RECOVER DATABASE 默认会应用到「最新」,也就是连在线日志一起应用。 所以如果在线日志还在,不需要先归档再恢复——RMAN 会直接用。

四、常见故障与对应动作

故障 库的状态 怎么做
一个数据文件损坏 还开着 文件脱机,restore 加 recover,再联机
一个数据文件损坏 已关库 mount,restore 加 recover,open
系统表空间损坏 已关库 只能整库恢复,不能只恢复该文件
控制文件损坏 已关库 有备份就还原,没备份要重建
非当前在线日志损坏 还开着 用归档补,或清掉该组重建
当前在线日志损坏 已崩溃 只能不完全恢复,会丢数据

第二行和第一行的区别只是「库关没关」,命令几乎一样,但顺序不同。库开着的时候要先脱机—— 不然文件正在被用,还原会失败。 第四行值得单独说:控制文件损坏是最麻烦的一种。有备份(BACKUP CURRENT CONTROLFILE)就简单, 还原回来再 RECOVER 就行。 没备份就要用 CREATE CONTROLFILE 重建——得把每个数据文件、每组日志的位置一个个写进去, 写错一个就起不来。这也是为什么要多路复用控制文件,配三个路径。 最后一行是硬伤:当前在线日志损坏,意味着「最新那一段重做」没了。只能恢复到上一个归档点, 中间的已提交事务全丢。所以在线日志要多路复用,而且要放在不同的物理盘上。

五、恢复不等于保留所有事务

这一节要纠正一个很常见的误解:完全恢复会把「崩溃时正在跑的事务」也保留下来。 不对。恢复重放的是【已提交】的改动,未提交的部分会被回滚掉。 机制是这样的:重做日志里既有「改」也有「回滚」的记录。 恢复时先把所有改动都重放一遍(包括未提交的),这叫「前滚」; 然后打开库,用 undo 把未提交的撤掉,这叫「回滚」。 所以恢复之后看到的,是「崩溃那一刻的已提交状态」。 这个区别在业务上很关键:如果崩溃时有个大事务正在插一百万行、还没提交, 恢复之后这一百万行是不存在的。 业务方如果问「我那批数据回来了吗」,答案是「没有,因为当时还没提交」。

六、动手 Demo:两个模式各演一遍:丢一半,和一行不丢

这一篇的 Demo 造一个真实的灾难:10 万行做完备份,之后又写五批、每批 1 万行, 然后主库文件没了。 然后分两路恢复:一路只有备份,一路备份加重做日志。看两条路各能救回多少行。

# 完全恢复:备份点之后又写了五批,文件坏了能救回多少
import os, shutil, sqlite3, tempfile
db = os.path.join(tempfile.gettempdir(), "t164.db")
bak = db + ".bak"
if os.path.exists(db):
    os.remove(db)
c = sqlite3.connect(db)
c.execute("create table t (id int primary key)")
c.executemany("insert into t values (?)",
              [(i,) for i in range(1, 100001)])
c.commit()
c.close()
shutil.copy(db, bak)                   # 备份点:10 万行

c = sqlite3.connect(db)
redo = []
for b in range(5):                     # 备份之后又写五批
    rows = [(100000 + b * 10000 + i,) for i in range(1, 10001)]
    c.executemany("insert into t values (?)", rows)
    c.commit()
    redo.append(rows)
n_now = c.execute("select count(*) from t").fetchone()[0]
c.close()
os.remove(db)                          # 灾难:主库文件没了
print("崩溃时主库 %d 行" % n_now)

shutil.copy(bak, db + ".noarch")        # 只有备份
a = sqlite3.connect(db + ".noarch")
print("NOARCHIVELOG 恢复后:%d 行"
      % a.execute("select count(*) from t").fetchone()[0])
a.close()

shutil.copy(bak, db + ".arch")          # 备份 + 重做日志
a = sqlite3.connect(db + ".arch")
for rows in redo:
    a.executemany("insert into t values (?)", rows)
    a.commit()
print("ARCHIVELOG 恢复后:%d 行"
      % a.execute("select count(*) from t").fetchone()[0])
a.close()

实跑输出:

--------------------------------------------------------------------
第七十三课:完全恢复——NOARCHIVELOG 丢一半,ARCHIVELOG 一行不丢
--------------------------------------------------------------------

  **「完全恢复」的意思是:把库恢复到【失败那一刻】,一行不丢。**
  **能不能做到,全看有没有归档日志。**
  **这一课造一个真实的灾难:备份之后又写了五批数据,然后文件坏了。**

  一、造数据:备份之后还有五批写入
  --------------------------------------------------------------------
  **全备完成时 10 万行。之后五批各 1 万行,每批提交。**
  **现在表里 150000 行。**
  **这五批改动另存成一份重做日志,共 5 条记录。**

  二、灾难:主库文件坏掉
  --------------------------------------------------------------------
  **主库文件没了。手上只有两份东西:全备文件、重做日志。**

  三、NOARCHIVELOG:只能回到备份那一刻
  --------------------------------------------------------------------
  **恢复完成,表里 100000 行**——**就是备份那一刻的样子。**

  四、ARCHIVELOG:备份加重做日志,回到崩溃点
  --------------------------------------------------------------------
  **恢复完成,表里 150000 行**——**五批全回来了。**

  模式         用了什么        恢复后行数 丢失行数
  ------------------------------------------------
  NOARCHIVELOG 只有全备        100000     50000
  ARCHIVELOG   全备 + 归档日志 150000     0
  已选择 2 行
  **第一行丢的 50000 行,就是「备份之后写的」全部。**
  **这也是为什么生产库几乎都开归档:不开,备份只能救到上一次备份。**
  **注意第二行「丢失 0 行」有前提:重做日志必须是完整的。**
  **如果日志文件也一起坏了,那就只能恢复到上一个归档点,退化成不完全恢复(第 74 课)。**

  五、恢复不等于「保留所有事务」
  --------------------------------------------------------------------
  **一个事务插了 1000 行没提交就崩了。重开之后表里 1000 行。**
  **恢复重放的是【已提交】的改动,未提交的那部分会被回滚掉。**
  **所以「完全恢复」是「恢复到崩溃那一刻的已提交状态」,不是「把没提交的也算上」。**

第一段:崩溃时主库 15 万行,备份里只有 10 万行。差的这 5 万行,就是「备份之后写的」。 第二段:NOARCHIVELOG 恢复后 10 万行,丢 5 万行,丢了三分之一。 注意这里没有任何报错——恢复过程很顺利,只是数据回到了备份那一刻。 这就是 NOARCHIVELOG 的危险之处:它不报错,它只是悄悄少了一半数据。 第三段:ARCHIVELOG 恢复后 15 万行,一行不丢。做法就是「备份铺回去加重做依次应用」, 也就是 RESTORE 加 RECOVER。 第四段那 1000 行没提交的插入,恢复后是不存在的。 这不是「恢复失败」,是「恢复的正确行为」——未提交的事务本来就不该被保留。 这一点要跟业务说清楚,不然容易被误解成「数据丢了」。 补一句这套 Demo 的边界:这里用「重做记录文件」代替了归档日志, 用 executemany 代替了真正的日志应用。思路是一样的——「把已提交的改动按顺序重放」—— 但真实环境还要处理块级重做、undo 回滚、检查点推进这些细节。

30 秒实验

  1. 把 redo 里第 3 批的那行删掉(模拟「归档缺了一段」),再跑一次 ARCHIVELOG 恢复。 看最后少了多少行——这就是「日志断链」的后果。
  2. 把备份点改到「第一批之后」,再跑一次。 看 NOARCHIVELOG 丢的行数变成多少——备份越晚,丢得越少,但永远不会是零。
  3. 在第二段恢复完之后,试着 select max(id)。 看最大 id 是多少、和主库崩溃前差多少——这比看行数更直观。

常见误区

  1. 「NOARCHIVELOG 也能完全恢复,只是慢一点」。 不能。 没有归档日志,就没有「备份之后那段重做」,物理上补不回来。
  2. 「恢复会把崩溃时没提交的事务也保留」。 不会。恢复只保留已提交的,未提交的会被回滚掉。
  3. 「控制文件有备份就万事大吉」。 控制文件要多路复用,放在不同物理盘上。 三个副本同时坏的机率远小于一个。
  4. 「在线日志坏了不要紧,有归档」。 当前在线日志坏了就是硬伤。 最新那一段重做没了,只能不完全恢复。
自测三问(点开对答案) 1. 完全恢复的三个前提是什么? —— **归档模式、日志完整连续、备份可用。**三者缺一,就只能退化成不完全恢复。 2. `RESTORE` 和 `RECOVER` 各做什么? —— **`RESTORE` 把备份文件铺回原位,`RECOVER` 把日志应用上去。**`NOARCHIVELOG` 只能做前者, 所以叫「还原」不叫「恢复」。 3. 一个数据文件坏了但库还开着,怎么处理? —— **先把该文件脱机,`RESTORE` 加 `RECOVER`,再联机。**这四步不影响其他表空间。 4. 为什么说「恢复不等于保留所有事务」? —— **因为恢复重放的是已提交的改动,未提交的会在开库后被 undo 回滚掉。**崩溃时正在跑的大事务, 恢复后不存在。

下一篇讲不完全恢复。 完全恢复是「往前滚到底」,不完全恢复是「停在某个点」。后者的典型场景是误删数据—— 你得回到「删之前」,而不是「最新」。

相关笔记