MySQL同步NoSQL
2022-11 月份更新,此篇文章为早期简单的封装使用的(简单快速使用使用,且混杂了 ES 的使用,建议查阅新的文章)
新文章地址: https://www.cnblogs.com/Alay/p/16902163.html ( 新文章中的方式,纯粹的 binlog 封装 ,且支持自定义扩展 )
此示例使用框架:mysql-binlog-connector-java  (https://github.com/shyiko/mysql-binlog-connector-java)
配置文件(Nacos)
spring: # MySQL 连接信息 datasource: host: ${MYSQL-HOST:mysql-host} port: ${MYSQL-PORT:3306} username: ${MYSQL-USER:root} password: ${MYSQL-PWD:root}
非 root 用户,启动是如果出现权限问题:Access denied; you need (at least one of) the SUPER, REPLICATION CLIENT .....
# REPLICATION CLIENT(客户端)、REPLICATION SLAVE(服务端)、SUPER(管理)和 RELOAD 四个权限最好在创建用户时就赋予,以免造成不必要的麻烦,当然,要是主主配置,最好给予ALL权限。 GRANT REPLICATION SLAVE, SUPER, RELOAD, REPLICATION CLIENT ON *.* TO user@'%'; 连接的用户 xxx_user
加入依赖:
<!-- MySQL binlog 日志监听 -->
<dependency>
   <groupId>com.github.shyiko</groupId>
   <artifactId>mysql-binlog-connector-java</artifactId>
   <version>${binlog.version}</version>
</dependency>
依赖更新为:2022-11-14
<properties> <!-- BinLog 连接组件版本 --> <zendesk.binlog.version>0.27.5</zendesk.binlog.version> </properties> <!-- 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>
入门简单案例讲解:
读取 BinLog 的主体程序:
/** * 监听MySQL binlog * CommandLineRunner SpringBoot启动后执行的代码(后置初始化) * * @author Alay * @date 2020-12-26 15:43 */ @Component public class BinLogRunner implements CommandLineRunner { @Value("${spring.datasource.host}") private String host; @Value("${spring.datasource.port}") private int port; @Value("${spring.datasource.username}") private String userName; @Value("${spring.datasource.password}") private String password; @Override public void run(String... args) throws Exception { // 客户端连接建立 BinaryLogClient logClient = new BinaryLogClient(host, port, userName, password); /* // 此配置为系列化处理, EventDeserializer eventDeserializer = new EventDeserializer(); eventDeserializer.setCompatibilityMode( EventDeserializer.CompatibilityMode.DATE_AND_TIME_AS_LONG, EventDeserializer.CompatibilityMode.CHAR_AND_BINARY_AS_BYTE_ARRAY ); logClient.setEventDeserializer(eventDeserializer); */ logClient.setServerId(1); logClient.registerEventListener(new BinLogEvent()); // 此处可能出现异常处理,AuthenticationException 因为 MySQL 访问密码加密协议的不同问题 logClient.connect(); } /** * 查询表结构 * 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_NAME = 'person_domain_user_stat' # 表名 * AND TABLE_schema = 'braineex' # 库名 */ class BinLogEvent implements BinaryLogClient.EventListener { @Override public void onEvent(Event event) { EventType eventType = event.getHeader().getEventType(); if (eventType == EventType.TABLE_MAP) { TableMapEventData tableData = event.getData(); System.out.println("tableId:" + tableData.getTableId()); System.out.println("库名:" + tableData.getDatabase()); System.out.println("表名:" + tableData.getTable()); /** * 字段名集合,位置:在event对象中 Event -> EventData -> TableMapEventData ->TableMapEventMetadata * SHOW VARIABLES LIKE '%BINLOG%' binlog_row_metadata=FULL 的时候才会 Binlog日志才会存在此值 * SET GLOBAL binlog_row_metadata='FULL' */ tableData.getEventMetadata().getColumnNames().forEach(System.out::println); } EventData eventData = event.getData(); if (null != eventData) { if (eventData instanceof DeleteRowsEventData) { System.out.println("删除操作"); DeleteRowsEventData deleteRowsEventData = (DeleteRowsEventData) eventData; } if (eventData instanceof UpdateRowsEventData) { System.out.println("修改操作"); UpdateRowsEventData updateRowsEventData = (UpdateRowsEventData) eventData; List<Map.Entry<Serializable[], Serializable[]>> rows = updateRowsEventData.getRows(); for (Map.Entry<Serializable[], Serializable[]> row : rows) { /** * 执行更行前 row 记录的所有值的 Array * 顺序和 event.getHeader() 中的 columnNames 字段名顺序一一对应 */ Serializable[] oldValues = row.getKey(); /**执行更行后 row 记录的所有的值 Array * 顺序和 event.getHeader() 中的 columnNames 字段名顺序一一对应 */ Serializable[] newValues = row.getValue(); } } if (eventData instanceof WriteRowsEventData) { System.out.println("写入操作"); WriteRowsEventData writeRowsEventData = (WriteRowsEventData) eventData; System.out.println(writeRowsEventData.getTableId()); } } } } }
MySQL 加密协议问题异常处理:GitHub上作者作者讨论 ( https://github.com/shyiko/mysql-binlog-connector-java/issues/240 )
2022-11-10 更新问题: 以上问题在变更依赖后不在出现:新的依赖源码地址: https://github.com/osheroff/mysql-binlog-connector-java
在MySQL 中执行查询语句:将MySQL 加密协议修改为 8.0 之前的:mysql_native_password,这仅仅是权宜之计,万全之策等作者更新新的协议,感兴趣的朋友可以持续追踪,作者后续会 对 8.0 之后的版本的加密协议进行兼容处理的,
暂时的权益之计:
ALTER USER '访问的用户名如:root' @'%' IDENTIFIED WITH mysql_native_password BY '密码如:root';
查看 MySQL 版本号:SELECT VERSION(); 查看当前数据库库名:SELECT DATABASE()
BinLog 的头信息中没有返回表的字段名,tableData.getEventMetadata().getColumnNames();
若需要字段名方案有二:
一:自行前往数据库中查询(推荐)
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` = 'behelpful' # 库名 AND `table_name` = 'person_information' # 表名 ORDER BY `ordinal_position` # 按表设计结构的顺序排序,从数字 1 开始
查询所有的
SELECT `table_schema`, #库名 `table_name`, # 表名 `engine`, # 引擎 `table_comment`, # 表注释 `table_collation`, # 表字符集及排序规则 `create_time` # 建表时间 FROM `information_schema`.`tables` WHERE `table_schema` = ( SELECT DATABASE() # 当前所在的库 ) ORDER BY `create_time` DESC
如:JDBC 查询,或者,通过以上SQL 语句字段自建一个对象,通过 Mybatis 方式查询(伪代码)
Connection connection = ... DatabaseMetaData metaData = connection.getMetaData(); ResultSet tableResultSet = metaData.getTables(null, "public", null, new String[]{"TABLE"}); try { while (tableResultSet.next()) { String tableName = tableResultSet.getString("TABLE_NAME"); ResultSet columnResultSet = metaData.getColumns(null, "public", tableName, null); try { while (columnResultSet.next()) { String columnName = columnResultSet.getString("COLUMN_NAME"); ... } } finally { columnResultSet.close(); } } } finally { tableResultSet.close(); }
方案二(实际项目中不推荐使用,测试时使用此方法,方便):
设置 SET GLOBAL binlog_row_metadata='FULL' (不推荐,因为通常我们并不需要全局,只是需要指定的个别数据库表),所以推荐使用查询,然后存储缓存
默认值:MINIMAL
SET GLOBAL binlog_row_metadata='FULL'; SHOW VARIABLES LIKE '%BINLOG%';

通过以上代码,根据 BinLog 的到的信息
头信息事件中得到:库 -> 表,及字段名,每次事件前必先触发一次头事件
其他事件,根据具体的业务需求进行封装成为具体的 Java 对象,然后使用很多方案可以实现 NoSQL 的数据同步,如使用 Kafka ,ActiveMQ,等都可以实现
整合到项目中:码云中有具体示例: https://gitee.com/chxlay/be-helpful behelpful-search 模块
@Data @ConfigurationProperties(prefix = "spring.datasource") public class MySqlConnection { private String host; private int port; private String userName; private String password; }
BinLog 监听器:
logClient.connect()执行时间太长,但是不影响正常的 Log 事件监听,所以此处使用异步处理 @Async 、@Order,避免耽搁 SpringBoot 启动时间
/** * 监听MySQL binlog * CommandLineRunner SpringBoot启动后执行的代码(后置初始化)*/ @Order @Component @AllArgsConstructor @EnableConfigurationProperties(value = MySqlConnection.class) public class BinLogRunner implements CommandLineRunner { /** * 监听器(这里只注册了一个监听器,然后在其中进行逻辑分发事件处理) */ private final ListenerAllocate listenerAllocate; private final MySqlConnection mySQLConnection; @Async @Order @Override public void run(String... args) throws Exception { EventDeserializer eventDeserializer = new EventDeserializer(); // 由于下面的自定义系列化需要反系列化对象的 tableMapEventByTableId 字段值,而此字段是私有的,所以通过反射拿 Field field = eventDeserializer.getClass().getDeclaredField("tableMapEventByTableId"); field.setAccessible(true); Map<Long, TableMapEventData> tableMapEventByTableId = (Map<Long, TableMapEventData>) field.get(eventDeserializer); // 自定义反系列化类 (读写更新) eventDeserializer.setEventDataDeserializer(EventType.EXT_WRITE_ROWS, new WriteRowsDeserializer(tableMapEventByTableId)); eventDeserializer.setEventDataDeserializer(EventType.EXT_UPDATE_ROWS, new UpdateRowsDeserializer(tableMapEventByTableId)); eventDeserializer.setEventDataDeserializer(EventType.EXT_DELETE_ROWS, new DeleteRowsDeserializer(tableMapEventByTableId)); BinaryLogClient logClient = new BinaryLogClient(mySQLConnection.getHost(), mySQLConnection.getPort(), mySQLConnection.getUserName(), mySQLConnection.getPassword()); logClient.setServerId(1); logClient.setEventDeserializer(eventDeserializer); // 监听器 logClient.registerEventListener(listenerAllocate); /** * 此处可能出现异常处理,AuthenticationException 因为 MySQL 访问密码加密协议的不同问题 * 关于此问题作者的交流讨论: https://github.com/shyiko/mysql-binlog-connector-java/issues/240 */ logClient.connect(); } }
具体的监听器,
BinLog 的事件触发顺序
启动服务时:
事件顺序:ROTATE、FORMAT_DESCRIPTION
启动后:(事件中有个共通的值,threadId,需要自行研究,反复启动,切换不同的库不停的增删改,修改结构等操作,此值均保持不变)
事件触发顺序:
每次操作只触发一次(批量操作也只会触发一次):ANONYMOUS_GTID,QUERY,(若是修改表结构操作,只触发此处的两个事件)
每条记录row触发一次独立的事件:TABLE_MAP(携带表、库、字段信息)、多选一:EXT_UPDATE_ROWS(执行更新),EXT_DELETE_ROWS,EXT_WRITE_ROWS
每次操作只触发一次(批量操作也只会触发一次)XID
/** * 负责分配 BinLog 事件到具体的处理类中的(管理者角色)*/ @Slf4j @Component public class ListenerAllocate extends AbsBinLogEvent { @Override protected void init() { } /** * 一次MySQL的修改、插入、删除,会触发多次事件,会调用方法多次,注意处理好逻辑优化性能 * 启动时: * 事件顺序:ROTATE、FORMAT_DESCRIPTION * </br> * 启动后: * 事件触发顺序: * 每次操作只触发一次(批量操作也只会触发一次):ANONYMOUS_GTID,QUERY, * 每条记录row 触发一次独立的事件:TABLE_MAP(携带表库字段信息)、多选一:EXT_UPDATE_ROWS(执行更新),EXT_DELETE_ROWS,EXT_WRITE_ROWS * 每次操作只触发一次(批量操作也只会触发一次) XID * * @param event */ @Override public void onEvent(Event event) { EventType eventType = event.getHeader().getEventType(); // 只处理我想要处理的事件 if (eventType != EventType.QUERY && eventType != EventType.TABLE_MAP && eventType != EventType.EXT_UPDATE_ROWS && eventType != EventType.EXT_DELETE_ROWS && eventType != EventType.EXT_WRITE_ROWS) { return; } EventData eventData = event.getData(); /** * 此事件操作的是修改表结构,修改后需要将缓存中存储的表结构删除 * 解析得出修改表结构的 sql 语句进行解析 * sql='ALTER TABLE `database_name(数据库的名称)`.`person_information`\r\n后面是具体的执行语句 */ if (EventType.QUERY == eventType) { QueryEventData queryEventData = (QueryEventData) eventData; String sql = queryEventData.getSql(); // 增、删、改的事件,不做处理 if (sql.startsWith(SearchConstants.EVENT_SQL_BEGIN)) { return; } // 修改表结构的事件 String tableInfo = sql.substring(12); // 注意表名后面有一个空格需要去除 tableInfo = StrUtil.subBefore(tableInfo, " \r\n", false); tableInfo = tableInfo.replace("`", ""); String[] tableArr = tableInfo.split("\\."); // 移除缓存 sqlSchemaColumnService.removeTableColumn(tableArr[0], tableArr[1]); return; } /** * 表结构映射事件 */ if (eventType == EventType.TABLE_MAP) { TableMapEventData mapEventData = (TableMapEventData) eventData; long tableId = mapEventData.getTableId(); // 获取头文件中存储的 tableId <---> tableName 的映射 String tableFullName = tableFullNameMap.get(tableId); if (null != tableFullName) { // 此表已经不是第一次触发该事件了,不需要重复的处理做准备的工作 return; } if (null == tableFullName) { String tableName = mapEventData.getTable(); // 此事件不是我要处理的数据库表的日志事件,不在我的事件实例中,不做处理 if (null == eventInstanceMap.get(tableName)) { return; } String database = mapEventData.getDatabase(); tableFullName = database + "." + tableName; tableFullNameMap.put(tableId, tableFullName); return; } } /** * 触发的事件是具体操作的事件 */ if (eventType == EventType.EXT_UPDATE_ROWS || eventType == EventType.EXT_WRITE_ROWS || eventType == EventType.EXT_DELETE_ROWS) { try { // 通过反射获取到事件对象数据的 tableId的值 Field tableIdField = eventData.getClass().getDeclaredField("tableId"); tableIdField.setAccessible(true); long tableId = (long) tableIdField.get(eventData); String tableFullName = tableFullNameMap.get(tableId); // 不是我想要监听的数据库表的的事件,不做处理 if (null == tableFullName) { return; } // 通过表名 获得具体是时间处理类对象 tableName <----> eventInstance String[] tableNameArr = tableFullName.split("\\."); AbsBinLogEvent eventInstance = eventInstanceMap.get(tableNameArr[1]); // 调用具体的事件逻辑处理 eventInstance.onEvent(event); } catch (NoSuchFieldException | IllegalAccessException e) { log.error("执行 BinLog 同步数据失败,时间类型{},原因:{},Msg:{}", eventType.toString(), e.getCause(), e.getMessage()); } } } }
补充:QUERY 事件的补充
修改表结构中触发的 BinLog 事件如下:(sql='ALTER TABLE `behelpful`.`person_information,修改表事件, sql 信息会携带 修改的语句)
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 `braineex`.`person_information` ------------>> 修改表结构事件 MODIFY COLUMN `nick_name` varchar(31) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称' AFTER `age`' } }
增、删、改 触发的 QUERY 中 sql = ' BEGIN ' ,数据增删改 触发的BinLog 事件中, sql 不会携带执行的 sql 语句,而是 为 BEGIN
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='braineex', sql='BEGIN' } }
解析 BinLog 事件的具体类 (抽象类,以上匿名内部类的方式将其添加到具体处理),日志解析得到具体的 row -> Java 对象以后使用任何方式操作 同步均可
/** * 具体的 BinLog 事件增删改事件处理的逻辑代码和公共的变量管理存储*/ public abstract class AbsBinLogEvent<T extends ISearchModel> implements BinaryLogClient.EventListener { /** * Map< tableId, MySQL表名> */ protected volatile static Map<Long, String> tableFullNameMap = new HashMap<>(1 << 3); /** * Map< tableId, MySQL表对应 Java 实体类的类对象 Class> */ protected volatile static Map<String, AbsBinLogEvent> eventInstanceMap = new HashMap<>(1 << 4); protected ISearchService<T> baseEsService; @Autowired protected ActionListener updateListener; @Autowired protected ActionListener indexListener; @Autowired protected ActionListener deleteListener; @Autowired protected SqlSchemaColumnService sqlSchemaColumnService; /** * 初始化需要处理的表的事件处理类 * 初始化变量,及注入 * * @throws BaseRuntimeException */ @PostConstruct protected abstract void init() throws BaseRuntimeException; @Override public void onEvent(Event event) { EventData eventData = event.getData(); /** * 修改操作 */ if (eventData instanceof UpdateRowsEventData) { T entity = this.updateEvent(eventData); // 同步 ES 的操作 T esEntity = baseEsService.selectById(entity.getEsId()); if (null == esEntity) { baseEsService.saveEntityAsy(entity, indexListener); } else { baseEsService.updateByIdAsy(entity, updateListener); } return; } /** * 写入操作 */ if (eventData instanceof WriteRowsEventData) { T entity = this.saveEvent(eventData); // 写入数据 baseEsService.saveEntityAsy(entity, indexListener); return; } /** * 删除操作 */ if (eventData instanceof DeleteRowsEventData) { T entity = this.deleteEvent(eventData); // 删除 ES 中数据 baseEsService.deleteByIdAsy(entity.getEsId(), deleteListener); return; } } protected T saveEvent(EventData eventData) { WriteRowsEventData writeRowsEventData = (WriteRowsEventData) eventData; List<Serializable[]> rows = writeRowsEventData.getRows(); T entity = this.rowsToEntity(rows, writeRowsEventData.getTableId()); return entity; } protected T updateEvent(EventData eventData) { UpdateRowsEventData updateRowsEventData = (UpdateRowsEventData) eventData; // rows 每一个 Entry 是条记录,其中 Key 为修改前的记录,Value 为修改后的新的记录 List<Map.Entry<Serializable[], Serializable[]>> rows = updateRowsEventData.getRows(); // 获取修改后的新的值 List<Serializable[]> newValues = rows.stream().map(entry -> entry.getValue()).collect(Collectors.toList()); // 将修改后的新的值转成 Java 对象 T entity = this.rowsToEntity(newValues, updateRowsEventData.getTableId()); return entity; } protected T deleteEvent(EventData eventData) { DeleteRowsEventData deleteRowsEventData = (DeleteRowsEventData) eventData; List<Serializable[]> rows = deleteRowsEventData.getRows(); T entity = this.rowsToEntity(rows, deleteRowsEventData.getTableId()); return entity; } protected T rowsToEntity(List<Serializable[]> rows, Long tableId) { String tableFullName = tableFullNameMap.get(tableId); String[] tableNameArr = tableFullName.split("\\."); // 获得当前 row 的数据库中对应的字段名称 String[] columnNames = sqlSchemaColumnService.columnsByTable(tableNameArr[0], tableNameArr[1]); JSONObject beanJSON = new JSONObject(); for (Serializable[] row : rows) { for (int i = 0; i < row.length; i++) { beanJSON.put(columnNames[i], row[i]); } } T entity = this.jsonToBean(beanJSON); return entity; } protected T jsonToBean(JSONObject beanJSON) { T entity = JSON.toJavaObject(beanJSON, this.entityClass()); return entity; } /** * 获取调用方法实现类中泛型的具体类对象 * * @return */ protected Class<T> entityClass() { // 当前调用方法的 Impl实现类的父类的类型 ParameterizedType superclass = (ParameterizedType) this.getClass().getGenericSuperclass(); // 当前调用方法的 Impl实现类的泛型的类型,实现类必须带泛型,否则报错 Type[] type = superclass.getActualTypeArguments(); Class clazz = (Class) type[0]; return clazz; } /** * 获取实体类映射的数据库表名称 * * @param entityClazz * @return */ protected String entityTableName(Class<?> entityClazz) { boolean isAnno = entityClazz.isAnnotationPresent(TableName.class); if (isAnno) { TableName annotation = entityClazz.getAnnotation(TableName.class); // 表名 @TableName(value = "person_information") return annotation.value(); } throw new BaseRuntimeException("操作不允许", "ERROR"); } }
处理表结构的查询、缓存的 Service 实现类,方案中使用了 MP(以上说所的方案 一):
/** * 查询获得数据库中表结构*/ @Service public class SqlSchemaColumnServiceImpl extends ServiceImpl<SqlSchemaColumnMapper, SqlSchemaColumn> implements SqlSchemaColumnService { @Autowired private IRedisUtil iRedisUtil; /** * 此方法不可类内部调用,否则使用AOP处理Jedis将不会关闭 * 保证字段的顺序,故用RedisZSet 进行存储 * * @param database * @param tableName * @return */ @ThreadJedis @Override public String[] columnsByTable(String database, String tableName) { String key = CacheEnum.SQL_SCHEMA.key + database + "." + tableName; Jedis jedis = iRedisUtil.threadJedis(); jedis.select(CacheEnum.SQL_SCHEMA.index); Boolean exists = jedis.exists(key); String[] columnNames; if (exists) { // 集合中所有成员数 Long total = jedis.zcard(key); columnNames = new String[total.intValue()]; for (int i = 0; i < total; i++) { Set<String> columns = jedis.zrange(key, i, i); String column = columns.iterator().next(); columnNames[i] = column; } } else { // 查询数据表结构存入缓存 List<SqlSchemaColumn> schemaColumns = this.list(Wrappers.<SqlSchemaColumn>lambdaQuery() .eq(SqlSchemaColumn::getTableSchema, database) .eq(SqlSchemaColumn::getTableName, tableName) .orderByAsc(SqlSchemaColumn::getOrdinalPosition)); List<String> columns = schemaColumns.stream().map(SqlSchemaColumn::getColumnName).collect(Collectors.toList()); columnNames = new String[columns.size()]; for (int i = 0; i < columns.size(); i++) { columnNames[i] = columns.get(i); // 存入缓存 jedis.zadd(key, i + 1, columns.get(i)); } } return columnNames; } @ThreadJedis @Override public boolean removeTableColumn(String database, String tableName) { String key = CacheEnum.SQL_SCHEMA.key + database + "." + tableName; Jedis jedis = iRedisUtil.threadJedis(); jedis.select(CacheEnum.SQL_SCHEMA.index); Long del = jedis.del(key); return del > 0; } }
数据库表结构对应的 实体类:
/** * MySQL 表机构 */ @Data @EqualsAndHashCode(callSuper = true) @TableName(value = "`information_schema`.`columns`") public class SqlSchemaColumn extends Model<SqlSchemaColumn> { private static final long serialVersionUID = 1L; /** * 数据库名称 */ private String tableSchema; /** * 数据表名 */ private String tableName; /** * 字段名 */ private String columnName; private String ordinalPosition; private String columnDefault; private String isNullable; private String dataType; private String characterMaximumLength; private String characterOctetLength; private String numericPrecision; private String numericScale; private String characterSetName; private String collationName; }
补充:
由于 数据库中我使用的是 Bit 类型记录 Java 对象中 Boolean 类型,导致时间中 EvenData 中返回的 数据 Serializable[] rows 对应的字段为 BitSet 类型,
需要自定义 凡系列化规则,官方文档中有相应的解释,怎样自定义反系列化规则(伪代码)
EventDeserializer eventDeserializer = new EventDeserializer(); // do not deserialize EXT_DELETE_ROWS event data, return it as a byte array eventDeserializer.setEventDataDeserializer(EventType.EXT_DELETE_ROWS, new ByteArrayEventDataDeserializer()); // skip EXT_WRITE_ROWS event data altogether eventDeserializer.setEventDataDeserializer(EventType.EXT_WRITE_ROWS, new NullEventDataDeserializer()); // use custom event data deserializer for EXT_DELETE_ROWS eventDeserializer.setEventDataDeserializer(EventType.EXT_DELETE_ROWS, new EventDataDeserializer() { ... }); BinaryLogClient client = ... client.setEventDeserializer(eventDeserializer);
通过源码追踪得出反系列化多数代码在 AbstractRowsEventDataDeserializer 抽象类中
其有三个子类:
DeleteRowsEventDataDeserializer ( 作用于删除事件 EventType.EXT_DELETE_ROWS)
UpdateRowsEventDataDeserializer ( 作用于更新时间 EventType.EXT_UPDATE_ROWS)
WriteRowsEventDataDeserializer  ( 作用于删除事件 EventType.EXT_WRITE_ROWS)
由于我并不需要大面积的重写 以上三个类的反系列化规则,我仅仅需要方反系列化规则 MySQL 中的 Bit 反系列化为 Boolean 类型,
所以,我采用直接粗暴的继承 以上三个类,
重写 AbstractRowsEventDataDeserializer 中的 deserializeBit(int meta,ByteArrayInputStream inputStream) 即可

以 EXT_UPDATE_ROWS 事件为例,其余两个同理:
原方法:
protected Serializable deserializeBit(int meta, ByteArrayInputStream inputStream) throws IOException { int bitSetLength = (meta >> 8) * 8 + (meta & 0xFF); return inputStream.readBitSet(bitSetLength, false); }
重写后,只需要细微的改动,返回值从 BitSet 替换为 Boolean 即可
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(); Boolean booleanValue = Boolean.valueOf(cardinality == 1); return booleanValue; } }
具体示例,可前往 本人的 码云 中查看: https://gitee.com/chxlay/be-helpful
本文来自博客园,作者:Vermeer,转载请注明原文链接:https://www.cnblogs.com/chxlay/p/15115543.html
 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号