MySQL 之 binlog 封装
本篇文章讲述的是 MySQL binlog 的监听使用;
早期有过一片简陋的,直接在业务模块直接使用,没有单独抽取封装:https://www.cnblogs.com/chxlay/p/15115543.html
关于 如何搭建主从,或者如何开启 主服务的 binlog 可以参看: https://www.cnblogs.com/chxlay/p/15650522.html
此项目地址已开放,地址: https://github.com/chxlay/kwanyon-binlog-java 此代码库的地址是新开放的,有些许改动,后没有进行测试
****** 注意: 后续的优化更新不再对博文进行修改更新,以 GitHub的更新为准 ******
编写初衷
1、可能有人会觉得,已经有 canal 这么成熟的技术框架了,还有必要自己封装一个进行实现吗?
2、canal 是一个比较成熟的,我之所以没有使用的原因是因为,需要安装一个 canal 服务端程序,然后 才能进行编写 客户端进行使用,造成服务资源的开支,那能否有一个较为轻量级的 binlog 框架呢,
3、这里就自行编写一个,借助 binlog 日志监听连接器 mysql-binlog-connector-java ,自行实现一个 无需服务端安装,直接使用客户端的 组件,除了 canal 之外的 轻量级选择
依赖方案选择
使用到的三方依赖支持, 两个依赖可选
1,mysql-binlog-connector-java ( groupId 为 com.github.shyiko ) 此依来 不建议使用,MySQL 8. x 以上版本存在 密码协议问题
GitHub 源码地址 :https://github.com/shyiko/mysql-binlog-connector-java
<!-- https://mvnrepository.com/artifact/com.github.shyiko/mysql-binlog-connector-java --> <dependency> <groupId>com.github.shyiko</groupId> <artifactId>mysql-binlog-connector-java</artifactId> <version>0.21.0</version> </dependency>
这是一个比较早期的依赖,写文章当时,此依赖最后更新日期为 2020 年 6 月,
存在的问题 MySQL 8.x 以上的版本 加密协议问题异常,关于问题 讨论 GitHub 中: https://github.com/shyiko/mysql-binlog-connector-java/issues/240
在个人早期的文章中有简单使用说明: https://www.cnblogs.com/chxlay/p/15115543.html 以及问题的处理 (不建议使用旧版本的文章的方式)
2、 mysql-binlog-connector-java (groupId 为 com.zendesk ) 此依赖其实也是 完全是将 上一个依赖 clone 后开发的
关于二者的关系,README 文件中有描述,感兴趣的朋友可以翻阅:
github 源码地址: https://github.com/osheroff/mysql-binlog-connector-java
<!-- https://mvnrepository.com/artifact/com.zendesk/mysql-binlog-connector-java --> <dependency> <groupId>com.zendesk</groupId> <artifactId>mysql-binlog-connector-java</artifactId> <version>0.27.5</version> </dependency>
第一个依赖貌似已经脱更,第二个依赖还在持续更新中
结构原理
图地址: https://assets.processon.com/chart_image/6376ea13f346fb01d40ece02.png
封装完成后的代码文件结构如下图:
代码示例:
1、依赖
<dependencies> <!-- MySQL binlog 日志监听 https://github.com/osheroff/mysql-binlog-connector-java --> <dependency> <groupId>com.zendesk</groupId> <artifactId>mysql-binlog-connector-java</artifactId> <version>${zendesk.binlog.version}</version> </dependency> <!-- 必须引入框架,整合 springboot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <scope>provided</scope> </dependency> <!-- 可根据个人习惯 或 项目技术栈 选择性引入 或 移除 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <!-- 日志依赖,可结合项目中使用的日志框架进行选择性引入 或 移除 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <scope>provided</scope> </dependency> </dependencies>
2、插件注解开关
@Documented @Target(value = ElementType.TYPE) @Retention(value = RetentionPolicy.RUNTIME) @Import(value = BinlogConfiguration.class) public @interface EnableBinlogListener { }
3、属性配置文件:
@Setter @ConfigurationProperties(prefix = "kwanyon.binlog") public class IBinlogProperties { /** * Master 主机连接地址 */ private String host; /** * 数据库名称 */ private String datebase; /** * 端口号 */ private int port = 3306; /** * 连接用户名 */ private String username; /** * 连接密码 */ private String password; /** * binlog 需要接收处理的 表名 */ private List<String> tables = new ArrayList<>(); }
自动注入配置类
@RequiredArgsConstructor @Configuration(proxyBeanMethods = false) @ComponentScan(value = {"com.kwanyon.common.binlog.support"}) @EnableConfigurationProperties(IBinlogProperties.class) public class BinlogConfiguration { private final ApplicationContext context; private final IBinlogProperties binlogProperties;
@Bean @Order @ConditionalOnMissingBean public MessagePublisher messagePublisher() { return new EventMessagePublisher(context); } @Bean @ConditionalOnMissingBean public IBinLogRunner iBinLogRunner(DefBinlogListener eventListener) { return new IBinLogRunner(eventListener, binlogProperties); } }
binlog 连接启动类 :
@Async 注意了,client.connect() 是一个阻塞的,所以需要单独开一条线程进行执行,我这里是使用Spring的 @Async ,前提是项目中启用了异步 @EnableAsync
也可以 自行使用多线程方式方式实现
@RequiredArgsConstructor public class IBinLogRunner implements CommandLineRunner { /** * Binlog 监听器作为集群的一个从节点角色,必须具备一个服务唯一ID */ private static final long SERVER_ID = 100000L; private final DefBinlogListener binlogListener; private final IBinlogProperties binlogProperties; @Async @Override public void run(String... args) throws Exception { BinaryLogClient client = new BinaryLogClient(binlogProperties.host(), binlogProperties.port(), binlogProperties.datebase(), binlogProperties.username(), binlogProperties.password()); /** * Binlog 监听器作为集群的一个从节点角色,必须具备一个服务唯一ID */ client.setServerId(SERVER_ID); // 自定义系列化(没有自定义系列化需求的朋友可以使用默认的 new EventDeserializer() ) EventHeaderV4Deserializer eventHeaderV4Deserializer = new EventHeaderV4Deserializer(); NullEventDataDeserializer nullEventDataDeserializer = new NullEventDataDeserializer(); IdentityHashMap<EventType, EventDataDeserializer> eventDataDeserializers = new IdentityHashMap<>(); Map<Long, TableMapEventData> tableMapEventByTableId = new HashMap<>(); this.registerDefaultEventDataDeserializers(eventDataDeserializers, tableMapEventByTableId); EventDeserializer eventDeserializer = new EventDeserializer(eventHeaderV4Deserializer, nullEventDataDeserializer, eventDataDeserializers, tableMapEventByTableId); client.setEventDeserializer(eventDeserializer); // 监听器 client.registerEventListener(binlogListener); client.connect(); } /** * 源码中的注册事件类型(这里自定义编写了 bit --> Boolean 的反系列化规则),没有这个需求的可以不需要这个函数,而使用框架默认的反系列化配置 * * @param eventDataDeserializers * @param tableMapEventByTableId * @see EventDeserializer 源码中的注册事件类型 */ private void registerDefaultEventDataDeserializers(IdentityHashMap<EventType, EventDataDeserializer> eventDataDeserializers, Map<Long, TableMapEventData> tableMapEventByTableId) { // 启动完成后 binlog 日志文件信息事件(日志文件索引,日志文件坐标) eventDataDeserializers.put(EventType.ROTATE, new RotateEventDataDeserializer()); // 启动时连接成功后的版本相关描述信息事件 eventDataDeserializers.put(EventType.FORMAT_DESCRIPTION, new FormatDescriptionEventDataDeserializer()); eventDataDeserializers.put(EventType.INTVAR, new IntVarEventDataDeserializer()); eventDataDeserializers.put(EventType.QUERY, new QueryEventDataDeserializer()); eventDataDeserializers.put(EventType.TABLE_MAP, new TableMapEventDataDeserializer()); eventDataDeserializers.put(EventType.XID, new XidEventDataDeserializer()); eventDataDeserializers.put(EventType.WRITE_ROWS, new WriteRowsEventDataDeserializer(tableMapEventByTableId)); eventDataDeserializers.put(EventType.UPDATE_ROWS, new UpdateRowsEventDataDeserializer(tableMapEventByTableId)); eventDataDeserializers.put(EventType.DELETE_ROWS, new DeleteRowsEventDataDeserializer(tableMapEventByTableId)); // 自定义反系列化类 (读写更新) eventDataDeserializers.put(EventType.EXT_WRITE_ROWS, (new WriteRowsDeserializer(tableMapEventByTableId)).setMayContainExtraInformation(true)); eventDataDeserializers.put(EventType.EXT_UPDATE_ROWS, (new UpdateRowsDeserializer(tableMapEventByTableId)).setMayContainExtraInformation(true)); eventDataDeserializers.put(EventType.EXT_DELETE_ROWS, (new DeleteRowsDeserializer(tableMapEventByTableId)).setMayContainExtraInformation(true)); eventDataDeserializers.put(EventType.ROWS_QUERY, new RowsQueryEventDataDeserializer()); eventDataDeserializers.put(EventType.GTID, new GtidEventDataDeserializer()); eventDataDeserializers.put(EventType.PREVIOUS_GTIDS, new PreviousGtidSetDeserializer()); eventDataDeserializers.put(EventType.XA_PREPARE, new XAPrepareEventDataDeserializer()); eventDataDeserializers.put(EventType.ANNOTATE_ROWS, new AnnotateRowsEventDataDeserializer()); eventDataDeserializers.put(EventType.MARIADB_GTID, new MariadbGtidEventDataDeserializer()); eventDataDeserializers.put(EventType.BINLOG_CHECKPOINT, new BinlogCheckpointEventDataDeserializer()); eventDataDeserializers.put(EventType.MARIADB_GTID_LIST, new MariadbGtidListEventDataDeserializer()); eventDataDeserializers.put(EventType.TRANSACTION_PAYLOAD, new TransactionPayloadEventDataDeserializer()); } }
自定义反系列化编写
(数据库中的 bit 反系列化为 Java 中的 Boolean 如果没有需要的用户可以简化配置)
删除行数据时反系列化
/** * Java类BitSet 自定义系列化规则 * * @author Alay * @date 2022-11-14 13:51 */ public class DeleteRowsDeserializer extends DeleteRowsEventDataDeserializer { public DeleteRowsDeserializer(Map<Long, TableMapEventData> tableMapEventByTableId) { super(tableMapEventByTableId); this.setMayContainExtraInformation(true); } /** * 自定义系列换中 数据库 Bit 字段转为 Boolean 类型 * * @param meta * @param inputStream * @return * @throws IOException */ @Override protected Serializable deserializeBit(int meta, ByteArrayInputStream inputStream) throws IOException { int bitSetLength = (meta >> 8) * 8 + (meta & 0xFF); BitSet bitSet = inputStream.readBitSet(bitSetLength, false); int cardinality = bitSet.cardinality(); return cardinality == 1; } }
更新行数据时自定义系列化
bit --> Boolean
/** * Java类BitSet 自定义系列化规则 * * @author Alay * @date 2022-11-14 13:51 */ public class UpdateRowsDeserializer extends UpdateRowsEventDataDeserializer { public UpdateRowsDeserializer(Map<Long, TableMapEventData> tableMapEventByTableId) { super(tableMapEventByTableId); this.setMayContainExtraInformation(true); } /** * 自定义系列换中 数据库 Bit 字段转为 Boolean 类型 * * @param meta * @param inputStream * @return * @throws IOException */ @Override protected Serializable deserializeBit(int meta, ByteArrayInputStream inputStream) throws IOException { int bitSetLength = (meta >> 8) * 8 + (meta & 0xFF); BitSet bitSet = inputStream.readBitSet(bitSetLength, false); int cardinality = bitSet.cardinality(); return cardinality == 1; } }
插入行数据时自动以反系列
bit ----> Boolean
/** * Java类BitSet 自定义系列化规则 * * @author Alay * @date 2022-11-14 13:51 */ public class WriteRowsDeserializer extends WriteRowsEventDataDeserializer { public WriteRowsDeserializer(Map<Long, TableMapEventData> tableMapEventByTableId) { super(tableMapEventByTableId); this.setMayContainExtraInformation(true); } /** * 自定义系列换中 数据库 Bit 字段转为 Boolean 类型 * * @param meta * @param inputStream * @return * @throws IOException */ @Override protected Serializable deserializeBit(int meta, ByteArrayInputStream inputStream) throws IOException { int bitSetLength = (meta >> 8) * 8 + (meta & 0xFF); BitSet bitSet = inputStream.readBitSet(bitSetLength, false); int cardinality = bitSet.cardinality(); return cardinality == 1; } }
4、核心功能代码:
常量类:
public interface BinlogConst { /** * INSERT、UPDATE、DELETE 的事件的 SQL */ String EVENT_SQL_BEGIN = "BEGIN"; /** * 创建表 */ String EVENT_SQL_CREATE = "CREATE"; /** * 修改表 */ String EVENT_SQL_ALTER = "ALTER"; /** * 删除表 */ String EVENT_SQL_DROP = "DROP"; /** * 重命名 RENAME TABLE `database`.`table_name` TO `database`.`new_table_name` */ String EVENT_SQL_RENAME = "RENAME"; }
binlog 日志监听入口: 监听器
@Component @RequiredArgsConstructor public class DefBinlogListener implements BinaryLogClient.EventListener { private final ServiceAdapter serviceAdapter; /** * 一次MySQL的修改、插入、删除,会触发多次事件,会调用方法多次,注意处理好逻辑优化性能 * 启动时事件顺序:ROTATE、FORMAT_DESCRIPTION * 启动后事件触发顺序: * 1、每次操作只触发一次(批量操作也只会触发一次):ANONYMOUS_GTID,QUERY, * 2、每条记录row 触发一次独立的事件:TABLE_MAP(携带表库字段信息)批量也会触发多次 * 3、多选一具体事件:EXT_UPDATE_ROWS(执行更新),EXT_DELETE_ROWS,EXT_WRITE_ROWS * 4、每次操作只触发一次(批量操作也只会触发一次) XID * @param event */ @Override public void onEvent(Event event) { EventType eventType = event.getHeader().getEventType(); EventData eventData = event.getData(); // 无需处理 if (null == eventData) return; // 匹配事件处理器 EventProcess eventProcess = serviceAdapter.matchProcess(eventType); if (null == eventProcess) return; // 事件处理 eventProcess.process(eventData); } }
binlog 事件类型适配器 注册
/** * 服务注册适配器 * @author Alay * @date 2022-11-15 11:01 */ @Component public class ServiceAdapter { private static final List<EventProcess> EVENT_PROCESSES = new ArrayList<>(1 << 5); public synchronized static void registry(EventProcess eventParser) { EVENT_PROCESSES.add(eventParser); } public EventProcess matchProcess(EventType eventType) { for (EventProcess eventProcess : EVENT_PROCESSES) { boolean support = eventProcess.support(eventType); if (support) return eventProcess; } // 没有定义或注册事件处理器 return null; } }
事件处理器接口:
/** * 事件处理器 * * @author Alay * @date 2022-11-15 11:31 */ public interface EventProcess<T extends EventData> { /** * 事件注册器 */ @EventListener(value = WebServerInitializedEvent.class) default void registry() { ServiceAdapter.registry(this); }
boolean support(EventType eventType);
void process(T eventData); }
事件处理器具体实现类(目前仅仅实现了常用的,并不是所有的事件类型都编写了对应的 事件处理器,需要扩展的请自行扩展)
实现类:
1、启动时 binlog 文件初始化读取事件处理器
/** * 启动时事件顺序:ROTATE、FORMAT_DESCRIPTION * * @author Alay * @date 2022-11-15 10:51 */ @Component public class EventRotateProcess implements EventProcess<RotateEventData> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public boolean support(EventType eventType) { return EventType.ROTATE == eventType; } @Override public void process(RotateEventData eventData) { logger.info("---------------<<< 监听的 binlog 文件名:{} >>>--------------------", eventData.getBinlogFilename()); logger.info("---------------<<< binlog 坐标 :{} >>>--------------------", eventData.getBinlogPosition()); } }
2、启动时 binlog MySQL 版本信息 事件处理器
/** * 启动时版本信息数据 * 启动时事件顺序:ROTATE、FORMAT_DESCRIPTION * @author Alay * @date 2022-11-15 10:53 */ @Component public class EventFormatDescProcess implements EventProcess<FormatDescriptionEventData> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final EventType eventType = EventType.FORMAT_DESCRIPTION; @Override public boolean support(EventType eventType) { return this.eventType == eventType; } @Override public void process(FormatDescriptionEventData eventData) { logger.info("---------------<<< binlog 启动并连接成功 >>>--------------------"); logger.info("---------------<<< binlog 版本:{} >>>--------------------", eventData.getBinlogVersion()); logger.info("---------------<<< MySQL 版本:{} >>>--------------------", eventData.getServerVersion()); } }
3、 QUERY (2) 事件处理器
/** * 此事件操作的是修改表结构,修改后需要将缓存中存储的表结构删除 * 解析得出修改表结构的 sql 语句进行解析 * sql='ALTER TABLE `db_name`.`table_name`后面是具体的执行语句 * @author Alay * @date 2022-11-15 11:30 */ @Component @RequiredArgsConstructor public class EventQueryProcess implements EventProcess<QueryEventData> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final MessagePublisher messagePublisher; private final IBinlogProperties binlogProperties; private final TableSchemaManager tableSchemaManager; private final DropEventParser dropEventParser; private final AlterEventParser alterEventParser; private final CreateEventParser createEventParser; private final RenameEventParser renameEventParser; @Override public boolean support(EventType eventType) { return EventType.QUERY == eventType; } @Override public void process(QueryEventData eventData) { String sql = eventData.getSql(); // INSERT、UPDATE、DELETE 的事件,不做处理 if (BinlogConst.EVENT_SQL_BEGIN.equals(sql)) return; BinlogMessage binlogMessage = null; // 修改表事件 if (sql.startsWith(BinlogConst.EVENT_SQL_ALTER)) { sql = this.beautifySql(sql); // sql 美化 eventData.setSql(sql); logger.info("binlog 修改表事件 SQL: {}", sql); binlogMessage = alterEventParser.parseEvent(eventData); // 修改表结构,删除缓存 tableSchemaManager.removeCache(binlogMessage.getSchema() + "." + binlogMessage.getTableName()); } // 新增表事件 if (sql.startsWith(BinlogConst.EVENT_SQL_CREATE)) { sql = this.beautifySql(sql); // sql 美化 eventData.setSql(sql); logger.info("binlog 新增表事件 SQL: {}", sql); binlogMessage = createEventParser.parseEvent(eventData); } // 删除表事件 if (sql.startsWith(BinlogConst.EVENT_SQL_DROP)) { // DROP TABLE IF EXISTS `t_student` /* generated by server */ sql = sql.substring(0, sql.indexOf("/*")); sql = this.beautifySql(sql); // sql 美化 eventData.setSql(sql); logger.info("binlog 删除表事件 SQL: {}", sql); binlogMessage = dropEventParser.parseEvent(eventData); // 修改表结构,删除缓存 tableSchemaManager.removeCache(binlogMessage.getSchema() + "." + binlogMessage.getTableName()); } // 表名重命名 RENAME TABLE `database`.`table_name` TO `database`.`new_table_name` if (sql.startsWith(BinlogConst.EVENT_SQL_RENAME)) { sql = this.beautifySql(sql); // sql 美化 eventData.setSql(sql); // RENAME TABLE `database`.`table_name` TO `database`.`new_table_name` RenameMessage renameMessage = renameEventParser.parseEvent(eventData); // 修改表结构,删除缓存 tableSchemaManager.removeCache(renameMessage.getSchema() + "." + renameMessage.getBeforeTable()); binlogMessage = renameMessage; } // 不是我需要处理的事件,不做处理 if (null == binlogMessage) return; // 判断是否需要处理此表的日志相关 List<String> tables = binlogProperties.tables(); // 不需要处理,不属于处理的表名中 if (!tables.contains(binlogMessage.getSchema() + "." + binlogMessage.getTableName())) return; messagePublisher.publish(binlogMessage); } /** * 美化Sql * sql = sql.replace("\n", "").replace("\t", "").replace(" ", " ").replace("( ", "(").replace(" )", ")").replace(" ,", ","); */ private String beautifySql(String sql) { sql = sql.replaceAll("[\\s\n]+", " "); // 去除尾部空格 char[] chars = sql.toCharArray(); char blank = ' '; for (int i = chars.length - 1; i >= 0; i--) { char c = chars[i]; if (blank != c) break; sql = sql.substring(0, i); } return sql; } }
4、表结构映射关系 事件处理器
/** * INSERT / UPDATE / DELETE 将会触发此事件 * @author Alay * @date 2022-11-15 11:35 */ @Component @RequiredArgsConstructor public class EventTableMapProcess implements EventProcess<TableMapEventData> { private final IBinlogProperties binlogProperties; private final TableSchemaManager tableSchemaManager; @Override public boolean support(EventType eventType) { return EventType.TABLE_MAP == eventType; } @Override public void process(TableMapEventData eventData) { /** * 表结构映射事件 */ TableMapEventData mapEventData = eventData; // tableId 并非表名 long tableId = mapEventData.getTableId(); boolean hasCache = tableSchemaManager.hasCache(tableId); // 此表已经不是第一次触发该事件了,不需要重复的处理做准备的工作 if (hasCache) return; // 库名 String database = mapEventData.getDatabase(); // 表名 String tableName = mapEventData.getTable(); // 是否数据我们需要处理的表,不属于的不处理 boolean needProcess = binlogProperties.tables().contains(database + "." +tableName); if (!needProcess) return; // 表结构对象封装 TableSchema tableSchema = tableSchemaManager.queryTableSchema(tableId, database, tableName); tableSchemaManager.cacheTable(tableSchema); } }
5、插入行数据事件处理器
@Component @RequiredArgsConstructor public class EventInsertRowProcess implements EventProcess<WriteRowsEventData> { private final MessagePublisher messagePublisher; private final TableSchemaManager tableSchemaManager; private final InsertEventParser insertEventParser; @Override public boolean support(EventType eventType) { return EventType.EXT_WRITE_ROWS == eventType; } @Override public void process(WriteRowsEventData eventData) { // 表结构数据获取 TableSchema cacheSchema = tableSchemaManager.getCacheSchema(eventData.getTableId()); // 解析插入的数据事件 InsertMessage insertMessage = insertEventParser.parseEvent(eventData, cacheSchema); messagePublisher.publish(insertMessage); } }
6、删除行数据 事件处理器
@Component @RequiredArgsConstructor public class EventDeleteRowProcess implements EventProcess<DeleteRowsEventData> { private final MessagePublisher messagePublisher; private final TableSchemaManager tableSchemaManager; private final DeleteEventParser deleteEventParser; @Override public boolean support(EventType eventType) { return EventType.EXT_DELETE_ROWS == eventType; } @Override public void process(DeleteRowsEventData eventData) { long tableId = eventData.getTableId(); // 表结构数据 TableSchema cacheSchema = tableSchemaManager.getCacheSchema(tableId); DeleteMessage deleteMessage = deleteEventParser.parseEvent(eventData, cacheSchema); // 发送解析后的消息数据 messagePublisher.publish(deleteMessage); } }
7、更行行数据 事件处理器
@Component @RequiredArgsConstructor public class EventUpdateRowProcess implements EventProcess<UpdateRowsEventData> { private final MessagePublisher messagePublisher; private final TableSchemaManager tableSchemaManager; private final UpdateEventParser updateEventParser; @Override public boolean support(EventType eventType) { return EventType.EXT_UPDATE_ROWS == eventType; } @Override public void process(UpdateRowsEventData eventData) { long tableId = eventData.getTableId(); // 表结构数据获取 TableSchema cacheSchema = tableSchemaManager.getCacheSchema(tableId); // 解析 binlog 事件数据 UpdateMessage updateMessage = updateEventParser.parseEvent(eventData, cacheSchema); // 发送消息数据 messagePublisher.publish(updateMessage); } }
表结构数据信息 schema 获得:
关于表结构获取的方式有两种, 一种是 主动查询数据库, 第二种是 设置 MySQL 配置 , binlog 事件中 携带表结构数据(此法不推荐,每次事件都携带表结构数据将是不小的 IO 开支)
关于两种方案的是有在我的第一篇文章中有叙述:https://www.cnblogs.com/Alay/p/15115543.html
第一种:修改 MySQL配置,binlog 日志数据携带表 schema 信息
不推荐,每次事件都需要携带 表 schema 的数据,但并不是每次事件都需要 schema 信息数据,浪费 IO
第二种:自行编写代码按需查询
推荐使用;自行编写代码查询数据库 schema 信息并缓存使用,代码如下:
@Component @RequiredArgsConstructor public class TableSchemaManager { private final Logger logger = LoggerFactory.getLogger(this.getClass()); /** * tableId 与 表全名的映射关系 */ private static final Map<Long, String> TABLE_ID_MAP = new HashMap<>(1 << 4); /** * 表名(全名)与表结构的映射 */ private static final Map<String, TableSchema> SCHEMA_MAP = new HashMap<>(1 << 4); private final IBinlogProperties binlogProperties; private String url; @EventListener(value = WebServerInitializedEvent.class) public void initSqlUrl() { url = String.format("jdbc:mysql://%s:%s/information_schema", binlogProperties.host(), binlogProperties.port()); } private static final String TABLE_SCHEMA_SQL = "SELECT " + "`TABLE_SCHEMA`," + "`TABLE_NAME`," + "`COLUMN_NAME`," + "`ORDINAL_POSITION`," + "`COLUMN_DEFAULT`," + "`IS_NULLABLE`," + "`DATA_TYPE`," + "`CHARACTER_MAXIMUM_LENGTH`," + "`CHARACTER_OCTET_LENGTH`," + "`NUMERIC_PRECISION`," + "`NUMERIC_SCALE`," + "`CHARACTER_SET_NAME`," + "`COLLATION_NAME` " + "FROM" + "`information_schema`.`columns` " + "WHERE " + "`TABLE_SCHEMA` = ? " + "AND `TABLE_NAME` = ?"; public TableSchema queryTableSchema(Long tableId, String database, String tableName) { // 内存中获取 TableSchema tableSchema = SCHEMA_MAP.get(database + "." + tableName); if (null != tableSchema) return tableSchema; // 数据库中查询获取 Connection connection = null; PreparedStatement statement = null; ResultSet rs = null; try { connection = DriverManager.getConnection(url, binlogProperties.username(), binlogProperties.password()); statement = connection.prepareStatement(TABLE_SCHEMA_SQL); statement.setString(1, database); statement.setString(2, tableName); // 执行 Query 查询 语句 rs = statement.executeQuery(); tableSchema = TableSchema.builder(database, tableName); while (rs.next()) { // 字段创建 TableSchema.Column column = tableSchema.buildColumn(rs.getString(3)); column.ordinalPosition(rs.getInt(4)); column.columnDefault(rs.getString(5)); column.isNullable(rs.getString(6)); column.dataType(rs.getString(7)); column.characterMaximumLength(rs.getString(8)); column.characterOctetLength(rs.getString(9)); column.numericPrecision(rs.getString(10)); column.numericScale(rs.getString(11)); column.characterSetName(rs.getString(12)); column.collationName(rs.getString(13)); // 每个字段 tableSchema.addColumn(column); } // 排序 List<TableSchema.Column> columns = tableSchema.columns().stream().sorted().collect(Collectors.toList()); // 每个字段的详细数据,临时使用,为了减少存储空间,使用完后,将字段数据设置为 null tableSchema.columns(null); List<String> columnNames = columns.stream().map(TableSchema.Column::columnName).collect(Collectors.toList()); tableSchema.columnNames(columnNames); tableSchema.tableId(tableId); return tableSchema; } catch (SQLException e) { logger.error("message:{}, case:{}", e.getMessage(), e.getCause()); throw new RuntimeException(e.getMessage()); } finally { try { if (null != rs) rs.close(); } catch (SQLException e) { throw new RuntimeException(e.getMessage()); } try { if (null != statement) statement.close(); } catch (SQLException e) { throw new RuntimeException(e.getMessage()); } try { if (null != connection) connection.close(); } catch (SQLException e) { throw new RuntimeException(e.getMessage()); } } } public TableSchema getCacheSchema(Long tableId) { String tableInfo = TABLE_ID_MAP.get(tableId); return SCHEMA_MAP.get(tableInfo); } public boolean hasCache(Long tableId) { return TABLE_ID_MAP.get(tableId) != null; } public void cacheTable(TableSchema tableSchema) { // 本地缓存 SCHEMA_MAP.put(tableSchema.tableSchema() + "." + tableSchema.tableName(), tableSchema); // tableId 映射关系缓存 TABLE_ID_MAP.put(tableSchema.tableId(), tableSchema.tableSchema() + "." + tableSchema.tableName()); } public void removeCache(String tableInfo) { TableSchema tableSchema = SCHEMA_MAP.get(tableInfo); if (null != tableSchema) { // 移除 TABLE_ID_MAP.remove(tableSchema.tableId()); SCHEMA_MAP.remove(tableInfo); } } }
Event 数据解析
核心接口:
public interface BinlogEventParser<E extends EventData, T extends BinlogMessage> { /** * 解析 Binlog 事件 * @param event Binlog 事件数据 * @param args 其他参数数据(可能为null,只是部分事件解析器需要使用) * @return */ T parseEvent(E event, Object... args); }
实现类:
(并不是所有的 事件类型都编写了实现类,仅仅对常用的事件类型进行了编写,需要扩展的请自行扩展)
1、创建表事件消息解析
@Component public class CreateEventParser implements BinlogEventParser<QueryEventData, BinlogMessage> { private static final String ACTION = "CREATE"; @Override public BinlogMessage parseEvent(QueryEventData event, Object... args) { // 解析表名 String tableName = this.parseTableName(event.getSql()); // 暂时没有过多的数据封装 return BinlogMessage.builder() .sql(event.getSql()) .schema(event.getDatabase()) .tableName(tableName) .action(ACTION); } /** * CREATE TABLE `t_student` ( `id` INT, `name` VARCHAR ( 20 ), `sex` VARCHAR ( 2 ), `password` VARCHAR ( 32 ), `birthday` DATETIME ) * @param sql * @return */ private String parseTableName(String sql) { // 去除 符号 ` sql = sql.replaceAll("`", ""); // 去除 CREATE TABLE String str = sql.substring(13); // 得到表名 String tableName = str.substring(0, str.indexOf(" ")); return tableName; } }
2、删除表事件 消息解析
@Component public class DropEventParser implements BinlogEventParser<QueryEventData, BinlogMessage> { private static final String ACTION = "DROP"; @Override public BinlogMessage parseEvent(QueryEventData event, Object... args) { // DROP TABLE IF EXISTS `t_student` String tableName = this.parseTableName(event.getSql()); // 暂时没有过多的数据封装 return BinlogMessage.builder() .sql(event.getSql()) .schema(event.getDatabase()) .tableName(tableName) .action(ACTION); } private String parseTableName(String sql) { // DROP TABLE IF EXISTS `t_student` sql = sql.replaceAll("`", ""); String tableName = sql.substring(sql.lastIndexOf(" ") + 1); return tableName; } }
3、修改消息解析器
@Component public class AlterEventParser implements BinlogEventParser<QueryEventData, BinlogMessage> { private static final String ACTION = "ALTER"; @Override public BinlogMessage parseEvent(QueryEventData event, Object... args) { /** * 修改表结构的事件 ( db_name.table_name ) * ALTER TABLE xtech_office.t_student ADD COLUMN `xxx`(这个是字段名称) varchar(255) NULL AFTER birthday * ALTER TABLE xtech_office.t_student MODIFY COLUMN `xxxx` * ALTER TABLE xtech_office.t_student DROP COLUMN `xxx` (这个是字段名称) */ String tableName = this.parseTableName(event.getSql()); return BinlogMessage.builder() .schema(event.getDatabase()) .tableName(tableName) .sql(event.getSql()).action(ACTION); } private String parseTableName(String sql) { sql = sql.replaceAll("`", ""); /** * 修改表结构的事件 ( db_name.table_name ) * ALTER TABLE xtech_office.t_student ADD COLUMN `xxx`(这个是字段名称) varchar(255) NULL AFTER birthday * ALTER TABLE xtech_office.t_student MODIFY COLUMN `xxxx` * ALTER TABLE xtech_office.t_student DROP COLUMN `xxx` (这个是字段名称) */ String str = sql.substring(sql.indexOf(".")); String tableName = str.substring(1,str.indexOf(" ")); return tableName; } }
4、重命名表事件 消息解析
@Component public class RenameEventParser implements BinlogEventParser<QueryEventData, RenameMessage> { private static final String ACTION = "RENAME"; @Override public RenameMessage parseEvent(QueryEventData event, Object... args) { // DROP TABLE IF EXISTS `t_student` String[] tableNameArr = this.parseTableName(event.getSql()); // 暂时没有过多的数据封装 RenameMessage renameMessage = RenameMessage.builder() .beforeTable(tableNameArr[0]) .afterTable(tableNameArr[1]); renameMessage.sql(event.getSql()) .schema(event.getDatabase()) .tableName(tableNameArr[1]) .action(ACTION); return renameMessage; } private String[] parseTableName(String sql) { // RENAME TABLE `database`.`table_name` TO `database`.`new_table_name` sql = sql.replaceAll("`", ""); String[] tableNameArr = new String[2]; // 去除 RENAME TABLE String str = sql.substring(13); String schema = str.substring(0, str.indexOf(" ")); String beforeName = schema.substring(schema.indexOf('.') + 1); tableNameArr[0] = beforeName; // 新的表名 String afterName = str.substring(str.lastIndexOf(".") + 1); tableNameArr[1] = afterName; return tableNameArr; } }
5、插入数据事件 消息解析
@Component public class InsertEventParser implements BinlogEventParser<WriteRowsEventData, InsertMessage> { private static final String ACTION = "INSERT"; @Override public InsertMessage parseEvent(WriteRowsEventData event, Object... args) { // 表结构 TableSchema tableSchema = (TableSchema) args[0]; // 新增的这一行数据 List<Serializable[]> newRows = event.getRows(); Map<String, Serializable> insertData = this.tableDataParse(tableSchema.columnNames(), newRows); InsertMessage insertMessage = InsertMessage.builder().insertData(insertData); insertMessage.tableId(event.getTableId()) .tableName(tableSchema.tableName()) .schema(tableSchema.tableSchema()) .action(ACTION); return insertMessage; } private Map<String, Serializable> tableDataParse(List<String> columnNames, List<Serializable[]> newRows) { Map<String, Serializable> data = new HashMap<>(); Serializable[] valueArr = newRows.get(0); for (int i = 0; i < columnNames.size(); i++) { String columName = columnNames.get(i); data.put(columName, valueArr[i]); } return data; } }
6、删除数据事件 消息解析
@Component public class DeleteEventParser implements BinlogEventParser<DeleteRowsEventData,DeleteMessage> { private static final String ACTION = "DELETE"; @Override public DeleteMessage parseEvent(DeleteRowsEventData event, Object... args) { // 表结构 TableSchema tableSchema = (TableSchema) args[0]; // 删除的这一行数据 List<Serializable[]> deleteRows = event.getRows(); Map<String, Serializable> insertData = this.tableDataParse(tableSchema.columnNames(), deleteRows); DeleteMessage deleteMessage = DeleteMessage.builder().deleteData(insertData); deleteMessage.tableId(event.getTableId()) .tableName(tableSchema.tableName()) .schema(tableSchema.tableSchema()) .action(ACTION); return deleteMessage; } private Map<String, Serializable> tableDataParse(List<String> columnNames, List<Serializable[]> deleteRows) { Map<String, Serializable> data = new HashMap<>(); Serializable[] valueArr = deleteRows.get(0); for (int i = 0; i < columnNames.size(); i++) { String columName = columnNames.get(i); data.put(columName, valueArr[i]); } return data; } }
7、更新消息解析器
@Component public class UpdateEventParser implements BinlogEventParser<UpdateRowsEventData, UpdateMessage> { private static final String ACTION = "UPDATE"; @Override public UpdateMessage parseEvent(UpdateRowsEventData event, Object... args) { // 表结构 TableSchema tableSchema = (TableSchema) args[0]; List<String> columnNames = tableSchema.columnNames(); // 修改前的数据 List<Map.Entry<Serializable[], Serializable[]>> rows = event.getRows(); // 解析数据 Map<String, Serializable>[] maps = this.tableDataParse(columnNames, rows); UpdateMessage updateMessage = UpdateMessage.builder() .beforeData(maps[0]) .afterData(maps[1]); updateMessage.tableId(event.getTableId()) .tableName(tableSchema.tableName()) .schema(tableSchema.tableSchema()) .action(ACTION); return updateMessage; } private Map<String, Serializable>[] tableDataParse(List<String> columnNames, List<Map.Entry<Serializable[], Serializable[]>> rows) { Map<String, Serializable>[] dataMapArr = new HashMap[2]; // 数据体 Map.Entry<Serializable[], Serializable[]> entry = rows.get(0); // 修改前的数据 Serializable[] before = entry.getKey(); Map<String, Serializable> beforeData = new HashMap<>(); for (int i = 0; i < columnNames.size(); i++) { String columName = columnNames.get(i); beforeData.put(columName, before[i]); } dataMapArr[0] = beforeData; // 修改后的数据 Serializable[] after = entry.getValue(); Map<String, Serializable> afterData = new HashMap<>(); for (int i = 0; i < columnNames.size(); i++) { String columName = columnNames.get(i); afterData.put(columName, after[i]); } dataMapArr[1] = afterData; return dataMapArr; } }
事件数据体示例展示:
1、修改表结构的事件数据实例如下:(以修改表结构为例)
Event{
header=EventHeaderV4{
timestamp=1609379484000,
eventType=QUERY,
serverId=1,
headerLength=19,
dataLength=256,
nextPosition=20948893,
flags=0
},
data=QueryEventData{
threadId=305142,
executionTime=1,
errorCode=0,
database='braineex',
sql='ALTER TABLE `database_name`.`table_name` ------------>> 修改表结构事件
MODIFY COLUMN
`nick_name` varchar(31)
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci NULL
DEFAULT NULL
COMMENT '昵称'
AFTER `age`'
}
}
2、增、删、改 事件的数据体实例如下:
Event{ header=EventHeaderV4{ timestamp=1609380048000, eventType=QUERY, serverId=1, headerLength=19, dataLength=60, nextPosition=20949876, flags=8 }, data=QueryEventData{ threadId=305142, executionTime=0, errorCode=0, database='database_name', sql='BEGIN' } }
消息发送器
解析后的消息,发送出去
核心发送接口:
public interface MessagePublisher { /** * 日志消息发送器 * @param message */ void publish(BinlogMessage message); }
1、通过 Spring 的事件 发送(默认实现)
这也是一个默认的实现方式, 可通过注入其他方式发送器,替换 此默认发送器、
/** * Spring 事件发送实现方案(默认实现方案) * @author Alay * @date 2022-11-17 20:23 */ @RequiredArgsConstructor public class EventMessagePublisher implements MessagePublisher { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final ApplicationContext context; @Override public void publish(BinlogMessage message) { logger.info(message.toString()); MessageEvent messageEvent = new MessageEvent(message); context.publishEvent(messageEvent); } public static class MessageEvent extends ApplicationEvent { public MessageEvent(Object source) { super(source); } } }
MQ 方式发送消息方案(未实现)
未实现的发送器示例(请根据需求自行实现)
1、Kafka
/** * Kafka 发送消息实现方案 * 注入时 请使用注解 @Primary ,使其优先于 EventMessageSender * @author Alay * @date 2022-11-17 21:54 */ public class KafkaMessagePublisher implements MessagePublisher { @Override public void publish(BinlogMessage message) { // 这里只是示例,使用者请自行去实现,注入时 请使用注解 @Primary ,使其优先于 EventMessageSender } }
2、Rabbit
/** * RabbitMQ 消息队列发送实现方案 * 注入时 请使用注解 @Primary ,使其优先于 EventMessageSender * @author Alay * @date 2022-11-17 21:55 */ public class RabbitMessagePublisher implements MessagePublisher { @Override public void publish(BinlogMessage message) { // 这里只是示例,使用者请自行去实现,注入时 请使用注解 @Primary ,使其优先于 EventMessageSender } }
实体类消息体:
这里仅仅编写核心字段,为了精简代码,getter / setter 等附带的函数就不在下面代码中展示
1、通用的 消息体:
@Getter @ToString public class BinlogMessage implements Serializable { /** * 事件的 TableId, 可能为null( CREATE/ALTER/RENAME时候为 null) */ private Long tableId; /** * 库名(不会为 null ) */ private String schema; /** * 表名(不会为 null ) * * @return */ private String tableName; /** * SQL 语句,可能为null( 只有 CREATE TABLE / DROP TABLE / ALTER TABLE / RENAME TABLE 时候才有 ) * * @return */ private String sql; /** * 操作的动作 */ private String action; }
2、表重命名 事件
@Getter public class RenameMessage extends BinlogMessage { /** * 修改前的表名 */ private String beforeTable; /** * 修改后的表名 */ private String afterTable; }
3、插入事件消息
@Setter public class InsertMessage extends BinlogMessage { /** * 插入的数据内容 */ private Map<String, Serializable> insertData; }
4、删除事件消息
@Getter public class DeleteMessage extends BinlogMessage { private Map<String, Serializable> deleteData; }
5、更新数据事件消息
@Getter public class UpdateMessage extends BinlogMessage { /** * 修改前的数据 */ private Map<String, Serializable> beforeData; /** * 修改后的数据 */ private Map<String, Serializable> afterData; }
6、表接口实体:
@Getter @Setter @Accessors(chain = true, fluent = true) public class TableSchema implements Serializable { private static final long serialVersionUID = 1L; /** * 给binlog 使用的 */ private Long tableId; /** * 数据库名称 */ private String tableSchema; /** * 数据表名 */ private String tableName; /** * 字段 */ private List<Column> columns; /** * 字段名称 */ private List<String> columnNames; @Getter @Setter public static class Column implements Comparable<Column> { /** * 字段名 */ private String columnName; /** * 表结构字段排序 1,2,3, .... */ private Integer ordinalPosition; /** * 默认值 */ private String columnDefault; /** * 是否可以为 null (YES/NO) */ private String isNullable; /** * 数据类型 (varchar / int / date ....) */ private String dataType; /** * 数据类型长度 */ private String characterMaximumLength; /** * 以字节为单位的最大长度 */ private String characterOctetLength; private String numericPrecision; private String numericScale; /** * 字符集 utf8 */ private String characterSetName; /** * utf8_general_ci */ private String collationName; @Override public int compareTo(Column o) { return Integer.compare(this.ordinalPosition, o.ordinalPosition); } } public static TableSchema builder(String dbname, String tableName) { TableSchema schema = new TableSchema(); schema.tableSchema = dbname; schema.tableName = tableName; schema.columns = new ArrayList<>(); schema.columnNames = new ArrayList<>(); return schema; } public Column buildColumn(String columnName) { Column column = new Column(); column.columnName = columnName; return column; } public void addColumn(Column column) { this.columns.add(column); } }
使用示例:
1、 引入依赖
以上的封装是将其封装为 一个独立的模块
<dependency> <groupId>com.kwanyon.starter</groupId> <artifactId>kwanyon-common-binlog</artifactId> </dependency>
2、主启动类或者配置类,加入启动开关
@EnableBinlogListener
即可启动使用
3、配置文件:
# 这里是示例的配置,开发中配置已迁移到 nacos kwanyon: # binlog 连接配置 binlog: host: ${MASTER_HOST:spring.datasource.host} port: ${spring.datasource.port:3306} username: ${spring.datasource.druid.username:root} password: ${spring.datasource.druid.password:123456} tables: # 这里配置的意思是, binlog 日志监听的表千千万,但是我只处理我这里配置的表名(微服务中每个模块单独配置,不同的模块 监听处理的是不同的表事件) - database.table_name_1 - database.table_name_2
本文来自博客园,作者:Vermeer,转载请注明原文链接:https://www.cnblogs.com/chxlay/p/16902163.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步