Skip to content

Redo Log - 重做日志详解 ​

定义 ​

Redo Log (重做日志) 是InnoDB存储引擎的一种物理日志,记录数据页被修改后的物理变化。当事务提交或日志缓冲区刷新时,Redo Log会被写入磁盘文件(ib_logfile0和ib_logfile1),确保即使发生系统崩溃,已提交的事务也能在恢复时被重做(Redo),从而保证事务的持久性 (Durability)。

核心特征 ​

特征说明
日志类型物理日志(Physical Log)
记录内容数据页的物理修改(如"在偏移量X处写入Y字节")
日志格式MLOG_1BYTE、MLOG_2BYTES、MLOG_4BYTES等
文件大小固定大小,循环使用(由innodb_log_file_size控制)
文件数量默认2个(ib_logfile0, ib_logfile1)
写入时机事务提交、Log Buffer满、每秒刷新、检查点
刷盘策略由innodb_flush_log_at_trx_commit参数控制

与WAL的关系 ​

WAL (Write-Ahead Logging) - 预写日志机制(理论/原则)
  ↓ 具体实现
Redo Log - 重做日志(InnoDB的实现方式)
  ↓ 包含
├── 日志缓冲区(Log Buffer) - 内存中的日志
└── 日志文件组(Log Files) - 磁盘上的日志文件

WAL是理论框架,规定"先写日志后写数据"的原则;Redo Log是具体实现,是WAL机制在InnoDB中的物理载体。

为什么需要Redo Log? ​

问题1: 随机IO性能差 ​

如果每次修改都直接写入数据页(Data Page):

  • 数据页分散在磁盘不同位置 → 随机IO
  • 每次修改都需要fsync()刷盘 → 性能极低
  • 假设TPS=1000,则需要1000次随机IO/s

解决方案:

  • 将随机IO转换为顺序IO
  • 先写Redo Log(顺序追加) → 性能提升10-100倍
  • 后台异步刷脏页(Checkpoint机制)

问题2: 崩溃恢复 ​

如果修改数据页过程中发生崩溃:

  • 数据页可能只写了一半 → 页断裂 (Page Tear)
  • 无法区分哪些修改已提交,哪些未提交
  • 数据一致性被破坏

解决方案:

  • Redo Log采用**追加写 (Append-Only)**方式,原子性强
  • 崩溃后通过Redo Log重做已提交事务
  • 通过Undo Log回滚未提交事务

性能对比 ​

sql
-- 测试场景: 插入10万条记录

-- 方案1: 直接修改数据页(禁用Redo Log - 仅用于对比)
SET GLOBAL innodb_flush_log_at_trx_commit = 0;
INSERT INTO test_table SELECT * FROM generate_series(1, 100000);
-- 耗时: ~30秒 (随机IO)

-- 方案2: 使用Redo Log(正常配置)
SET GLOBAL innodb_flush_log_at_trx_commit = 1;
INSERT INTO test_table SELECT * FROM generate_series(1, 100000);
-- 耗时: ~5秒 (顺序IO)

-- 性能提升: 6倍

Redo Log的物理结构 ​

日志文件组布局 ​

ib_logfile0 (默认5MB~512MB)    ib_logfile1 (默认5MB~512MB)
┌─────────────────────┐     ┌─────────────────────┐
│ LOG_BLOCK_HDR (12B) │     │ LOG_BLOCK_HDR (12B) │
│ Redo Record #1  │     │ Redo Record #N+1  │
│ Redo Record #2  │     │ Redo Record #N+2  │
│ ...       │     │ ...       │
│ LOG_BLOCK_TRAILER │     │ LOG_BLOCK_TRAILER │
└─────────────────────┘     └─────────────────────┘
   ↑            ↑
   └──── 循环写入(Log Group) ──────────┘

关键概念:

  • Log Group: 一组Redo Log文件,InnoDB将其视为一个连续的环形缓冲区
  • Log Block: 每个日志文件被分成512字节的块(与磁盘扇区对齐)
  • LSN (Log Sequence Number): 全局递增的日志序列号,标识日志位置

Log Block结构 ​

c
/* storage/innobase/include/log0log.h */
#define LOG_BLOCK_HDR_SIZE  12  /* 块头大小 */
#define LOG_BLOCK_TRL_SIZE  8 /* 块尾大小 */
#define LOG_BLOCK_DATA_SIZE 512 /* 块总大小 */
#define LOG_BLOCK_MAX_REC_SIZE (LOG_BLOCK_DATA_SIZE - LOG_BLOCK_HDR_SIZE - LOG_BLOCK_TRL_SIZE)

/* Log Block布局 */
struct log_block_t {
  /* Header (12 bytes) */
  uint32_t block_no;  /* 块编号 */
  uint16_t data_len;  /* 数据长度 */
  uint16_t first_rec_group; /* 第一个记录组偏移 */
  uint16_t checkpoint_no; /* 检查点编号 */
  
  /* Data (492 bytes) */
  byte data[LOG_BLOCK_DATA_SIZE - LOG_BLOCK_HDR_SIZE - LOG_BLOCK_TRL_SIZE];
  
  /* Trailer (8 bytes) */
  uint32_t checksum;  /* 校验和 */
};

Redo Record格式 ​

每条Redo Record记录一次物理修改:

c
/* Redo Record通用格式 */
struct redo_record_t {
  uint16_t total_len;   /* 记录总长度 */
  uint8_t  type;    /* 记录类型(MLOG_*) */
  uint8_t  space;   /* 表空间ID */
  uint32_t page_no;   /* 页号 */
  uint16_t offset;    /* 修改偏移量 */
  uint16_t len;     /* 修改长度 */
  byte   data[];    /* 修改的数据内容 */
};

/* 常见记录类型 */
enum mlog_type_t {
  MLOG_1BYTE = 1,   /* 修改1字节 */
  MLOG_2BYTES = 2,  /* 修改2字节 */
  MLOG_4BYTES = 3,  /* 修改4字节 */
  MLOG_8BYTES = 4,  /* 修改8字节 */
  MLOG_REC_INSERT = 5, /* 记录插入 */
  MLOG_REC_CLUST_DELETE_MARK = 6, /* 聚集索引删除标记 */
  MLOG_LIST_END_DELETE = 7, /* 链表删除结束 */
  MLOG_PAGE_CREATE = 11, /* 页创建 */
  MLOG_COMP_REC_INSERT = 55, /* 压缩记录插入 */
  // ... 共50+种类型
};

InnoDB源码分析 ​

Redo Log写入流程 ​

cpp
/* storage/innobase/log/log0log.cc */

/**
 * 将Mini-Transaction的修改写入Redo Log
 * @param mtr Mini-Transaction对象
 */
void mtr_commit(mtr_t* mtr)
{
  /* 1. 检查是否已提交 */
  if (mtr->is_committed()) {
    return;
  }
  
  /* 2. 获取MTR中的所有日志块 */
  mtr_buf_t* log = mtr->get_log();
  ulint len = log->len();
  
  if (len == 0) {
    mtr->set_state(MTR_STATE_COMMITTED);
    return;
  }
  
  /* 3. 预留日志空间 */
  mutex_enter(&log_sys->mutex);
  lsn_t start_lsn = log_sys->lsn;
  lsn_t end_lsn = start_lsn + len;
  
  /* 4. 检查是否需要切换日志块 */
  if (log_sys->buf_free % LOG_BLOCK_DATA_SIZE + len > LOG_BLOCK_MAX_REC_SIZE) {
    /* 填充当前块的剩余空间 */
    ulint pad_len = LOG_BLOCK_DATA_SIZE - (log_sys->buf_free % LOG_BLOCK_DATA_SIZE);
    memset(log_sys->buf + log_sys->buf_free, 0, pad_len);
    log_sys->buf_free += pad_len;
    log_sys->lsn += pad_len;
    
    /* 更新块头信息 */
    log_block_set_data_len(
    log_sys->buf + (log_sys->buf_free - LOG_BLOCK_DATA_SIZE),
    LOG_BLOCK_DATA_SIZE);
  }
  
  /* 5. 复制日志到Log Buffer */
  memcpy(log_sys->buf + log_sys->buf_free, log->data(), len);
  log_sys->buf_free += len;
  log_sys->lsn += len;
  
  /* 6. 设置最后一个块的长度 */
  ulint last_block_offset = log_sys->buf_free % LOG_BLOCK_DATA_SIZE;
  if (last_block_offset > 0) {
    log_block_set_data_len(
    log_sys->buf + (log_sys->buf_free - last_block_offset),
    last_block_offset);
  }
  
  /* 7. 根据配置决定是否立即刷盘 */
  bool flush_to_disk = false;
  if (srv_flush_log_at_trx_commit == 1) {
    flush_to_disk = true;
  } else if (srv_flush_log_at_trx_commit == 2 && 
     my_timer_since(mtr->start_timer) >= 1000000) {
    /* 每秒刷新一次 */
    flush_to_disk = true;
  }
  
  mutex_exit(&log_sys->mutex);
  
  /* 8. 刷盘(如果需要) */
  if (flush_to_disk) {
    log_buffer_flush_to_disk();
  }
  
  /* 9. 标记MTR为已提交 */
  mtr->set_state(MTR_STATE_COMMITTED);
}

Redo Log刷盘策略 ​

cpp
/* storage/innobase/log/log0log.cc */

/**
 * 将Log Buffer刷新到磁盘
 */
void log_buffer_flush_to_disk()
{
  mutex_assert_owner(&log_sys->mutex);
  
  /* 1. 获取需要刷新的LSN范围 */
  lsn_t flushed_to_disk_lsn = log_sys->flushed_to_disk_lsn;
  lsn_t current_lsn = log_sys->lsn;
  
  if (current_lsn <= flushed_to_disk_lsn) {
    return; /* 已经全部刷新 */
  }
  
  /* 2. 计算需要写入的数据量 */
  ulint len = static_cast<ulint>(current_lsn - flushed_to_disk_lsn);
  
  /* 3. 确定写入位置(处理环形缓冲区回绕) */
  ulint from_offset = static_cast<ulint>(flushed_to_disk_lsn % log_sys->size);
  ulint to_offset = from_offset + len;
  
  /* 4. 执行写入操作 */
  if (to_offset <= log_sys->size) {
    /* 不需要回绕,一次性写入 */
    os_file_write(
    log_sys->files[0],
    log_sys->buf + from_offset,
    from_offset,
    len);
  } else {
    /* 需要回绕,分两次写入 */
    ulint first_part = log_sys->size - from_offset;
    ulint second_part = to_offset - log_sys->size;
    
    /* 写入第一部分 */
    os_file_write(
    log_sys->files[0],
    log_sys->buf + from_offset,
    from_offset,
    first_part);
    
    /* 写入第二部分(回到文件开头) */
    os_file_write(
    log_sys->files[1],
    log_sys->buf,
    0,
    second_part);
  }
  
  /* 5. 调用fsync确保数据落盘 */
  for (ulint i = 0; i < log_sys->n_files; i++) {
    os_file_flush(log_sys->files[i]);
  }
  
  /* 6. 更新刷新位置 */
  log_sys->flushed_to_disk_lsn = current_lsn;
}

崩溃恢复重做流程 ​

cpp
/* storage/innobase/log/log0recv.cc */

/**
 * 崩溃恢复时的Redo阶段
 * @param start_lsn 恢复起始LSN
 * @param end_lsn 恢复结束LSN
 */
void recv_recover_page(
  buf_block_t* block,
  page_t* page)
{
  lsn_t page_lsn = page_get_max_lsn(page);
  lsn_t current_lsn = log_sys->lsn;
  
  /* 1. 检查页面是否需要恢复 */
  if (page_lsn >= current_lsn) {
    return; /* 页面已是最新 */
  }
  
  /* 2. 从Redo Log中读取需要重做的记录 */
  recv_group_t* group = recv_sys->groups;
  recv_addr_t* recv_addr;
  
  HASH_SEARCH(addr_hash, group->addr_hash,
      calc_hash(block->space, block->page_no),
      recv_addr,
      recv_addr->space == block->space &&
      recv_addr->page_no == block->page_no);
  
  if (recv_addr == NULL) {
    return; /* 没有该页面的重做记录 */
  }
  
  /* 3. 按LSN顺序应用所有Redo Record */
  recv_op_t* op;
  UT_LIST_BASE_NODE_CHECK(recv_op_t, recv_addr->recovered_ops, op) {
    if (op->lsn <= page_lsn) {
    continue; /* 已应用的记录跳过 */
    }
    
    /* 4. 根据操作类型应用重做 */
    switch (op->type) {
    case RECV_MLOG_1BYTE:
    page_write_byte(page, op->offset, op->data[0]);
    break;
    
    case RECV_MLOG_2BYTES:
    page_write_ui16(page, op->offset, mach_read_from_2(op->data));
    break;
    
    case RECV_MLOG_4BYTES:
    page_write_ui32(page, op->offset, mach_read_from_4(op->data));
    break;
    
    case RECV_REC_INSERT:
    page_cur_insert_rec_low(page, op->offset, op->data, op->len);
    break;
    
    case RECV_PAGE_CREATE:
    page_create_low(page, op->data);
    break;
    
    default:
    ut_error;
    }
    
    page_set_max_lsn(page, op->lsn);
  }
  
  /* 5. 标记页面为脏页 */
  buf_block_set_dirty(block, TRUE);
}

关键配置参数 ​

innodb_log_file_size ​

ini
# 单个Redo Log文件大小(单位:MB)
# 范围: 4MB ~ 512GB (MySQL 8.0+)
# 默认: 48MB (MySQL 5.7), 2048MB (MySQL 8.0+)

[mysqld]
innodb_log_file_size = 2048M

影响:

  • 越大越好? 不一定。太大会延长恢复时间
  • 太小问题: 频繁触发检查点,增加IO负担
  • 推荐值:
    • 写密集型应用: 2GB~4GB
    • 普通应用: 512MB~1GB
    • 测试环境: 48MB~128MB

计算公式:

推荐大小 = (每小时最大写入量 × 保留时间) / 日志文件数量

例如:
- 每小时写入: 10GB
- 希望保留: 1小时
- 文件数量: 2
- 单个文件大小: 10GB / 2 = 5GB

innodb_log_files_in_group ​

ini
# Redo Log文件数量
# 范围: 2 ~ 100
# 默认: 2

[mysqld]
innodb_log_files_in_group = 2

影响:

  • InnoDB以循环方式写入日志文件
  • 文件越多,单个文件越小(总大小固定)
  • 推荐保持默认值2

innodb_flush_log_at_trx_commit ​

ini
# Redo Log刷盘策略
# 0: 每秒刷新并刷盘
# 1: 每次事务提交都刷新并刷盘(默认,最安全)
# 2: 每次事务提交都刷新,但每秒才刷盘

[mysqld]
innodb_flush_log_at_trx_commit = 1

三种模式对比:

值刷新策略刷盘策略性能安全性适用场景
0每秒1次每秒1次最高最低(丢失1秒数据)日志、缓存等非关键数据
1每次提交每次提交最低最高(不丢失数据)金融、订单等关键数据
2每次提交每秒1次中等中等(丢失1秒数据)折中方案

性能测试:

sql
-- 测试脚本
CREATE TABLE test_redo (
  id INT AUTO_INCREMENT PRIMARY KEY,
  data VARCHAR(255)
) ENGINE=InnoDB;

-- 模式0: 约10,000 TPS
SET GLOBAL innodb_flush_log_at_trx_commit = 0;
INSERT INTO test_redo (data) VALUES (REPEAT('a', 255)); -- 重复执行

-- 模式1: 约1,000 TPS  
SET GLOBAL innodb_flush_log_at_trx_commit = 1;
INSERT INTO test_redo (data) VALUES (REPEAT('a', 255)); -- 重复执行

-- 模式2: 约5,000 TPS
SET GLOBAL innodb_flush_log_at_trx_commit = 2;
INSERT INTO test_redo (data) VALUES (REPEAT('a', 255)); -- 重复执行

innodb_log_buffer_size ​

ini
# Log Buffer大小(单位:MB)
# 范围: 1MB ~ 16GB
# 默认: 16MB

[mysqld]
innodb_log_buffer_size = 64M

影响:

  • 越大: 能容纳更多大事务,减少刷新次数
  • 太小: 大事务频繁刷新日志,性能下降
  • 推荐值:
    • 有大事务(BLOB/TEXT): 64MB~256MB
    • 普通应用: 16MB~32MB
    • 小事务为主: 8MB~16MB

Redo Log工作流程 ​

完整的事务提交流程 ​

sql
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

底层执行步骤:

Step 1: 修改数据页(内存)
┌─────────────────────────────────┐
│ Buffer Pool       │
│           │
│  Page #100 (accounts.id=1)  │
│  balance: 1000 → 900    │ ← 直接在内存中修改
│           │
│  Page #200 (accounts.id=2)  │
│  balance: 500 → 600     │ ← 直接在内存中修改
└─────────────────────────────────┘

Step 2: 生成Redo Record(内存)
┌─────────────────────────────────┐
│ Log Buffer        │
│           │
│  Redo Record #1:      │
│  type=MLOG_4BYTES     │
│  space=1, page_no=100     │
│  offset=50, data=0x00000384   │
│           │
│  Redo Record #2:      │
│  type=MLOG_4BYTES     │
│  space=1, page_no=200     │
│  offset=50, data=0x00000258   │
└─────────────────────────────────┘

Step 3: COMMIT时写入Redo Log(磁盘)
┌─────────────────────────────────┐
│ ib_logfile0       │
│           │
│  LSN 1000: MLOG_4BYTES ...  │ ← 持久化
│  LSN 1001: MLOG_4BYTES ...  │ ← 持久化
│  LSN 1002: MLOG_COMMIT ...  │ ← 标记提交
└─────────────────────────────────┘

Step 4: 返回客户端成功
COMMIT OK!

Step 5: 后台异步刷脏页(Checkpoint)
Buffer Pool中的脏页 → 数据文件(ibd)

崩溃恢复场景 ​

场景: 事务提交后,数据页还未刷盘,系统崩溃

时间线:
T1: 事务修改Page #100(内存)
T2: 生成Redo Record(内存)
T3: COMMIT, Redo Record写入ib_logfile0(磁盘) ✓
T4: 返回客户端成功 ✓
T5: 系统崩溃! 💥 (Buffer Pool中的数据丢失)

恢复过程:
┌──────────────────────────────────────────┐
│ MySQL重启           │
└──────────────────────────────────────────┘
   ↓
┌──────────────────────────────────────────┐
│ InnoDB自动启动崩溃恢复       │
│ 1. 读取ib_logfile0和ib_logfile1   │
│ 2. 扫描所有Redo Record      │
│ 3. 找到LSN最大的记录       │
└──────────────────────────────────────────┘
   ↓
┌──────────────────────────────────────────┐
│ Redo阶段(重做)         │
│ FOR 每个Redo Record:       │
│ IF 记录的LSN > 数据页的LSN THEN   │
│   应用修改到数据页        │
│ END IF           │
│ END FOR            │
│              │
│ 结果: Page #100恢复到balance=900   │
└──────────────────────────────────────────┘
   ↓
┌──────────────────────────────────────────┐
│ Undo阶段(回滚)         │
│ FOR 每个未提交事务:         │
│ 使用Undo Log回滚修改        │
│ END FOR            │
└──────────────────────────────────────────┘
   ↓
┌──────────────────────────────────────────┐
│ 数据库恢复到一致状态 ✓        │
└──────────────────────────────────────────┘

实际案例分析 ​

案例1: Redo Log满导致性能下降 ​

问题现象:

[Warning] InnoDB: Log file ./ib_logfile0 is full
[Warning] InnoDB: Checkpoint age: 8500000

原因分析:

  1. innodb_log_file_size设置过小(48MB)
  2. 大批量INSERT操作产生大量Redo Log
  3. Checkpoint速度跟不上日志生成速度
  4. InnoDB被迫等待Checkpoint完成

解决方案:

ini
[mysqld]
# 调整前
innodb_log_file_size = 48M
innodb_log_buffer_size = 16M

# 调整后
innodb_log_file_size = 2048M
innodb_log_buffer_size = 64M
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000

验证效果:

sql
-- 监控Redo Log使用情况
SHOW ENGINE INNODB STATUS\G

-- 查看输出:
Log sequence number    1234567890
Log buffer assigned up to  1234500000
Log buffer flushed up to   1234400000
Pending log writes     0

-- 计算使用率
使用率 = (1234567890 - 1234400000) / (2048 * 1024 * 1024) ≈ 0.008%

案例2: 批量导入优化 ​

场景: 导入1亿条记录

优化前(耗时2小时):

sql
-- 使用默认配置
SET GLOBAL innodb_flush_log_at_trx_commit = 1;

LOAD DATA INFILE '/data/huge_file.csv'
INTO TABLE huge_table
FIELDS TERMINATED BY ',';

优化后(耗时15分钟):

sql
-- 1. 临时调整配置
SET GLOBAL innodb_flush_log_at_trx_commit = 2;
SET GLOBAL innodb_log_buffer_size = 256 * 1024 * 1024; -- 256MB
SET GLOBAL innodb_log_file_size = 4096 * 1024 * 1024;  -- 4GB

-- 2. 禁用自动提交
SET AUTOCOMMIT = 0;

-- 3. 分批导入
LOAD DATA INFILE '/data/huge_file.csv'
INTO TABLE huge_table
FIELDS TERMINATED BY ','
ROWS PER BATCH = 100000;

-- 4. 手动提交
COMMIT;

-- 5. 恢复配置
SET GLOBAL innodb_flush_log_at_trx_commit = 1;
SET GLOBAL innodb_log_buffer_size = 16 * 1024 * 1024;

性能对比:

指标优化前优化后提升
总耗时7200秒900秒8倍
TPS13,888111,1118倍
Redo Log写入50GB50GB-
fsync次数1亿次900次11万倍

案例3: 崩溃恢复实战 ​

故障场景:

2026-04-10 14:30:00 [ERROR] mysqld got signal 11
2026-04-10 14:30:00 [ERROR] Stack trace:
...
2026-04-10 14:30:05 [Note] Starting crash recovery...

恢复过程:

bash
# 1. 查看错误日志
tail -f /var/log/mysql/error.log

# 输出:
InnoDB: Doing recovery: scanned up to log sequence number 1234567890
InnoDB: Doing recovery: scanned up to log sequence number 1234600000
InnoDB: 1 transaction(s) which must be rolled back or cleaned up
InnoDB: Starting in background the rollback of uncommitted transactions

# 2. 监控恢复进度
SHOW ENGINE INNODB STATUS\G

# 输出:
--- LOG ---
Log sequence number    1234600000
Log flushed up to    1234600000
Pages flushed up to    1234500000
Last checkpoint at     1234500000

# 3. 恢复完成
2026-04-10 14:35:00 [Note] InnoDB: Starting final batch to recover pages
2026-04-10 14:35:30 [Note] InnoDB: Last binlog file './mysql-bin.000123'
2026-04-10 14:35:30 [Note] InnoDB: Rollback of non-committed transactions completed
2026-04-10 14:35:30 [Note] InnoDB: Shutdown completed; log sequence number 1234600000
2026-04-10 14:35:31 [Note] mysqld: ready for connections.

恢复耗时分析:

  • Redo阶段: 3分钟(扫描500MB日志)
  • Undo阶段: 2分钟(回滚1个大事务)
  • 总计: 5分钟

优化建议:

ini
[mysqld]
# 加快恢复速度
innodb_log_file_size = 2048M    # 增大日志文件
innodb_log_buffer_size = 64M    # 增大日志缓冲
innodb_flush_log_at_trx_commit = 1  # 保证数据安全
innodb_io_capacity = 2000     # 提高IO能力
innodb_io_capacity_max = 4000

Redo Log vs Undo Log ​

对比项Redo LogUndo Log
作用重做已提交事务回滚未提交事务
日志类型物理日志逻辑日志
记录内容"页X偏移Y处写入Z""将记录A改为B"
保证特性持久性(Durability)原子性(Atomicity)
文件格式ib_logfile0, ib_logfile1ibdata1(共享表空间)
写入时机事务提交时数据修改时
删除时机Checkpoint完成后事务提交后(延迟清理)
MVCC支持不支持支持(版本链)
崩溃恢复Redo阶段使用Undo阶段使用

最佳实践 ​

1. 配置优化 ​

ini
[mysqld]
# 生产环境推荐配置
innodb_log_file_size = 2048M      # 根据写入量调整
innodb_log_files_in_group = 2     # 保持默认
innodb_log_buffer_size = 64M      # 有大事务时增大
innodb_flush_log_at_trx_commit = 1    # 关键业务用1,非关键用2
innodb_io_capacity = 2000       # SSD可调更高
innodb_io_capacity_max = 4000
innodb_max_dirty_pages_pct = 75     # 控制脏页比例

2. 监控告警 ​

sql
-- 监控Redo Log生成速度
SELECT 
  VARIABLE_NAME,
  VARIABLE_VALUE,
  (VARIABLE_VALUE - @prev_value) AS diff_per_second
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_os_log_written'
INTO @name, @value, @diff;

SET @prev_value = @value;

-- 告警规则
-- 如果每秒Redo Log生成 > 100MB, 发送告警

-- 监控检查点年龄
SHOW ENGINE INNODB STATUS\G
-- 检查 "Checkpoint age" 值
-- 如果接近 innodb_log_file_size * innodb_log_files_in_group, 需要告警

3. 批量操作优化 ​

python
import mysql.connector
from contextlib import contextmanager

@contextmanager
def batch_import_mode(connection):
  """批量导入优化模式"""
  cursor = connection.cursor()
  
  try:
    # 1. 临时调整配置
    cursor.execute("SET SESSION innodb_flush_log_at_trx_commit = 2")
    cursor.execute("SET SESSION autocommit = 0")
    
    yield cursor
    
    # 2. 手动提交
    connection.commit()
    
  except Exception as e:
    # 3. 异常回滚
    connection.rollback()
    raise e
    
  finally:
    # 4. 恢复配置
    cursor.execute("SET SESSION innodb_flush_log_at_trx_commit = DEFAULT")
    cursor.execute("SET SESSION autocommit = DEFAULT")
    cursor.close()

# 使用示例
connection = mysql.connector.connect(...)

with batch_import_mode(connection) as cursor:
  for i in range(1000000):
    cursor.execute(
    "INSERT INTO large_table (col1, col2) VALUES (%s, %s)",
    (i, f"data_{i}")
    )
    # 每10000条提交一次
    if i % 10000 == 0:
    connection.commit()

4. 故障排查 ​

bash
# 1. 检查Redo Log是否成为瓶颈
mysqladmin extended-status | grep Innodb_os_log_written

# 2. 检查fsync频率
iostat -x 1 | grep sda
# 关注 await 和 svctm 指标

# 3. 查看InnoDB状态
mysql -e "SHOW ENGINE INNODB STATUS\G" | grep -A 10 "LOG"

# 4. 监控检查点进度
mysql -e "SHOW ENGINE INNODB STATUS\G" | grep "Checkpoint age"

参考资料 ​

MySQL官方文档 ​

源码文件 ​

  • storage/innobase/log/log0log.cc - Redo Log核心实现
  • storage/innobase/log/log0recv.cc - 崩溃恢复实现
  • storage/innobase/include/log0log.h - 日志系统头文件
  • storage/innobase/mtr/mtr0mtr.cc - Mini-Transaction实现

相关术语 ​

  • WAL - 预写日志机制
  • 检查点 - Checkpoint机制
  • [Undo Log](./Undo Log.md) - 回滚日志
  • 双写缓冲 - Doublewrite Buffer

技术博客 ​

  • Jeremy Cole: InnoDB transaction history
  • Facebook MySQL Blog: InnoDB redo log tuning
  • Percona: Understanding InnoDB log files

版本历史:

  • 2026-04-12: 初始版本,详尽讲解Redo Log原理与实践

Released under MIT License.