三种备份:完整(全库)、差异(上次完整之后的改动)、日志(上次日志备份之后的事务)。 还原必须按「完整 → 差异 → 日志」的顺序接;顺序错了差异会把完整覆盖掉,而且不报错。 备份放同一台机器等于没备份:「备份成功」和「能恢复」是两件不同的事,演练过才算数。
17.1 讲威胁、17.2 讲第三层防线,这一节讲最后一道保险:数据真没了怎么办。
它也是全书唯一一节「讲怎么把数据找回来」的内容。
教材 17.3 分三小节:数据备份、数据恢复、备份策略,
「小结」和「实践与练习」并进本篇末尾。
沙箱没有 SQL Server 的备份接口,所以演示是用文件拷贝 + 重放做的模型:
完整备份 = 拷一份文件、差异备份 = 再拷一次、日志 = 记下变更再重放。
复刻的是「备份链」的流程语义,BACKUP DATABASE 原码照贴不执行。
一、三种备份,各管一段
| 类型 | 备份什么 | 多大 | 恢复时要什么 |
|---|---|---|---|
| 完整备份 | 整个库 | 最大 | 自己就够了 |
| 差异备份 | 上次「完整备份」之后的改动 | 中等 | 最近一次完整 + 最近一次差异 |
| 事务日志备份 | 上次「日志备份」之后的所有事务 | 最小 | 完整 + 差异 + 一串日志(按顺序) |
关键在于「差异」的基准是「完整备份」,不是「上次差异」。 所以每天的差异备份会越来越大(离完整备份越来越远); 而日志备份只装「上次日志备份之后」的部分,所以小而频繁。 这决定了策略: 完整每周一次、差异每天一次、日志每 15 分钟一次—— 这样最多丢 15 分钟的数据,恢复时最多还原「一周的全量 + 一天的差异 + 一串日志」。 为什么不全用完整备份? 因为大。 一个几百 GB 的库,每天全量备份既慢又占空间, 而差异和日志只装变化量——这是「用恢复的复杂度换备份的成本」。
二、真跑一条备份链
| 备份文件 | 里面有几行 | 什么时候做的 |
|---|---|---|
full.bak |
1 | 最开始 |
diff.bak |
3 | 插了 2 行之后 |
| 日志(1 条插入) | — | 又插 1 行之后 |
这条链很朴素: 先插 1 行、做完整备份;再插 2 行、做差异备份;
再插 1 行、做日志备份。此刻库里有 4 行。
注意 diff.bak 里是 3 行而不是 2 行——因为差异备份是「对着完整备份」比出来的,
它包含了完整备份里那 1 行 + 后来的 2 行,所以「差异能单独叠加在完整之上」。
这也是差异和日志的核心区别:
差异是「快照式的差」,可以只还原最近一个;日志是「顺序的记录」,必须一条条重放。
三、恢复:必须按顺序接起来
| 怎么恢复 | 最终几行 | 说明 |
|---|---|---|
| 只还原完整备份 | 1 | 回到最开始 |
| 完整 + 差异 | 3 | 回到差异那一刻 |
| 完整 + 差异 + 日志 | 4 | 回到最后一笔事务 |
这三行就是「备份链」的意思: 想回到最新状态, 必须「完整 → 差异 → 日志」按顺序接,一个都不能跳。 顺序错了会怎样? 先还原差异再还原完整, 差异直接被完整覆盖掉——结果比不做还糟,而且不报错。 这类错误最危险的地方就是「不报错」:你以为恢复了,其实回到了更早的状态。 还有一个必须记住的细节: 差异备份和日志备份都依赖「它之前的那份完整备份」。 如果那份完整备份的文件坏了、丢了,后面所有备份全废。 所以「多留几份完整备份」不是浪费,是唯一能救命的冗余。
四、RPO 和 RTO:两个指标决定策略
| 指标 | 问的是什么 | 由谁决定 |
|---|---|---|
| RPO(能丢多少) | 出事时最多能丢多长时间的数据 | 日志备份的频率 |
| RTO(多久恢复) | 从出事到能用要多久 | 备份的大小 + 恢复演练 |
RPO 全靠日志备份频率: 每 15 分钟一次日志备份,最多丢 15 分钟的数据; 想只丢 1 分钟,就得每分钟备一次——代价是日志文件和维护量。 RTO 全靠演练: 备份文件再完整,没人试过恢复,就等于没有备份。 「备份成功」和「能恢复」是两件事——这是运维里最贵的教训之一。 备份脚本天天报成功、真出事时还原报错,这种事发生过太多次。 原因往往是:备份文件确实生成了,但没人验证过它完整。
五、教材给的 SQL Server 写法
-- 完整备份
BACKUP DATABASE sales TO DISK = 'D:\bak\sales_full.bak' WITH INIT;
-- 差异备份
BACKUP DATABASE sales TO DISK = 'D:\bak\sales_diff.bak' WITH DIFFERENTIAL;
-- 日志备份
BACKUP LOG sales TO DISK = 'D:\bak\sales_log.trn';
-- 还原(注意顺序和 NORECOVERY)
RESTORE DATABASE sales FROM DISK = 'D:\bak\sales_full.bak' WITH NORECOVERY;
RESTORE DATABASE sales FROM DISK = 'D:\bak\sales_diff.bak' WITH NORECOVERY;
RESTORE LOG sales FROM DISK = 'D:\bak\sales_log.trn' WITH RECOVERY;
GO
那串 NORECOVERY 是关键: 它的意思是「还没完,别急着打开这个库」。
最后一个才用 RECOVERY,那时数据库才变成可用状态。
漏了 NORECOVERY 会怎样? 第一个还原完数据库就「上线」了,
后面两个还原直接报错——而且这时候库是半旧的,业务已经在往上写了。
这是最容易造成「二次事故」的操作顺序错误。
还有一句: 日志备份的前提是库是「完整恢复模式」。
如果是「简单恢复模式」,日志会被自动截断,你根本备不出日志——
所以「能不能做日志备份」取决于库的模式,不是取决于你会不会写语句。
六、备份的三条铁律
| 铁律 | 为什么 |
|---|---|
| 异地(或异机)留一份 | 勒索软件第一件事就是加密同机的备份 |
| 定期验证能恢复 | 「备份成功」不等于「能还原」 |
| 演练过才算数 | 没练过的流程,真出事时一定会卡住 |
第一条最反直觉: 备份放在同一台机器上, 等于没备份——硬盘坏了、被加密了、被误删了,业务数据和备份一起没。 第三条最容易被敷衍。 「演练」不是「看一眼备份文件在不在」, 是「拿真备份、在别的机器上、掐着表还原一遍,然后让业务跑一遍」。 只有这样的演练才能发现真问题: 备份文件是不是缺了几个、 还原脚本里的路径对不对、磁盘空间够不够、要多久——这些全是计划里没有的细节。
七、动手 Demo:备份链:从 1 行到 4 行,三次恢复
这一段搭一条最小备份链:完整备份 → 改数据 → 差异备份 → 再改 → 日志备份, 然后做三次恢复,看每次能回到哪个状态。 沙箱里「备份」就是拷文件、「日志」就是重放那一条插入—— 形式简化了,但「按顺序接」这个规矩一点没变。
import shutil, sqlite3
c = sqlite3.connect(u"bak.db", isolation_level=None)
c.execute(u"CREATE TABLE tb_order (id INTEGER PRIMARY KEY AUTOINCREMENT, total REAL)")
c.execute(u"INSERT INTO tb_order (total) VALUES (100)")
shutil.copyfile(u"bak.db", u"full.bak") # 完整备份:1 行
c.execute(u"INSERT INTO tb_order (total) VALUES (200)")
c.execute(u"INSERT INTO tb_order (total) VALUES (300)")
shutil.copyfile(u"bak.db", u"diff.bak") # 差异备份:3 行
c.execute(u"INSERT INTO tb_order (total) VALUES (400)") # 这一条进日志
# 恢复一:只还原完整
shutil.copyfile(u"full.bak", u"r1.db")
print(count(u"r1.db")) # 1
# 恢复二:完整 + 差异
shutil.copyfile(u"diff.bak", u"r2.db")
print(count(u"r2.db")) # 3
# 恢复三:完整 + 差异 + 重放日志
shutil.copyfile(u"diff.bak", u"r3.db")
sqlite3.connect(u"r3.db").execute(u"INSERT INTO tb_order (total) VALUES (400)")
print(count(u"r3.db")) # 4
实跑输出:
--------------------------------------------------------------------
备份与恢复:备份链是怎么接起来的
--------------------------------------------------------------------
① 三种备份,各管一段
--------------------------------------------------------------------
| 类型 | 备份什么 | 多大 | 恢复时要什么 |
|---|---|---|---|
| 完整备份 | 整个库 | 最大 | 自己就够了 |
| 差异备份 | 上次「完整备份」之后的改动 | 中等 | 最近一次完整 + 最近一次差异 |
| 事务日志备份 | 上次「日志备份」之后的所有事务 | 最小 | 完整 + 差异 + 一串日志(按顺序) |
关键在于「差异」的基准是「完整备份」,不是「上次差异」。
所以每天的差异备份会越来越大(离完整备份越来越远),而日志备份只装「上次日志备份之后」的部分,
所以小而频繁。
这决定了策略: 完整每周一次、差异每天一次、日志每 15 分钟一次——这样最多丢 15 分钟的数据,
恢复时最多还原「一周的全量 + 一天的差异 + 一串日志」。
② 真跑一条备份链
--------------------------------------------------------------------
第 0 步:建库、插 1 行,做一次完整备份(拷一份文件,等价于 BACKUP DATABASE)。
之后插了 2 行,做一次差异备份。此时库里有 3 行。
再插 1 行,做一次日志备份(记下这 1 行的插入)。此时库里有 4 行。
| 备份文件 | 里面有几行 | 什么时候做的 |
|---|---:|---|
| full.bak | 1 | 最开始 |
| diff.bak | 3 | 插了 2 行之后 |
| log(1 条插入) | — | 又插 1 行之后 |
③ 恢复:必须按顺序接起来
--------------------------------------------------------------------
| 怎么恢复 | 最终几行 | 说明 |
|---|---:|---|
| 只还原完整备份 | 1 | 回到最开始 |
| 完整 + 差异 | 3 | 回到差异那一刻 |
| 完整 + 差异 + 日志 | 4 | 回到最后一笔事务 |
这三行就是「备份链」的意思: 想回到「最新的状态」,就必须「完整 → 差异 → 日志」按顺序接,
一个都不能跳。
顺序错了会怎样? 先还原差异再还原完整,差异直接把完整覆盖掉——结果比不做还糟,而且不报错。
还有一个必须记住的细节: 差异备份和日志备份都依赖「它之前的那份完整备份」。
如果那份完整备份的文件坏了、丢了,后面所有备份全废。所以「多留几份完整备份」不是浪费。
④ RPO 和 RTO:两个指标决定策略
--------------------------------------------------------------------
| 指标 | 问的是什么 | 由谁决定 |
|---|---|---|
| RPO(能丢多少) | 出事时最多能丢多长时间的数据 | 日志备份的频率 |
| RTO(多久恢复) | 从出事到能用要多久 | 备份的大小 + 恢复演练 |
RPO 全靠日志备份频率: 每 15 分钟一次日志备份,最多丢 15 分钟的数据;
想只丢 1 分钟,就得每分钟备一次——代价是日志文件和维护量。
RTO 全靠演练: 备份文件再完整,没人试过恢复,就等于没有备份。
「备份成功」和「能恢复」是两件事——这是运维里最贵的教训之一。
所以第三条铁律是「定期做恢复演练」: 拿真实备份、在别的机器上、掐着表还原一遍。
演练过的备份才算备份。
⑤ 教材给的 SQL Server 写法
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| -- 完整备份
| BACKUP DATABASE sales TO DISK = 'D:\bak\sales_full.bak' WITH INIT;
| -- 差异备份
| BACKUP DATABASE sales TO DISK = 'D:\bak\sales_diff.bak' WITH DIFFERENTIAL;
| -- 日志备份
| BACKUP LOG sales TO DISK = 'D:\bak\sales_log.trn';
| -- 还原(注意顺序和 NORECOVERY)
| RESTORE DATABASE sales FROM DISK = 'D:\bak\sales_full.bak' WITH NORECOVERY;
| RESTORE DATABASE sales FROM DISK = 'D:\bak\sales_diff.bak' WITH NORECOVERY;
| RESTORE LOG sales FROM DISK = 'D:\bak\sales_log.trn' WITH RECOVERY;
| GO
那串 NORECOVERY 是关键: 它的意思是「还没完,别急着打开这个库」。
最后一个才用 RECOVERY,那时数据库才变成可用状态。
漏了 NORECOVERY 会怎样?
第一个还原完数据库就「上线」了,后面两个还原直接报错——而且这时候库是半旧的,业务已经在往上写了。
⑥ 备份的三条铁律
--------------------------------------------------------------------
| 铁律 | 为什么 |
|---|---|
| 异地(或异机)留一份 | 勒索软件第一件事就是加密同机的备份 |
| 定期验证能恢复 | 「备份成功」不等于「能还原」 |
| 演练过才算数 | 没练过的流程,真出事时一定会卡住 |
第一条最反直觉: 备份放在同一台机器上,等于没备份——硬盘坏了、被加密了、被误删了,
业务数据和备份一起没。
1 → 3 → 4 这三个数就是备份链的形状。 每多接一段,就能多回到一段时间。
而每一段都以前一段为前提——这就是「链」。
diff.bak 里是 3 行、不是 2 行,这一点容易看错。
差异备份是「对着完整备份」比出来的,所以它本身就包含了完整的底子,
能直接覆盖在完整备份上——这也是它不能和日志互换的原因。
日志那段在模型里就是「重放一条 INSERT」。
真数据库的日志里记的是「哪个页、从什么改成什么」,按顺序重放就行——
所以日志必须连着、不能跳号,跳了就接不上。
注意「恢复三」必须先铺差异、再重放日志。 反过来做,
日志重放出来的那行会被差异备份覆盖掉——4 又变回 3,而且不报错。
30 秒实验
- 把「恢复三」的顺序倒过来(先重放日志、再铺差异),看最终行数。 会变成 3——这就是「顺序错了比不做还糟」的现场。
- 把日志那段的重放删掉,看「恢复三」变成几行。 还是 3——说明「有没有日志」决定了你能不能拿回最后一笔事务。
- 想一想:一个电商库,你能接受「丢 15 分钟订单」吗? 多数业务能接受,所以 15 分钟一次日志备份就够; 但如果是「支付流水」,丢 15 分钟就是灾难——那种就得做到「几乎实时」。 RPO 是业务决定的,不是 DBA 决定的。
常见误区
- 「备份成功就是有备份了」。 备份文件可能不完整、可能缺日志、可能还原脚本是错的。 没验证过的备份,只能算「一个希望」。
- 「差异备份和日志备份可以互换」。 不能。 差异是「对着完整备份的快照差」,只还原最近一个就行; 日志是「顺序的事务记录」,必须一条条重放。
- 「备份放在同一台机器上,省事」。 勒索软件第一件事就是找同机的备份一起加密。 异地一份是最低要求。
自测三问(点开对答案)
1. 完整、差异、日志三种备份,还原时怎么组合? —— **完整 → 差异 → 日志,按顺序接。** 想回到最新状态就三段都要;**只还原完整就回到最开始。** 2. 为什么还原时前几个要写 `NORECOVERY`? —— **因为「还没完」。** 写了它数据库不会上线,可以继续叠加后面的备份; **最后一步才用 `RECOVERY` 让库可用。** **漏了就会在中间就把库打开,后面全报错。** 3. RPO 由什么决定? —— **由日志备份的频率决定。** 15 分钟备一次日志,最多丢 15 分钟; **想要更小就得备得更勤**——**而「能接受丢多少」是业务定的,不是技术定的。**第 17 章到此结束, 基础知识、核心技术、高级应用三篇(1~17 章)全部读完。下一篇进第 18 章 18.1,开始项目实战: 从酒店管理的业务话里抠出实体、画第一版表结构。
- ☐ (本篇完,下一篇 18.1 需求分析)