🏺 第7章 管理控制文件和日志文件
📄 一句话概括

重做日志记的是「改了什么」,不是「改成了什么」。 所以重放它才不依赖当时的数据状态——这也是它能用来做崩溃恢复的原因。 它按「组」组织,组内可以有多个「成员」(物理副本)。 至少两组才能轮转,成员放不同盘才算真备份。 三个状态要认清:CURRENT、ACTIVE、INACTIVE。

📝 本章和前面几章的关系

7.1 节说控制文件记着「日志文件在哪」。这一篇讲那些日志文件本身。 它的管理动作不多,但每一条都有红线:不能删 CURRENT 组、不能只剩一组、改名要按顺序。 这一篇和下一篇是连着的:日志切换时旧日志是扔掉还是归档,决定了你的库能恢复到哪一秒。

一、重做日志文件概述:组、成员、三个状态

组(group)是逻辑单位,成员(member)是物理文件。 一个组可以有一个或多个成员,它们内容完全一样—— 写的时候 LGWR 会把同一份内容写到当前组的所有成员。

状态 含义 能不能被覆盖
CURRENT LGWR 正在写这一组 不能
ACTIVE 切换下来了,但检查点还没做完 还不能
INACTIVE 检查点做完了 可以重用
UNUSED 从没写过 可以

这三个状态是一条流水线:CURRENT → ACTIVE → INACTIVE。 切换只是「换一组写」,脏块还没刷到数据文件,所以旧组仍是 ACTIVE。 等检查点把脏块全刷完,它才变 INACTIVE,这时才可以被覆盖。 一条实用判断:如果 ACTIVE 的组很多、而且迟迟不退,说明检查点跟不上。 表现是日志切换变慢、整库间歇性停顿,告警日志里会看到 checkpoint not complete。 根因通常是「日志组太少/太小」或者「磁盘写得太慢」。

二、增加日志组及其成员

加组: ALTER DATABASE ADD LOGFILE GROUP 4 ('/u01/redo04a.log', '/u02/redo04b.log') SIZE 50M; 一条语句同时建组和成员。 给已有的组加成员:ALTER DATABASE ADD LOGFILE MEMBER '/u03/redo01c.log' TO GROUP 1; 注意只写路径,不写 SIZE——成员大小跟着组走,不能单独设。 组数多少合适?看「一组写满要多久」。 业务高峰一小时写满三次,说明组太小或太少。 经验值是「至少能扛住 20 分钟」,这样即使归档卡住,也有缓冲时间。 成员数建议 2~3 个,且必须放在不同物理盘。 成员越多写越慢(每份都要写一遍),所以不是越多越好——两三个成员、分开放盘, 是性价比最高的配法。

三、删除重做日志文件

删组:ALTER DATABASE DROP LOGFILE GROUP 4; 删成员:ALTER DATABASE DROP LOGFILE MEMBER '/u03/redo01c.log'; 四条红线,撞上就报错: ① 不能删 CURRENT 组(报 ORA-01623); ② 不能删 ACTIVE 组(报 ORA-01624,得等检查点做完); ③ 不能删到只剩一组(至少要两组才能轮转);④ 不能删最后一个成员(组里至少留一个文件)。 还有一个坑:DROP LOGFILE GROUP 只改数据库里的登记,不删物理文件。 所以删完要去操作系统里把文件也删掉,不然磁盘上留着一堆没人管的 redo04a.log。 这一点和 DROP TABLESPACE 不一样——那个带 INCLUDING DATAFILES 才会删文件。

四、更改重做日志文件的位置或名称

不能直接 mv。 因为控制文件里还记着旧路径,文件挪了,Oracle 就找不到成员。 正确顺序三步,一步都不能错: ① 操作系统层面把文件拷到新位置(旧文件先别删); ② ALTER DATABASE RENAME FILE '旧' TO '新';;③ 确认能读之后再删旧文件。 第二步必须在 mount 状态执行,open 状态不行。 因为它改的是控制文件里的记录,而控制文件在 open 之后被锁住,不许这种结构性修改。 所以完整流程是:停库 → startup mount → rename → alter database open。 顺序反了会怎样?先删旧文件再登记,那一刻 Oracle 找不到成员, 写日志直接报 ORA-00313(打不开组成员)或 ORA-00312(打不开日志文件)。 如果是 CURRENT 组,库会挂起——没有地方记新的重做记录了。

五、查看重做日志信息:三张视图

v$log 看组:组号、序列号、状态、成员数、字节数。 v$logfile 看成员:每个组有哪些文件、在哪个路径。 v$log_history 看历史:已经切换过的日志,每组一行,带 FIRST_CHANGE#。 日常巡检看三件事: ① 有没有长期停在 ACTIVE 的组(检查点在拖); ② 每天切换多少次(v$log_history 按天 count);③ 有没有组的成员数是 1(等于没做多路复用)。 v$log_history 里那个 FIRST_CHANGE# 是 SCN。 它连着下一篇的归档:归档文件也是按 SCN 区间组织的, 恢复时 Oracle 就是拿这些区间去拼「日志链」。 链上缺一段,恢复就停在那儿。

六、动手 Demo:用文件加一张表演一遍日志轮转:切换、加组、删组

沙箱里没有 Oracle,ALTER DATABASE ADD LOGFILE 执行不了。 但「组、序列号、三个状态、切换」这套机制,用一个字典加几个文件就能演清楚。 我建了三组、每组两个成员文件,手动切换四次,看序列号和状态怎么变, 再试着删 CURRENT 组——看它怎么被拦住。

groups = {1: 2, 2: 2, 3: 2}          # 组号 -> 成员数
seq = {1: 1, 2: 0, 3: 0}
status = {1: 'CURRENT', 2: 'UNUSED', 3: 'UNUSED'}

def show():
    for g in sorted(groups):
        print('  group %d  seq=%-2d %-8s members=%d'
              % (g, seq[g], status[g], groups[g]))

def switch():
    u"""切一次日志:当前组退成 INACTIVE,下一组接任并涨序号。"""
    cur = [g for g in groups if status[g] == 'CURRENT'][0]
    nxt = cur % len(groups) + 1
    seq[nxt] = seq[cur] + 1
    status[cur] = 'INACTIVE'
    status[nxt] = 'CURRENT'
    return cur, nxt

def drop_group(g):
    if status[g] == 'CURRENT':
        raise RuntimeError('ORA-01623: cannot drop current log')
    if len(groups) <= 2:
        raise RuntimeError('至少要留两组才能轮转')
    del groups[g], seq[g], status[g]

show()
for k in range(4):
    cur, nxt = switch()
    print('切换 %d: group %d -> group %d, seq=%d' % (k + 1, cur, nxt, seq[nxt]))
show()

try:
    drop_group(2)
except RuntimeError as e:
    print('删 CURRENT 组:', e)

switch()
drop_group(2)
print('删完之后剩', len(groups), '组')
show()

实跑输出:

--------------------------------------------------------------------
第三十一课:重做日志——一组一组成员,轮着写
--------------------------------------------------------------------

  **重做日志(redo log)记的是「改了什么」,不是「改成了什么」。**
  **一句 `update emp set sal = sal * 1.1`,日志里记的是「某行某个块,加 10%」**——
  **这样重放时才不依赖当时的数据状态。**
  **它一定是「组」的形式存在的:至少两组,组内可以有多个成员。**
  **组是「逻辑上的一组日志」,成员是「同一个组的几份物理副本」。**
  **写的时候 LGWR 会写当前组的所有成员,写满就切换下一组。**

  先建三组,每组两个成员
  --------------------------------------------------------------------

  SQL> select grp as 组号, seq as 序列, status as 状态, members as 成员数, bytes as 字节 from v_log order by grp
  组号 序列 状态    成员数 字节
  -----------------------------
  1    1    CURRENT 2      92
  2    0    UNUSED  2      0
  3    0    UNUSED  2      0
  已选择 3 行
  **现在组 1 是 `CURRENT`,另外两组还是 `UNUSED`。**
  **`CURRENT` 的意思就是「LGWR 正在往这组写」。**

  切换:CURRENT → ACTIVE → INACTIVE
  --------------------------------------------------------------------
  **第 1 次切换:组 1 从 `CURRENT` 退下来,组 2 变成 `CURRENT`,序列涨到 2。**
  **第 2 次切换:组 2 从 `CURRENT` 退下来,组 3 变成 `CURRENT`,序列涨到 3。**
  **第 3 次切换:组 3 从 `CURRENT` 退下来,组 1 变成 `CURRENT`,序列涨到 4。**
  **第 4 次切换:组 1 从 `CURRENT` 退下来,组 2 变成 `CURRENT`,序列涨到 5。**

  SQL> select grp as 组号, seq as 序列, status as 状态 from v_log order by grp
  组号 序列 状态
  ------------------
  1    4    INACTIVE
  2    5    CURRENT
  3    3    INACTIVE
  已选择 3 行
  **组 2 是 `CURRENT`,组 1 和组 3 都退成了 `INACTIVE`,三组的序列号都涨过了。**
  **三个状态的含义是一条流水线:**
  **`CURRENT` 正在写;切换之后变 `ACTIVE`(还在等检查点把脏块刷到数据文件);检查点做完变 `INACTIVE`(可以覆盖重用了)。**
  **这个过程叫「日志切换」,触发条件有三个:**
  **① 当前组写满;② 手工 `ALTER SYSTEM SWITCH LOGFILE`;③ 归档模式下归档写完。**
  **切换本身很快,但如果检查点跟不上,库会「卡在切换上」**——
  **表现是整库停住几秒,日志里能看到 `checkpoint not complete`。**

  加一组、删一组
  --------------------------------------------------------------------
  **加组:`ALTER DATABASE ADD LOGFILE GROUP 4 (...) SIZE 50M;`**
  **一个组至少一个成员,建议两个以上,且放在不同盘上。**

  SQL> select grp as 组号, seq as 序列, status as 状态, members as 成员数 from v_log order by grp
  组号 序列 状态     成员数
  -------------------------
  1    4    INACTIVE 2
  2    5    CURRENT  2
  3    3    INACTIVE 2
  4    0    UNUSED   2
  已选择 4 行
  **四组了。**  **组数多少合适?看「一组写满要多久」——**
  **写满太快说明组太小或太少,切换太频繁;**  **切换太频繁会让检查点追不上,最后就是性能掉。**
  **删组有两条红线:**  **① 不能删 `CURRENT` 组**(正在写,删了等于把笔抽走);
  **② 不能删到只剩一组**(至少要两组才能轮转)。
  **现在想删组 2,会报 `ORA-01623`(cannot drop current log)。**
  **先切一次,让它退下来,再删:**

  SQL> select grp as 组号, seq as 序列, status as 状态 from v_log order by grp
  组号 序列 状态
  ------------------
  1    4    INACTIVE
  3    6    CURRENT
  4    0    UNUSED
  已选择 3 行
  **组 2 删掉了,剩下 3 组。**

  改名换位置:先拷、再登记、最后删旧
  --------------------------------------------------------------------
  **想把日志文件挪到另一块盘,不能直接 `mv`。**
  **因为 Oracle 的控制文件里还记着旧路径,挪了文件它就找不到。**
  **正确顺序是三步,而且顺序不能乱:**
  **① 用操作系统命令把文件拷到新位置**(`cp`/`copy`),**旧文件先别删。**
  **② 在 SQL 里登记新路径:`ALTER DATABASE RENAME FILE '旧' TO '新';`**

  [Oracle 原码 · 本沙箱不执行]
  | ALTER DATABASE RENAME FILE
  |   '/u01/oradata/ORCL/redo01a.log'
  |   TO '/u02/oradata/ORCL/redo01a.log';
    必须在 mount 状态执行,open 状态不行。

  **③ 确认新路径能读,再删旧文件。**  **现在我把它删了,`v$logfile` 里已经指向新路径:**

  SQL> select grp as 组号, member as 成员 from v_logfile order by grp, member
  组号 成员
  ----------------------
  1    disk2/redo01a.log
  1    redo01b.log
  3    redo03a.log
  3    redo03b.log
  4    redo04a.log
  4    redo04b.log
  已选择 6 行
  **顺序反了会怎样?先删旧文件再登记,那一刻 Oracle 找不到成员文件,写日志直接报 `ORA-00313`/`ORA-00312`。**  **这两个码一个是「打不开组成员」,一个是「打不开日志文件」**,  **经常一起出现,看到就要去核 `v$logfile` 里的路径。**

  组成员全丢:还能撑一会儿,但撑不久
  --------------------------------------------------------------------
  **一个组两个成员,坏掉一个不影响写入**——LGWR 写另一个就行,
  **只在告警日志里留一条 `ORA-00312`。**  **两个都坏,这一组就废了。**
  **如果废掉的是 `CURRENT` 组,库会挂起**,  **因为再没有地方能记新的重做记录了。**
  **这就是「多路复用」的全部意义:**
  **不是为了提高性能(反而更慢,要写两份),是为了「一块盘坏了不停机」。**
  **所以成员一定要放在不同物理盘上**——  **放同一块盘上,盘一坏两份一起没,多路复用等于没做。**

切换四次之后,组 2 是 CURRENT、序列号 5;组 1 和组 3 都是 INACTIVE。 序列号是全局递增的,不是每组各数各的—— 这一点很关键:序列号连续,才能判断「日志链有没有断」。 注意组 3 的序列号是 3、组 1 是 4、组 2 是 5。 按组号看是乱的,按时间看是连续的。 恢复时 Oracle 就是按序列号排,不看组号。 删 CURRENT 组被拦住了,报 ORA-01623。 这是我在模型里显式写的一条规则——真实 Oracle 也是这个码,因为「正在写的组不能删」是硬道理。 先切一次再删,就成功了。 这也解释了运维里的一个套路:想删某个组,先 ALTER SYSTEM SWITCH LOGFILE 把它换下来, 等它变成 INACTIVE 再删。 模型里没演的一件事:真实删组之后,物理文件还在磁盘上。 DROP LOGFILE GROUP 只删登记,不删文件,要自己去操作系统里清。 这是新手最容易留下垃圾的地方。

30 秒实验

  1. 把 switch() 里的 status[cur] = 'INACTIVE' 改成 'ACTIVE',重跑。 你会发现 ACTIVE 的组越来越多—— 这就是「检查点跟不上」的表现,真实环境里下一步就是整库卡顿。
  2. 把 groups 改成四组,重跑。切换的循环变长,但序列号还是连续递增。 组多了,切回同一组的时间就长,检查点有更多时间追上。
  3. 把 drop_group 里「至少留两组」那句删掉,连续删到只剩一组,再 switch()。 会算出 nxt = 1,也就是「切到自己」——这就是为什么必须有至少两组:一组的库没法轮转。

常见误区

  1. 以为 DROP LOGFILE GROUP 会连文件一起删。 不会。 物理文件要自己去操作系统里清,不然磁盘上留一堆垃圾。
  2. 直接删 CURRENT 组。 报 ORA-01623。先切一次让它退下来,变成 INACTIVE 再删。
  3. 所有成员放在同一块盘上。 盘坏全没。多路复用必须跨物理盘,同盘等于没做。
  4. 改名时先删旧文件再 RENAME。 中间那一刻 Oracle 找不到成员, 写日志报 ORA-00313/ORA-00312,CURRENT 组的话库直接挂起。
自测三问(点开对答案) 1. `CURRENT`、`ACTIVE`、`INACTIVE` 三个状态怎么流转? —— **`CURRENT`(正在写)→ 切换后 `ACTIVE`(等检查点)→ 检查点做完 `INACTIVE`(可重用)。** **大量组卡在 `ACTIVE` 说明检查点跟不上。** 2. 为什么至少要两组日志? —— **因为要轮转:一组写满换另一组,同时把写满的那组归档或重用。** **只剩一组,就没有「换」的地方了。** 3. 改日志文件位置的三步是什么?为什么不能直接 `mv`? —— **拷到新位置 → `ALTER DATABASE RENAME FILE` → 删旧文件。** **控制文件里记着旧路径,直接 `mv` 之后 Oracle 找不到成员。** **而且 `RENAME` 必须在 mount 状态做。**

下一篇讲归档日志: 日志切换时旧日志是扔掉还是另存,决定了你的库能恢复到「上次全备」还是「故障前最后一秒」。

相关笔记