MySQL二进制日志binlog核心知识点小结

136 阅读8分钟

写在文章开头

本文将从MySQL二进制日志bin.log基本概念、工作机制和一些常见的运用场景角度展开探讨,希望对你有帮助。

我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:github.com/shark-ctrl/…

详解MySQL中的binlog

binlog的概念和基本作用

bin log实际上是一个物理日志,当我们对某个数据页进行修改操作时我们就会将这个操作写到bin log中,当我们数据库需要进行主备、主从复制等操作时,都可以基于bin log保证数据一致性。

binlog缓冲区

bin log缓冲区和我们的redo logundo log缓冲区有所不同,redo logundo log缓存都在存储引擎的共享缓冲区缓冲区buffer pool中,而bin log则是为每个工作线程独立分配一个内存作为bin log缓冲区:

bin log之所以是在每个线程中,原因有二:

  1. 考虑到mysql-server要求所有存储引擎对于bin.log都是兼容。
  2. bin_log_buffer设置在每个线程/事务中还能保证并发操作的性能,避免为各个线程争抢临界缓冲区的冲突导致并发性能下降。

binlog对应的3种记录格式

row:这种格式主要用于保证数据实时性的,例如我们执行下面这段SQL

update table set time=now() where id=1;

如果我们将其存到bin log之后很长一段时间才提交事务,那么时间就会有所延迟,所以MySQL为了保证数据实时性,就会将写入bin log中的SQLrow格式,如下图所示,可以看到row格式的SQL语句时间是当前时间的具体值,并且where条件写死了当前条件列,确保数据实时一致性:

当然这样做的缺点也很明显,如果涉及大批量操作,那么针对每条数据对应的都会生成对应的row语句,那么对于内存的占用就很高,进行恢复和同步时的IO和SQL执行时间也是非常不友好的。

stament:这种同步策略即执行的SQL是什么,对应传输过去的时对应的语句就是什么样的,这就会导致我们上文所说的一致性问题:

mixed:这种格式就是为了上述两种方案的混合体,如果操作可能出现数据不一致问题则用row格式,反之使用stament格式。

binlog文件日志格式

我们可以通过下面这条SQL语句看到我们本地的bin log文件:

show binary logs;

输出结果如下所示,可以看到bin log的格式基本都是mysql-bin.0000xxx:

mysql-bin.001606 440052 No
mysql-bin.001607 111520 No

binlog是如何完成写入

当我们开始事务时,将修改写入bin log cache中,一旦事务提交,就会将bin log通过write写入到文件系统缓存的page cache中,然后根据我们配置的刷盘参数将cache内容调用操作系统内核方法fsync将结果写入到bin log 物理文件中:

而调用系统函数fsync的实际是根据MySQL系统参数决定的,这个系统变量查询SQL如下

SHOW VARIABLES LIKE 'sync_binlog';

sync_binlog值分别三种:

0.当配置为了0时,每次事务提交都只会write,fsync调用时机是由系统决定的。 1.当配置设置为1时,每次事务提交都会调用fsync。 N. 当配置为N,代表提交了N个事务之后就会将page cache中的数据通过fsync进行刷盘。

binlog和redolog的区别

这个问题我们可以从以下几个场景来表述一下:

从使用场景来说:

  1. bin log常用于数据灾备或数据同步到其他异构程序中的场景。 2. redo log常用于故障恢复保证数据持久性。

从数据内容来说: 1. redo log存储的物理日志,即修改的数据内容,对应的redo block结构体针对各种偏移量和修改涉及的页都有及其复杂的涉及,这里就不多做赘述。 2. bin log则是记录可以是statment语句也可以是原生修改的row,具体可以通过查看binlog_format知晓。

生成范围:

  1. bin logMySQL server生成的事务日志,任何存储引擎都可以使用
  2. redo log只有innodb这个存储引擎支持。

(实践)基于flink cdc订阅binlog同步数据

接下来我们就基于spring boot演示一下如何基于flink cdc订阅bin.log完成db库中的tb_1tb_2的数据订阅和同步:

之所以笔者使用flink cdc而不是canel大体有以下几个原因:

  1. flink cdc支持全量和增量同步以及断点续传等功能,尤其是断点续传这一点对于需要保证异构数据库的数据一致性是非常好的。
  2. 性能表现更出色,按照阿里云的说法:

我们将全增量一体化框架与 Debezium 1.6 版本做 简单的 TPC-DS 读取测试对比,customer 单表数据量 6500 万,在 Flink CDC 用 8 个并发的情况下,吞吐提升了 6.8 倍,耗时仅 13 分钟,得益于并发读取的支持,如果用户需要更快的读取速度,用户可以增加并发实现。

话不说我们给出基础的集成步骤,首先是引入flink cdc和MySQL的依赖,这里笔者为了文章的简练只给出的flink cdc相关的pom依赖:

 <properties>
        <flink.version>1.13.6</flink.version>
    </properties>


<dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_2.12</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-java</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-java_2.12</artifactId>
            <version>${flink.version}</version>
        </dependency>
        <!--mysql -cdc-->
        <dependency>
            <groupId>com.ververica</groupId>
            <artifactId>flink-connector-mysql-cdc</artifactId>
            <version>2.0.0</version>
        </dependency>

然后我们在yml或者properties文件中给出MySQL配置即可,然后我们声明一个CdcInfo记录从bin.log中同步的数据:

@Data
public class CdcInfo {
    /**
     * 变更前数据
     */
    private JSONObject beforeData;
    /**
     * 变更后数据
     */
    private JSONObject afterData;

    private String operation;
    /**
     * binlog 文件名
     */
    private String binLogName;
    /**
     * binlog当前读取点位
     */
    private Integer filePos;
    /**
     * 数据库名
     */
    private String dbName;
    /**
     * 表名
     */
    private String tbName;
    /**
     * 变更时间
     */
    private Long changeTime;

}

然后我们编写一个关于bin.log通知事件的监听,针对flink cdc配置笔者都基于CommandLineRunner 这个拓展点完成配置,这里面涉及众多的flink cdc配置参数,可以看到笔者的程序同步模式配置的是initial即启动后会进行全量同步再进行增量同步,同时通过表达式db.tb_[1-2]+指明仅仅处理tb_1和tb_2表的数据更新变化。

@Component
public class MysqlCdcEventListener implements CommandLineRunner {

    //数据接收器用于应用架构更改和将更改数据写入外部系统
    private final CdcSink cdcSink;

    public MysqlCdcEventListener(CdcSink cdcSink) {
        this.cdcSink = cdcSink;
    }


    @Override
    public void run(String... args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        //设置并行度
        env.setParallelism(Runtime.getRuntime().availableProcessors());


        DebeziumSourceFunction<CdcInfo> debeziumSource = buildDebeziumSource();


        DataStream<CdcInfo> streamSource = env
                .addSource(debeziumSource, "mysql-source")
                .setParallelism(1);
        //将流数据交给
        streamSource.addSink(cdcSink);
        env.execute("mysql-stream-cdc");
    }


    /**
     * 构造变更数据源
     */
    private DebeziumSourceFunction<CdcInfo> buildDebeziumSource() {

        Properties debeziumProperties = new Properties();
        //设置快照为无锁
        debeziumProperties.put("snapshot.locking.mode", "none");

        return MySqlSource.<CdcInfo>builder()
                .hostname("xxxx")
                .port(3306)
                .databaseList("db")
                //监听db库中的[1-2]表
                .tableList("db.tb_[1-2]+")
                .username("xxxx")
                .password("xxxx")
                //设置为 initial:在第一次启动时对受监视的数据库表执行初始快照,并继续读取最新的 binlog
                .startupOptions(StartupOptions.initial())
                //设置序列化配置
                .deserializer(new MysqlDeserialization())
                .serverTimeZone("GMT+8")
                .debeziumProperties(debeziumProperties)
                .build();
    }


}

关于这些配置的信息建议读者移步官方文档的说明:nightlies.apache.org/flink/flink…

相应的使用配置示例读者也可以参考flink cdc对应的GitHub上的说明:github.com/gunnarmorli…

以及一些配置参数上的建议,也建议读者参考这篇文章:blog.csdn.net/weixin_4375…

上文代码示例中给出一个涉及反序列化生产CdcInfo的操作,笔者指明了MysqlDeserialization 这里也给出对应的源码示例:

public class MysqlDeserialization implements DebeziumDeserializationSchema<CdcInfo> {

    public static final String TS_MS = "ts_ms";
    public static final String BIN_FILE = "file";
    public static final String POS = "pos";
    public static final String CREATE = "CREATE";
    public static final String BEFORE = "before";
    public static final String AFTER = "after";
    public static final String SOURCE = "source";
    public static final String UPDATE = "UPDATE";


    @Override
    public void deserialize(SourceRecord sourceRecord, Collector<CdcInfo> collector) {
        //获取bin.log订阅到的信息
        String topic = sourceRecord.topic();
        String[] fields = topic.split("\.");
        String database = fields[1];
        String tableName = fields[2];
        Struct struct = (Struct) sourceRecord.value();
        final Struct source = struct.getStruct(SOURCE);
        CdcInfo tbCdcInfo = new CdcInfo();
        //获取前后变化数据
        tbCdcInfo.setBeforeData(convert2JsonObj(struct, BEFORE));
        tbCdcInfo.setAfterData(convert2JsonObj(struct, AFTER));
        //5.获取操作类型  CREATE UPDATE DELETE
        Envelope.Operation operation = Envelope.operationFor(sourceRecord);
        String type = operation.toString().toUpperCase();

        tbCdcInfo.setOperation(type);
        tbCdcInfo.setBinLogName(Optional.ofNullable(source.get(BIN_FILE)).map(Object::toString).orElse(""));
        tbCdcInfo.setFilePos(Optional.ofNullable(source.get(POS)).map(x -> Integer.parseInt(x.toString())).orElse(0));
        tbCdcInfo.setDbName(database);
        tbCdcInfo.setTbName(tableName);
        tbCdcInfo.setChangeTime(Optional.ofNullable(struct.get(TS_MS)).map(x -> Long.parseLong(x.toString())).orElseGet(System::currentTimeMillis));
        //7.输出数据
        collector.collect(tbCdcInfo);
    }

    /**
     * 从原始数据获取出变更之前或之后的数据
     */
    private JSONObject convert2JsonObj(Struct value, String fieldElement) {
        Struct element = value.getStruct(fieldElement);
        JSONObject jsonObject = new JSONObject();
        if (element != null) {
            Schema afterSchema = element.schema();
            List<Field> fieldList = afterSchema.fields();
            for (Field field : fieldList) {
                Object afterValue = element.get(field);
                jsonObject.put(field.name(), afterValue);
            }
        }
        return jsonObject;
    }


    @Override
    public TypeInformation<CdcInfo> getProducedType() {
        return TypeInformation.of(CdcInfo.class);
    }
}

此时我们启动程序后针对数据表进行修改操作就会收到数据消息的订阅了:

订阅到的数据:CdcInfo(beforeData={"id":1,"name":"xiaoming"}, afterData={"id":1,"name":"xiaoming1"}, operation=UPDATE, binLogName=binlog.000156, filePos=1256, dbName=db, tbName=tb_2, changeTime=1734622269654)

小结

我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:github.com/shark-ctrl/…

参考

(十一)MySQL日志篇之undo-log、redo-log、bin-log.....傻傻分不清!:juejin.cn/post/715795…

Flink CDC全量和增量同步数据如何保证数据的一致性:developer.aliyun.com/ask/588669

数据同步工具之FlinkCDC/Canal/Debezium对比:cloud.tencent.com/developer/a…

springboot集成flink-cdc:blog.csdn.net/leilei13666…

SpringBoot集成Flink-CDC,实现对数据库数据的监听 :blog.csdn.net/qq_43479493…

本文使用 markdown.com.cn 排版