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

posted @ 2021-08-08 17:45  Vermeer  阅读(340)  评论(0)    收藏  举报