在业务系统中,数据不光要保存到关系型数据库中,部分业务数据还要同步保存到Redis、ES、HBASE中。
像秒杀的业务,我们会将数据的库存缓存到Redis中。这个时候,我们对数据库中订单的增加或者减少,都需要同步响应的库存数据到Redis中。这种数据同步的代码跟业务的代码耦合度太高会不太优雅,此时我们可以把这些同步数据的操作,单独出来形成一个独立的模块。
上一篇笔记中,介绍到canal.deployer
+ springboot 项目启动日志监听的操作。这次我们用过使用canal控制台去做一些数据库的配置以及rocketmq的配置。
canal官网站下载canal.admin
我还是之前下载好的版本canal.admin-1.1.5.tar.gz
下载好后,完成解压缩。我们最终是要通过一个web页面去配置canal server ,会很方便,还可以配置选择集群的搭建。
打开安装路径{path}\canal-admin\conf\application.yml
文件并修改。
server:
port: 8089
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
spring.datasource:
address: 127.0.0.1:3306
database: canal_manager
username: root
password: 12345678
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false
hikari:
maximum-pool-size: 30
minimum-idle: 1
canal:
adminUser: admin
adminPasswd: admin
其中 database 为什么要配置为 canal_manager
,下面要说。
因为我们要通过canal的web页面去配置,所以,需要初始化管理页面的元数据。
元数据存放的位置:
{path}\canal-admin\conf\canal_manager.sql
有2种方式初始化元数据。
database:canal_manager
的数据库(建议使用拥有root权限的用户去操作,本地搭建的肯定是拥有root权限的用户)>source conf/canal_manager.sql
)初始化元数据完成后,我们通过{path}\canal-admin\bin\startup.bat
启动。
启动成功后,浏览器访问:
http://ip:8090/
会跳转到登录页面 通过admin/123456
进行登录,成功的页面如下所示:
进入到{path}\canal\conf\
这次我们先不修改canal.properties
,而是通过修改canal_local.properties的配置去进行覆盖,修改如下:
# register ip
canal.register.ip =
# canal admin config
canal.admin.manager = 127.0.0.1:8089
canal.admin.port = 11110
canal.admin.user = admin
canal.admin.passwd = 4ACFE3202A5FF5CF467898FC58AAB1D615029441
# admin auto register
canal.admin.register.auto = true
canal.admin.register.cluster =
canal.admin.register.name =
配置好后,我们需要启动canal server,这个时候我们需要注意下,因为现在是使用的canal_local.properties
的配置。所以,我们需要修改下启动文件bin\startup.bat
。
需要替换的位置如下:
这个步骤操作完,就可以启动了。
启动成功后,我们在canal.admin web 控制台刷新server管理,就可以看到canal server已经启动成功。
到这步,我们的canal.server搭建就已经成功了。
instance 名称
、所属集群/主机
。然后我们将之前配置好的{安装路径}\canal\conf\example\instance.properties
的文件,对照修改下。
然后保存即可。
我们编辑修改刚才创建好的Instance实例。修改配置下面这个部分,这个地方的配置,可以配置监听数据库全部表作为mq topic的名称,或者直接指定一个固定的表,作为MQtopic。
我这里是配置了指定了其中的一个表。
到这里,canal指定mq的配置完成,保存退出。
我们来测试下,是否有效。我对已经存在的表做了个insert的操作,如下:
此时,我们看下RocketMq的控制台,查看Topic是否自动创建了我们自定义配置的topic。
如图所示,topic已经创建完成。此时,我们在看下,canal给rocketMq发送的消息,是不是我们上面操作的Insert的操作。
没有问题,已经发送了消息,看看是不是我们操作的那个。
具体内容:
{
"data": [{
"RECORD_ID": "1000000004",
"TYPE_CODE": "T_DICT",
"PARAM_CODE": "STATUS",
"TYPE_CODE_NAME": "系统基础字典值类型",
"PARAM_CODE_NAME": "是否有效",
"STATUS": "VALID",
"CREATE_BY": "1",
"CREATE_DATE": "2021-07-08 20:11:44",
"UPDATE_BY": null,
"UPDATE_DATE": null
}],
"database": "mrsoftrock",
"es": 1625746304000,
"id": 3,
"isDdl": false,
"mysqlType": {
"RECORD_ID": "bigint",
"TYPE_CODE": "varchar(50)",
"PARAM_CODE": "varchar(50)",
"TYPE_CODE_NAME": "varchar(50)",
"PARAM_CODE_NAME": "varchar(1000)",
"STATUS": "varchar(10)",
"CREATE_BY": "bigint",
"CREATE_DATE": "timestamp",
"UPDATE_BY": "bigint",
"UPDATE_DATE": "timestamp"
},
"old": null,
"pkNames": ["RECORD_ID"],
"sql": "",
"sqlType": {
"RECORD_ID": -5,
"TYPE_CODE": 12,
"PARAM_CODE": 12,
"TYPE_CODE_NAME": 12,
"PARAM_CODE_NAME": 12,
"STATUS": 12,
"CREATE_BY": -5,
"CREATE_DATE": 93,
"UPDATE_BY": -5,
"UPDATE_DATE": 93
},
"table": "dict_desc",
"ts": 1625746304528,
"type": "INSERT"
}
也没问题,到这里,说明我们canal已经成功的监听到了MySQL的binlog的变化,并将操作直接投递发送到RocketMq中。
那么,剩下的操作,就是我们程序中监听MQ的消息,拿到message body数据,并操作。
mq接收消息对象:
package com.nacos.test.controller.mq.bean;
import lombok.*;
import lombok.experimental.FieldDefaults;
import java.util.List;
/**
* @author Mr.SoftRock
* @Date 2021/7/8 20:40
**/
@Getter
@Setter
@FieldDefaults(level = AccessLevel.PRIVATE)
@AllArgsConstructor
@NoArgsConstructor
public class CanalBean {
/**
* 数据
*/
List<DictDesc> data;
/**
* 数据库名称
*/
String database;
long es;
/**
* 递增,从1开始
*/
int id;
/**
* 是否是DDL语句
*/
boolean isDdl;
/**
* 表结构的字段类型
*/
MysqlType mysqlType;
/**
* UPDATE语句,旧数据
*/
List<DictDesc> old;
/**
* 主键名称
*/
List<String> pkNames;
/**
* sql语句
*/
String sql;
SqlType sqlType;
/**
* 表名
*/
String table;
long ts;
/**
* (新增)INSERT、(更新)UPDATE、(删除)DELETE、(删除表)ERASE等等
*/
String type;
}
package com.nacos.test.controller.mq.bean;
import lombok.*;
import lombok.experimental.FieldDefaults;
import java.util.Date;
/**
* @author Mr.SoftRock
* @Date 2021/7/8 20:45
**/
@Getter
@Setter
@FieldDefaults(level = AccessLevel.PRIVATE)
@AllArgsConstructor
@NoArgsConstructor
public class DictDesc {
Long recordId;
String typeCode;
String paramCode;
String tpeCodeName;
String paramCodeName;
String status;
Long createBy;
Date createDate;
Long updateBy;
Date updateDate;
}
package com.nacos.test.controller.mq.bean;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* @author Mr.SoftRock
* @Date 2021/7/8 20:44
**/
@Getter
@Setter
@FieldDefaults(level = AccessLevel.PRIVATE)
@AllArgsConstructor
@NoArgsConstructor
public class MysqlType {
int id;
int commodity_name;
int commodity_price;
int number;
int description;
}
package com.nacos.test.controller.mq.bean;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* @author Mr.SoftRock
* @Date 2021/7/8 20:43
**/
@Getter
@Setter
@FieldDefaults(level = AccessLevel.PRIVATE)
@AllArgsConstructor
@NoArgsConstructor
public class SqlType {
int id;
int commodity_name;
int commodity_price;
int number;
int description;
}
监听接收消息类:
package com.nacos.test.controller.mq;
import com.alibaba.fastjson.JSONObject;
import com.nacos.test.controller.mq.bean.CanalBean;
import com.nacos.test.controller.mq.bean.DictDesc;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.apache.rocketmq.spring.core.RocketMQPushConsumerLifecycleListener;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* @author Mr.SoftRock
* @Date 2021/07/08 20:33
**/
@Service
@Slf4j
@RocketMQMessageListener(consumerGroup = "canal-group",
topic = "mrsoftrock_dict_desc",
selectorExpression = "*",
consumeMode = ConsumeMode.ORDERLY,
consumeThreadMax = 1
)
public class CanalListener implements RocketMQListener<MessageExt>, RocketMQPushConsumerLifecycleListener {
@Override
public void onMessage(MessageExt message) {
log.info("---consume canal message begin");
try {
log.info(new String(message.getBody(), StandardCharsets.UTF_8));
} catch (Exception e) {
e.printStackTrace();
}
log.info("---consume canal message end");
}
@Override
public void prepareStart(DefaultMQPushConsumer consumer) {
try {
consumer.subscribe("mrsoftrock_dict_desc", "*");
} catch (Exception e) {
e.printStackTrace();
}
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
log.info("消费消息-mrsoftrock_dict_desc--consume order message begin");
try {
String message = new String(msgs.get(0).getBody(), StandardCharsets.UTF_8);
log.info("消费消息==>msgId:{},message:{}", msgs.get(0).getMsgId(), message);
CanalBean canalBean = JSONObject.parseObject(message, CanalBean.class);
String table = canalBean.getTable();
System.out.println(table.toString());
String type = canalBean.getType();
System.out.println(type);
List<DictDesc> data = canalBean.getData();
data.stream().forEach(tbTest -> {
log.info("获取到的数据库操作数据:---》" + JSONObject.toJSONString(tbTest));
if ("UPDATE".equals(type) && "dict_desc".equals(table)) {
//删除缓存
log.info("执行redis的操作---UPDATE");
} else if ("INSERT".equals(type) && "dict_desc".equals(table)) {
//添加缓存
log.info("执行redis的操作---INSERT");
}
});
} catch (Exception e) {
e.printStackTrace();
}
log.info("消费消息-mrsoftrock_dict_desc--consume order message end");
return ConsumeOrderlyStatus.SUCCESS;
});
}
}
我们启动下我们的springboot项目,然后看控制台的输出:
Redis的操作,我就不展开说了,随便写个set,get操作,验证下即可。
canal对MySQL的监听,然后发送到mq的过程中,会不会有顺序性的问题出现。
那么官方也分了几种不同的情况去处理,官方对rocketMq接受canal顺序消息的处理。也可直接看下图:
第一句很明显,binlog本身是有序的,那么就剩下控制,canal发送消息到mq之后
,如何保证顺序了。
至此,对阿里开源中间件canal的初步学习就完成了。不得不说,阿里的开源产品,做的确实可圈可点。
但是没有任何一项技术是完美无缺的,适合自己的才是最好的。