Py学习  »  DATABASE

MySQL 插入一条 SQL 语句,redo log 记录的是什么?

鸭哥聊Java • 昨天 • 21 次点击  

insert into order_pay ... 执行完,MySQL 崩了。

重启以后,这条数据还在不在?这个问题别急着看 binlog,先看 InnoDB 的 redo log。很多人一听 redo log,就以为里面记了一条 SQL:

insert into order_pay(order_no, user_id, amount, status)
values('P20260520001'100863990'INIT');

不对。

redo log 里基本不关心你写的 SQL 长什么样,它关心的是:哪个表空间、哪个数据页、哪个位置,被改成了什么样

我拿一段 Java 代码放一下现场,不写复杂,就一条支付单插入:

public class PayOrderWriter {

    private final DataSource dataSource;

    public PayOrderWriter(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void createPayOrder(String orderNo, long userId, int amount) throws Exception {
        String sql = """
                insert into order_pay(order_no, user_id, amount, status, created_at)
                values (?, ?, ?, ?, now())
                "
"";

        try (Connection conn = dataSource.getConnection()) {
            conn.setAutoCommit(false);

            try (PreparedStatement ps = conn.prepareStatement(sql)) {
                ps.setString(1, orderNo);
                ps.setLong(2, userId);
                ps.setInt( 3, amount);
                ps.setString(4"INIT");
                ps.executeUpdate();
            }

            conn.commit();
        }
    }
}

这段代码跑到 MySQL 里,不是直接把一行数据“写进磁盘文件”这么简单。

InnoDB 会先找到这张表的聚簇索引页。因为 InnoDB 的表数据本身就是按主键组织在 B+Tree 里的,所以插入一行,本质上是往某个索引页里塞一条记录。

如果这个页已经在 Buffer Pool 里,就直接改内存页。

如果不在,就先从磁盘读进 Buffer Pool,再改。

改完以后,这个页变成了脏页。脏页什么时候刷盘,不一定马上刷。这里要是每插一条都立刻刷数据页,性能基本就别看了。

所以 InnoDB 先写 redo log。

这地方我以前也见过不少误解:以为 redo log 是“备份 SQL”,崩了以后重新执行 SQL。不是。redo log 更像一份页修改记录。

大概可以理解成这样:

space_id = 58
page_no  = 12347
lsn      = 928377120
type     = MLOG_REC_INSERT
offset   = 312
payload  = 新插入记录的紧凑格式内容

这不是 MySQL 原样打印出来的日志,只是把意思摊开。

真正的 redo record 会更细,它会记录这次对数据页的修改,包含页号、日志类型、偏移、必要的数据内容。崩溃恢复时,InnoDB 根据这些 redo record,把对应的数据页重新“修”到崩溃前已经提交或应该保留的状态。

注意这里有两个点。

第一个,redo log 记录的是 InnoDB 存储引擎层的东西,不是 Server 层 SQL。

Server 层解析 SQL,优化,生成执行计划,然后调用 InnoDB 插入记录。到了 InnoDB 这边,它看到的已经不是“用户写了一条 insert”,而是“我要改某个 B+Tree 页”。

第二个,插入一行不一定只产生一条 redo。

比如这张表长这样:

create table order_pay (
    id bigint primary key auto_increment,
    order_no varchar(64not null,
    user_id bigint not null,
    amount int not null,
    status varchar(16not null,
    created_at datetime not null,
    unique key  uk_order_no(order_no),
    key idx_user_id(user_id)
engine=InnoDB;

你插入一条记录,至少会影响聚簇索引。

如果有唯一索引 uk_order_no,也要插入对应的二级索引记录。

如果有普通索引 idx_user_id,也要维护这个索引页。

运气好,几个目标页都有空间,那就是在几个页里追加或插入记录。

运气不好,页满了,发生 page split,那 redo 就更多了。不是只记录“插入一行”,还要记录页分裂、新页分配、页目录调整、链表指针变化等一堆东西。

所以看到一条简单 insert,不要脑补 redo log 也很简单。SQL 看着一行,底层可能改了好几个页。

再看一个更贴近排查的点。

线上如果有人说:

insert 很慢,是不是 redo log 写太多?

我一般不会马上信。我会先看表结构。

show create table order_pay;
show index from order_pay;

如果一张表上挂了七八个二级索引,那每插一行都要维护七八棵 B+Tree。redo 多只是结果,不是根。

还要看参数:

show variables like 'innodb_flush_log_at_trx_commit';
show variables like 'sync_binlog';

innodb_flush_log_at_trx_commit=1 时,每次事务提交都要把 redo log 刷到磁盘,安全性最好,性能压力也最明显。

这也是为什么有些系统单条 insert 压不高,不一定是 SQL 写得丑,可能是提交太碎。

比如这种代码,我第一眼就不太喜欢:

for (PayLine line : lines) {
    writer.createPayOrder(line.orderNo(), line.userId(), line.amount());
}

每一条都单独开事务、提交事务。业务量小没事,量一上来,redo flush、binlog sync、网络往返全都在放大。

我一般会改成批量提交,至少别让每条记录都自己扛一次提交成本:

public void createPayOrders(List lines) throws Exception {
    String sql = """
            insert into order_pay(order_no, user_id, amount, status, created_at)
            values (?, ?, ?, 'INIT', now())
            "
"";

    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql)) {

        conn.setAutoCommit(false);

        int n = 0;
        for (PayLine line : lines) {
            ps.setString(1, line.orderNo());
            ps.setLong(2, line.userId());
            ps.setInt(3, line.amount());
            ps.addBatch();

            if (++n % 500 == 0) {
                ps.executeBatch();
            }
        }

        ps.executeBatch();
        conn.commit();
    }
}

这段代码不是为了省 redo log。该记录的页修改还是要记录。

它省的是事务提交次数,减少频繁刷 redo 的压力。

最后把这事收一下。

MySQL 执行一条 insert,redo log 记录的不是 SQL 原文,也不是一整行“逻辑数据”。它记录的是 InnoDB 数据页被修改后的必要信息,保证崩溃以后能把页恢复回来。

binlog 偏逻辑,给主从复制、数据恢复用。

redo log 偏物理,给 InnoDB 崩溃恢复用。

一条 insert 背后,可能改聚簇索引页,可能改多个二级索引页,可能触发页分裂,还可能涉及 undo 页、事务系统页这些配套修改。

所以问“redo log 记录了什么”,别盯着 SQL 看。

要往下看一层:它记的是页怎么变了。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/197722