数据的异构实战(一) 基于canal进行日志的订阅和转换
什么是数据的异构处理。简单说就是为了满足我们业务的扩展性,将数据从某种特定的格式转换到新的数据格式中来。
为什么会有这种需求出现呢?
传统的企业中,主要都是将数据存储在了关系型数据库中,例如说MySQL这种数据库,但是为了满足需求的扩展,查询的维度会不断地增加,那么这个时候我们就需要做数据的异构处理了。
常见的数据异构有哪些?
例如MySQL数据转储到Redis,MySQL数据转储到es等等,也是因为这种数据异构的场景开始出现,陆陆续续有了很多中间件在市场中冒出,例如说rocketMq,kafka,canal这种组件。
下边有一张通俗易懂的数据异构过程图:
canal进行数据同步
首先,我们需要正确地打开canal服务器去订阅binlog日志。
关于binlog日志查看常用的几条命令如下:
#是否启用了日志 mysql>show variables like 'log_bin'; #怎样知道当前的日志 mysql> show master status; #查看mysql binlog模式 show variables like 'binlog_format'; #获取binlog文件列表 show binary logs; #查看当前正在写入的binlog文件 show master status\G #查看指定binlog文件的内容 show binlog events in 'mysql-bin.000002';
注意binlog日志格式要求为row格式:
ROW格式日志的特点
记录sql语句和每个字段变动的前后情况,能够清楚每行数据的变化历史,占用较多的空间,不会记录对数据没有影响的sql,例如说select语句就不会记录。可以使mysqlbinlog工具去查看内部信息。
STATEMENT模式的日志内容
STATEMENT格式的日志就和它本身的命名有点类似,只是单独地记录了sql的内容,但是没有记录上下文信息,在数据会UI福的时候可能会导致数据丢失。
MIX模式模式的日志内容
这种模式的日志内容比较灵活,当遇到了表结构变更的时候,就会记录为statement模式,如果遇到了数据修改的话就会变为row模式。
如何配置canal的相关信息?
比较简单,首先通过下载好canal的安装包,然后我们需要在canal的配置文件上边做一些手脚:
canal的example文件夹下边的properties文件 canal.instance.master.address=**.***.***.**:3306 # 日志的文件名称 canal.instance.master.journal.name=master-96-bin.000009 canal.instance.dbUsername=**** canal.instance.dbPassword=****
启动我们的canal程序,然后查看日志,如果显示下边这些内容就表示启动成功了:
2019-10-13 16:00:30.072 [main] ERROR com.alibaba.druid.pool.DruidDataSource - testWhileIdle is true, validationQuery not set 2019-10-13 16:00:30.734 [main] INFO c.a.otter.canal.instance.spring.CanalInstanceWithSpring - start CannalInstance for 1-example 2019-10-13 16:00:30.783 [main] INFO c.a.otter.canal.instance.core.AbstractCanalInstance - start successful....
ps:关于canal入门安装的教程网上有很多,这里我就不做过多的阐述了。
canal服务器搭建起来之后,我们便进入了java端的程序编码部分:
接着再来查看我们的客户端代码,客户端中我们需要通过java程序获取canal服务器的连接,然后进入监听binlog日志的状态。
可以参考下边的程序代码:
package com.sise.client.simple; import com.alibaba.otter.canal.client.CanalConnector; import com.alibaba.otter.canal.client.CanalConnectors; import com.alibaba.otter.canal.protocol.CanalEntry; import com.alibaba.otter.canal.protocol.Message; import com.google.protobuf.InvalidProtocolBufferException; import com.sise.common.dto.TypeDTO; import com.sise.common.handle.CanalDataHandler; import java.net.InetSocketAddress; import java.util.List; import java.util.stream.Collectors; /** * 简单版本的canal监听客户端 * * @author idea * @date 2019/10/12 */ public class SImpleCanalClient { private static String SERVER_ADDRESS = "127.0.0.1"; private static Integer PORT = 11111; private static String DESTINATION = "example"; private static String USERNAME = ""; private static String PASSWORD = ""; public static void main(String[] args) throws InterruptedException { CanalConnector canalConnector = CanalConnectors.newSingleConnector( new InetSocketAddress(SERVER_ADDRESS, PORT), DESTINATION, USERNAME, PASSWORD); canalConnector.connect(); canalConnector.subscribe(".*\\..*"); canalConnector.rollback(); for (; ; ) { Message message = canalConnector.getWithoutAck(100); long batchId = message.getId(); if(batchId!=-1){ // System.out.println(message.getEntries()); System.out.println(batchId); printEntity(message.getEntries()); } } } public static void printEntity(List<CanalEntry.Entry> entries){ for (CanalEntry.Entry entry : entries) { if (entry.getEntryType()!=CanalEntry.EntryType.ROWDATA){ continue; } try { CanalEntry.RowChange rowChange=CanalEntry.RowChange.parseFrom(entry.getStoreValue()); for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) { System.out.println(rowChange.getEventType()); switch (rowChange.getEventType()){ //如果希望监听多种事件,可以手动增加case case INSERT: String tableName = entry.getHeader().getTableName(); //测试选用t_type这张表进行映射处理 if ("t_type".equals(tableName)) { TypeDTO typeDTO = CanalDataHandler.convertToBean(rowData.getAfterColumnsList(), TypeDTO.class); System.out.println(typeDTO); } System.out.println("this is INSERT"); break; default: break; } } } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } } /** * 打印内容 * * @param columns */ private static void printColums(List<CanalEntry.Column> columns){ String line=columns.stream().map(column -> column.getName()+"="+column.getValue()) .collect(Collectors.joining(",")); System.out.println(line); } }
本地监听到了canal的example文件夹中配置的监听的日志信息之后,就会自动将该日志里面记录的数据进行打印读取。
那么这个时候我们还需要做多一步处理,那就是将坚听到的数据转换为可识别的对象,然后进行对象转移处理。
其实光是链接获取到canal的binlog日志并不困难,接着我们还需要将binlog日志进行统一的封装处理,需要编写一个特定的处理器将日志的内容转换为我们常用的DTO类:
下边这个工具类可以借鉴一下:
package com.sise.common.handle; import com.alibaba.otter.canal.protocol.CanalEntry; import com.sise.common.dto.CourseDetailDTO; import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Field; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 基于canal的数据处理器 * * @author idea * @data 2019/10/13 */ @Slf4j public class CanalDataHandler extends TypeConvertHandler { /** * 将binlog的记录解析为一个bean对象 * * @param columnList * @param clazz * @param <T> * @return */ public static <T> T convertToBean(List<CanalEntry.Column> columnList, Class<T> clazz) { T bean = null; try { bean = clazz.newInstance(); Field[] fields = clazz.getDeclaredFields(); Field.setAccessible(fields, true); Map<String, Field> fieldMap = new HashMap<>(fields.length); for (Field field : fields) { fieldMap.put(field.getName().toLowerCase(), field); } if (fieldMap.containsKey("serialVersionUID")) { fieldMap.remove("serialVersionUID".toLowerCase()); } System.out.println(fieldMap.toString()); for (CanalEntry.Column column : columnList) { String columnName = column.getName(); String columnValue = column.getValue(); System.out.println(columnName); if (fieldMap.containsKey(columnName)) { //基础类型转换不了 Field field = fieldMap.get(columnName); Class<?> type = field.getType(); if(BEAN_FIELD_TYPE.containsKey(type)){ switch (BEAN_FIELD_TYPE.get(type)) { case "Integer": field.set(bean, parseToInteger(columnValue)); break; case "Long": field.set(bean, parseToLong(columnValue)); break; case "Double": field.set(bean, parseToDouble(columnValue)); break; case "String": field.set(bean, columnValue); break; case "java.handle.Date": field.set(bean, parseToDate(columnValue)); break; case "java.sql.Date": field.set(bean, parseToSqlDate(columnValue)); break; case "java.sql.Timestamp": field.set(bean, parseToTimestamp(columnValue)); break; case "java.sql.Time": field.set(bean, parseToSqlTime(columnValue)); break; } }else{ field.set(bean, parseObj(columnValue)); } } } } catch (InstantiationException | IllegalAccessException e) { log.error("[CanalDataHandler]convertToBean,初始化对象出现异常,对象无法被实例化,异常为{}", e); } return bean; } public static void main(String[] args) throws IllegalAccessException { CourseDetailDTO courseDetailDTO = new CourseDetailDTO(); Class clazz = courseDetailDTO.getClass(); Field[] fields = clazz.getDeclaredFields(); Field.setAccessible(fields, true); System.out.println(courseDetailDTO); for (Field field : fields) { if ("java.lang.String".equals(field.getType().getName())) { field.set(courseDetailDTO, "name"); } } System.out.println(courseDetailDTO); } /** * 其他类型自定义处理 * * @param source * @return */ public static Object parseObj(String source){ return null; } }
接着是canal的核心处理器,主要的目的是将binlog转换为我们所希望的实体类对象,该类目前主要考虑兼容的数据类型为目前8种,比较有限,如果读者后续在实际开发中还遇到某些特殊的数据类型可以手动添加到map中。
package com.sise.common.handle; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 类型转换器 * * @author idea * @data 2019/10/13 */ public class TypeConvertHandler { public static final Map<Class, String> BEAN_FIELD_TYPE; static { BEAN_FIELD_TYPE = new HashMap<>(8); BEAN_FIELD_TYPE.put(Integer.class, "Integer"); BEAN_FIELD_TYPE.put(Long.class, "Long"); BEAN_FIELD_TYPE.put(Double.class, "Double"); BEAN_FIELD_TYPE.put(String.class, "String"); BEAN_FIELD_TYPE.put(Date.class, "java.handle.Date"); BEAN_FIELD_TYPE.put(java.sql.Date.class, "java.sql.Date"); BEAN_FIELD_TYPE.put(java.sql.Timestamp.class, "java.sql.Timestamp"); BEAN_FIELD_TYPE.put(java.sql.Time.class, "java.sql.Time"); } protected static final Integer parseToInteger(String source) { if (isSourceNull(source)) { return null; } return Integer.valueOf(source); } protected static final Long parseToLong(String source) { if (isSourceNull(source)) { return null; } return Long.valueOf(source); } protected static final Double parseToDouble(String source) { if (isSourceNull(source)) { return null; } return Double.valueOf(source); } protected static final Date parseToDate(String source) { if (isSourceNull(source)) { return null; } if (source.length() == 10) { source = source + " 00:00:00"; } SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date; try { date = sdf.parse(source); } catch (ParseException e) { return null; } return date; } protected static final java.sql.Date parseToSqlDate(String source) { if (isSourceNull(source)) { return null; } SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); java.sql.Date sqlDate; Date utilDate; try { utilDate = sdf.parse(source); } catch (ParseException e) { return null; } sqlDate = new java.sql.Date(utilDate.getTime()); return sqlDate; } protected static final java.sql.Timestamp parseToTimestamp(String source) { if (isSourceNull(source)) { return null; } SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date; java.sql.Timestamp timestamp; try { date = sdf.parse(source); } catch (ParseException e) { return null; } timestamp = new java.sql.Timestamp(date.getTime()); return timestamp; } protected static final java.sql.Time parseToSqlTime(String source) { if (isSourceNull(source)) { return null; } SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); Date date; java.sql.Time time; try { date = sdf.parse(source); } catch (ParseException e) { return null; } time = new java.sql.Time(date.getTime()); return time; } private static boolean isSourceNull(String source) { if (source == "" || source == null) { return true; } return false; } }
ps: t_type表是一张我们用于做测试时候使用的表,这里我们可以根据自己实际的业务需要定制不同的实体类对象
现在我们已经可以通过binlog转换为实体类了,那么接下来就是如何将实体类做额外的传输和处理了。数据的传输我们通常会借助mq这类型的中间件来进行操作,关于这部分的内容我会在后续的文章中做详细的输出。