MySQL的binlog本质上是一份数据库变更的流水账,只要开启了row格式,每一行数据的插入、更新、删除都会被记录成可解析的事件。相比在业务代码里硬编码缓存更新逻辑,订阅binlog相当于获得了一个与业务无关的变更通知中心,任何需要同步到Redis的数据都可以通过消费这份日志来完成。其核心思路是:用一个伪装成从库的客户端连接MySQL,请求指定位置开始的binlog事件流,然后针对每个事件提取变更前后的行数据,最后转换为Redis命令执行写入或删除。

一、binlog监听同步Redis的核心原理
MySQL的binlog有三种记录格式:statement、row和mixed。statement格式只记录SQL语句本身,虽然日志量小,但面对一些带函数、触发器或不确定条件的SQL时,无法还原出准确的数据变化。row格式则直接记录每一行数据的前后镜像,例如更新前该行的字段值是什么,更新后变成了什么,删除前该行的完整内容是什么。这种细粒度的数据变化正是缓存同步需要的,因此监听binlog做缓存更新时,几乎都要求将binlog_format设置为row。
在row格式下,binlog事件流由若干事件组成,其中最关键的是TableMapEvent、WriteRowsEvent、UpdateRowsEvent和DeleteRowsEvent。TableMapEvent描述了表结构信息,包括列名、列类型等;WriteRowsEvent携带插入行的全部字段值;UpdateRowsEvent包含更新前和更新后的两套行数据;DeleteRowsEvent只包含被删除行的字段值。解析这些事件后,就可以知道哪个表、哪个主键、哪些字段发生了变化,进而决定要更新Redis中的哪个key、使用什么value。
整个同步管道通常分为四层:第一层是binlog dump客户端,它通过MySQL复制协议伪装成从库,从指定位点开始拉取binlog事件;第二层是事件解析器,负责把二进制事件转换成结构化的数据对象;第三层是消息队列或内存队列,用于解耦解析速度与Redis写入速度;第四层是消费者,它根据事件类型和配置规则,将数据变更转换为Redis命令。这种分层设计让每一层都可以独立扩展,也为后续接入更多目标存储提供了可能。
二、主流binlog监听工具与选型对比
目前业界已经有不少成熟的binlog监听组件,最常被提及的是Canal、Maxwell和Debezium。Canal是阿里巴巴开源的项目,采用Java开发,通过模拟MySQL从库的交互协议来获取binlog,支持单机、集群模式,并且提供了完善的位点管理和HA机制。它的特点是与阿里系中间件集成紧密,国内使用案例丰富,但社区活跃度近年有所下降,且配置相对复杂。
Maxwell是一个轻量级的binlog解析工具,同样使用Java编写,它的最大优点是输出格式非常简洁,默认将每一行变更转成JSON字符串,字段包含database、table、type、ts以及data或old数据。Maxwell适合快速落地,尤其适合需要将binlog变更发送到Kafka的场景,其部署和运维成本较低,但功能相对单一,复杂过滤和定制能力不如Canal。
Debezium则是一个更通用、更贴近云原生生态的变更数据捕获平台,它基于Kafka Connect框架,可以为MySQL、PostgreSQL、MongoDB等多种数据库提供变更事件流。Debezium的事件格式遵循统一的结构,支持schema演化,与Kafka生态无缝衔接,适合已经深度使用Kafka的企业。如果团队已有Flink或Kafka Streams等流处理基础设施,使用Debezium加Flink CDC可以实现更灵活的实时计算与缓存同步。
| 组件 | 开发语言 | 输出格式 | 依赖组件 | 适用场景 |
|---|---|---|---|---|
| Canal | Java | 自定义protobuf/JSON | 可选ZooKeeper | 国内中大型项目,需要精细位点控制 |
| Maxwell | Java | JSON | 可选Kafka | 轻量级同步,快速集成Kafka |
| Debezium | Java | 统一CDC事件 | Kafka Connect | 云原生、多数据源统一采集 |
如果只是单机或小规模业务,自研一个简易同步器也未尝不可。但生产环境下更建议优先使用经过验证的组件,避免重复处理位点、断连重连、表结构变更等棘手问题。选型时需要重点评估团队的技术栈、数据量级、对延迟的容忍度以及后续扩展需求。
三、最小可用的binlog同步器实现
为了更直观地理解整个流程,这里使用Java和mysql-binlog-connector-java库实现一个简化版同步器。该库封装了MySQL复制协议的细节,开发者只需要注册事件监听器,就能拿到解析后的行数据。示例中的核心逻辑是:从WriteRowsEvent中提取插入或更新的字段,拼接Redis的key和value,然后调用Redis客户端写入;从DeleteRowsEvent中提取主键,删除对应缓存。
import com.github.shyiko.mysql.binlog.BinaryLogClient;
import com.github.shyiko.mysql.binlog.event.*;
import redis.clients.jedis.Jedis;
public class BinlogSyncDemo {
public static void main(String[] args) throws Exception {
BinaryLogClient client = new BinaryLogClient("127.0.0.1", 3306, "root", "password");
client.setServerId(2001);
Jedis jedis = new Jedis("127.0.0.1", 6379);
client.registerEventListener(event -> {
EventData data = event.getData();
if (data instanceof WriteRowsEventData) {
WriteRowsEventData write = (WriteRowsEventData) data;
for (Object[] row : write.getRows()) {
String key = "user:" + row[0].toString();
String value = row[1].toString();
jedis.set(key, value);
}
} else if (data instanceof UpdateRowsEventData) {
UpdateRowsEventData update = (UpdateRowsEventData) data;
for (Object entryObj : update.getRows()) {
Map.Entry entry = (Map.Entry) entryObj;
Object[] after = (Object[]) entry.getValue();
String key = "user:" + after[0].toString();
String value = after[1].toString();
jedis.set(key, value);
}
} else if (data instanceof DeleteRowsEventData) {
DeleteRowsEventData delete = (DeleteRowsEventData) data;
for (Object[] row : delete.getRows()) {
String key = "user:" + row[0].toString();
jedis.del(key);
}
}
});
client.connect();
}
}
上面代码只演示了最简单的映射方式:假设表的主键是第一列,第二列是需要缓存的值。真实业务中往往需要配置表名与Redis key模板的映射关系,以及选择性同步某些字段。例如订单表的订单号、状态、金额可以组合成一个JSON对象存入Redis,而日志表也许完全不需要同步。
另一个必须处理的问题是表ID与表名的映射。binlog事件中TableMapEvent携带的是表ID,而rows事件里只有表ID,需要通过TableMapEvent缓存来还原表名。完整实现还需要保存位点信息,保证程序重启后能从上次断开的位置继续消费,而不是从头开始。
四、生产环境落地要注意的工程问题
顺序性与并发控制:binlog事件天然有序,如果消费者使用单线程处理,顺序能得到保证。但单线程吞吐有限,一旦启用多线程或分布式消费,就可能出现同一行数据的更新乱序。常见的做法是按主键哈希分区,保证同一主键的变更始终由同一个消费者线程处理,从而维持行级顺序。
幂等与重复消费:监听程序可能因为网络抖动或重启导致部分事件被重复处理。好在Redis的set和del命令本身是幂等的,重复执行不会产生副作用。但如果涉及计数、累加等非幂等操作,就需要在业务层面加入版本号或唯一ID去重。对于更新类事件,直接覆盖写入通常是最安全的选择。
断点续传与位点管理:程序重启时如果从头消费binlog,不仅浪费资源还会产生大量重复写入。因此需要定期将binlog文件名和偏移量,或者GTID集合持久化到本地文件、Redis或数据库中。使用GTID的好处是发生主从切换后,新主库可以自动根据GTID集合定位到正确的起点,避免手动计算位点。
延迟监控与告警:binlog同步是异步链路,从数据库提交到Redis更新之间必然存在延迟。生产环境需要监控这个延迟,例如通过定期写入心跳表并比较事件时间戳来实现。当延迟超过阈值时及时告警,同时评估是否需要增加消费者实例或优化批量写入性能。Redis的pipeline和批量命令可以显著减少网络往返次数,适合高写入量的缓存同步场景。
总的来说,binlog监听同步Redis是一种成熟且可扩展的缓存一致性方案。它把缓存维护从业务代码中剥离出来,降低了系统耦合度,但也引入了异步延迟和运维复杂度。是否采用、选用哪种工具,需要结合团队规模、数据量以及一致性要求综合判断。