SpringBoot项目里用mysql-binlog-connector监听数据变更,我是这么做的(附完整代码)
SpringBoot项目中优雅集成mysql-binlog-connector的实践指南
当用户注销账户时需要触发短信通知,传统做法是在业务代码中直接耦合短信发送逻辑。这种设计不仅让代码臃肿,更会导致后续维护成本指数级增长。有没有一种方案,能在不侵入业务代码的前提下实现这类需求?数据库的binlog监听技术给出了完美答案。
1. 技术选型与方案设计
在Java生态中实现MySQL binlog监听,主流方案有两种技术路线:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| mysql-binlog-connector | 轻量级,直接嵌入应用 | 需自行处理解析逻辑 | 中小型业务,快速迭代 |
| Canal | 功能完善,经过阿里验证 | 需独立部署维护 | 大型复杂系统 |
我们项目最终选择了mysql-binlog-connector,主要基于以下考量:
- 项目处于快速迭代期,需要最小化运维成本
- 业务逻辑相对简单,不需要复杂的数据管道
- 团队更熟悉Java技术栈,希望保持技术栈统一
核心架构设计要点:
- 配置化监听:通过YAML文件定义需要监听的数据库和表
- 统一事件封装:将binlog事件转换为统一的POJO对象
- 异步处理队列:采用生产者-消费者模式解耦监听和处理
- 多线程消费:提高事件处理吞吐量
2. 环境准备与配置
2.1 MySQL服务器配置
确保MySQL已开启binlog功能(MySQL 5.7+默认关闭):
-- 检查binlog状态 SHOW VARIABLES LIKE 'log_bin'; -- 查看当前binlog文件 SHOW BINARY LOGS;若未开启,需要修改MySQL配置文件(通常为my.cnf或my.ini):
[mysqld] log_bin=mysql-bin binlog-format=ROW server-id=1关键配置说明:
binlog-format必须设为ROW才能获取行变更详情- 修改后需要重启MySQL服务生效
2.2 SpringBoot项目依赖
在pom.xml中添加核心依赖:
<dependency> <groupId>com.github.shyiko</groupId> <artifactId>mysql-binlog-connector-java</artifactId> <version>0.21.0</version> </dependency> <!-- 辅助工具包 --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1.1-jre</version> </dependency>注意:生产环境建议锁定依赖版本,避免兼容性问题
3. 核心实现解析
3.1 配置加载与初始化
创建配置类加载application.yml中的配置:
@Data @Component public class BinlogConfig { @Value("${binlog.host}") private String host; @Value("${binlog.port}") private int port; @Value("${binlog.username}") private String username; @Value("${binlog.password}") private String password; @Value("${binlog.database}") private String database; @Value("${binlog.tables}") private List<String> tables; }3.2 事件监听器实现
创建核心监听器类处理binlog事件:
public class BinlogEventListener implements BinaryLogClient.EventListener { private final BlockingQueue<BinlogEvent> eventQueue; private final Map<String, TableSchema> tableSchemas; @Override public void onEvent(Event event) { EventType type = event.getHeader().getEventType(); if (type == EventType.TABLE_MAP) { // 处理表结构映射事件 cacheTableSchema(event.getData()); } else if (isDataChangeEvent(type)) { // 处理数据变更事件 BinlogEvent binlogEvent = convertToBinlogEvent(event); eventQueue.offer(binlogEvent); } } private boolean isDataChangeEvent(EventType type) { return type == EventType.WRITE_ROWS || type == EventType.UPDATE_ROWS || type == EventType.DELETE_ROWS; } }3.3 事件处理器设计
采用生产者-消费者模式处理事件:
@Slf4j @Component public class BinlogEventProcessor implements CommandLineRunner { @Autowired private BinlogConfig config; @Override public void run(String... args) throws Exception { // 初始化连接 BinaryLogClient client = new BinaryLogClient( config.getHost(), config.getPort(), config.getUsername(), config.getPassword() ); // 创建事件队列 BlockingQueue<BinlogEvent> queue = new ArrayBlockingQueue<>(1000); // 启动消费者线程 ExecutorService executor = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { executor.submit(new EventConsumer(queue)); } // 注册监听器 client.registerEventListener(new BinlogEventListener(queue)); client.connect(); } private static class EventConsumer implements Runnable { private final BlockingQueue<BinlogEvent> queue; EventConsumer(BlockingQueue<BinlogEvent> queue) { this.queue = queue; } @Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { BinlogEvent event = queue.take(); processEvent(event); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } }4. 高级特性与优化
4.1 断点续传实现
通过记录binlog位置实现故障恢复:
public class BinlogPositionStore implements BinaryLogClient.LifecycleListener { @Override public void onConnect(BinaryLogClient client) { // 从数据库或文件加载上次的位置 long position = loadPosition(); client.setBinlogPosition(position); } @Override public void onEventDeserializationFailure( BinaryLogClient client, Exception ex) { // 记录异常并尝试恢复 savePosition(client.getBinlogFilename(), client.getBinlogPosition()); } }4.2 性能优化技巧
- 批量处理:积累一定数量事件后批量处理
- 异步写入:使用Disruptor等高性能队列替代BlockingQueue
- 缓存优化:缓存表结构信息减少数据库查询
// 使用Caffeine缓存表结构 LoadingCache<String, TableSchema> schemaCache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.HOURS) .build(this::loadTableSchema);4.3 监控与告警
集成Micrometer暴露监控指标:
public class BinlogMetrics { private final MeterRegistry registry; private final AtomicInteger queueSize = new AtomicInteger(); public void bindTo(MeterRegistry registry) { Gauge.builder("binlog.queue.size", queueSize::get) .description("当前待处理事件队列大小") .register(registry); } }5. 典型应用场景实现
5.1 用户注销发送短信
public class UserCancelHandler implements BinlogEventHandler { @Override public boolean canHandle(BinlogEvent event) { return "user".equals(event.getTable()) && "status".equals(event.getColumn()) && "INACTIVE".equals(event.getNewValue()); } @Override public void handle(BinlogEvent event) { String userId = event.getRow().get("id"); smsService.sendCancelConfirm(userId); } }5.2 数据同步到Elasticsearch
public class EsSyncHandler implements BinlogEventHandler { private final RestHighLevelClient esClient; @Override public void handle(BinlogEvent event) { IndexRequest request = new IndexRequest(event.getTable()) .id(event.getRow().get("id")) .source(event.getRow()); esClient.index(request, RequestOptions.DEFAULT); } }5.3 审计日志记录
@Aspect @Component public class AuditLogAspect { @AfterReturning( pointcut = "execution(* com..BinlogEventProcessor.process(..))", returning = "event") public void logAudit(BinlogEvent event) { AuditLog log = new AuditLog(); log.setOperation(event.getType().name()); log.setTableName(event.getTable()); log.setRecordId(event.getRow().get("id")); auditLogRepository.save(log); } }6. 踩坑经验分享
时区问题:MySQL服务器与应用时区不一致会导致时间字段解析错误
- 解决方案:在JDBC连接字符串中指定时区参数
大事务处理:单个事务包含大量变更可能造成内存溢出
- 解决方案:增加
maxBinlogCacheSize配置
- 解决方案:增加
表结构变更:ALTER TABLE操作会导致后续事件解析失败
- 解决方案:监听
ALTER事件并刷新表结构缓存
- 解决方案:监听
网络闪断:连接断开后如何自动重连
- 解决方案:实现
LifecycleListener并设置connectTimeout
- 解决方案:实现
client.setConnectTimeout(30000); client.registerLifecycleListener(new ReconnectListener());在电商项目中实际应用时,我们发现当商品价格变更频率很高时,原始方案会出现事件堆积。最终通过引入本地缓存+批量更新的方式,将ES同步性能提升了8倍。关键优化点在于对频繁变更的字段做了防抖处理,避免不必要的索引更新。
