大家好,我是孙嵓,今天这篇文章可有点东西啊,我觉得这个场景你司估计也用得到,其实这个功能不归我管,但我实在是看不下去同事对这个业务的操作,居然写了个定时任务每隔五分钟对表数据进行查询看看有没有新增更新的内容,没错我当时就是这个表情。
我顿时觉得单位工资给我开的太少了,单位不配拥有我,废话少说,这个周必须得谈谈提薪了,步入正题吧。
提示:这里简述项目相关背景:
场景一: A主系统有一张部门表,B子系统也需要部门表于是把A数据库的部门表复制到了B数据库;但是A系统如果要rud部门表的信息的话,B这边无法实时的更新部门表的信息就会触发一些不必要的问题
**场景二:**A系统有一个表信息缓存到redis了,当表信息要rud的时候,redis没有实时更新造成面试经典的缓存一致性问题。
本文只针对单体应用做说明,集群部署的话这边建议自行摸索亲~
Canal主要用途是基于MySQL 数据库增量日志解析,提供增量数据订阅和消费。
基于日志增量订阅和消费的业务包括
更多详细介绍请认准官方链接:
https://github.com/alibaba/canal
环境:windows10,mysql8.0.21,rabbitmq3.10.6,redis5.0.9, canal1.1.6
下载canal应用包(目前只用到了deployer就能实现需求):https://github.com/alibaba/canal/releases
linux:
vim /etc/my.cnf
windows:
mysql安装目录下的my.ini
拿my.ini为例,开启binlog,mysql8.0好像是自动开启的,其余版本需要手动开
[mysqld]
log-bin=mysql-bin # 开启binlog
binlog-format=ROW # 选择ROW模式
server_id=1 # 配置MySQL replaction需要定义,不和Canal的slaveId重复即可
校验是否成功
SHOW VARIABLES LIKE 'log_bin';
就是你下载的那个deployer包有几个配置文件需要下载,这里我们分情况讨论我们实现了两种方案一种是TCP,一种是RabbitMQ。
这里只列举需要改动的配置,其他的默认即可
conf/example/instance.properties
数据库查看binlog查看大小语句
show master status
tcp、rabbitmq通用配置
# position info
# 数据库所在地址
canal.instance.master.address=127.0.0.1:3306
# binlog日志名
canal.instance.master.journal.name=binlog.000090
# binlog日志偏移量其实就是他的大小(从这个大小开始进行增量监控)
canal.instance.master.position=436
# 数据库用户名和密码及字符
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
# table regex
# 数据解析关注的表,Perl正则表达式.
# 这个代表监控所有表
# canal.instance.filter.regex = .*\\..*
# 监控某个库表test库下的test表
canal.instance.filter.regex = test.test
# table black regex
# 过滤不需要监控的表,mysql8.0启动实例的时候会报错有的表明带BASE于是先将其过滤
canal.instance.filter.black.regex=.*\\.BASE.*
rabbitmq需要添加的配置
# mq config
# 这个是交换机的绑定队列的routing key
canal.mq.topic=canal.routing.key
conf/canal.properties
tcp方式维持默认配置即可
rabbitmq需要修改如下配置
# tcp, kafka, rocketMQ, rabbitMQ, pulsarMQ
canal.serverMode = rabbitMQ
##################################################
######### RabbitMQ #############
##################################################
rabbitmq.host = 127.0.0.1
rabbitmq.virtual.host =/
rabbitmq.exchange =canal.exchange
rabbitmq.username =guest
rabbitmq.password =guest
rabbitmq.deliveryMode =
这里只贴rabbitmq实现方式,想要tcp模式的话下次再说吧,贴不过来了
引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>com.alibaba.ottergroupId>
<artifactId>canal.clientartifactId>
<version>1.1.6version>
dependency>
<dependency>
<groupId>com.alibaba.ottergroupId>
<artifactId>canal.protocolartifactId>
<version>1.1.6version>
dependency>
添加rabbitmq配置
spring:
# rabbitmq
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
RabbitMQ监听
这里用的Hutool的数据库工具类,加了事务管理
/**
* Canal + RabbitMQ 监听数据库数据变化
*
* @author sunyan
*/
@Component
public class CanalRabbitMQListener {
@Autowired
private RedisService redisService;
@Autowired
private SpringApplicationConfig config;
private static final Logger log = LoggerFactory.getLogger(CanalRabbitMQListener.class);
private static String ERRORTIMES = "5";
private Session session;
CanalRabbitMQListener() {
//默认数据源
this.session = Session.create();
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "canal.queue", durable = "true"),
exchange = @Exchange(value = "canal.exchange"),
key = "canal.routing.key"
)
})
public void handleDataChange(@Header(AmqpHeaders.CHANNEL) Channel channel, Message msg, @Payload CanalMessage message) throws IOException {
// JSONObject object = JSONObject.parseObject(message);
log.info("Canal监听到数据发生变化\n库名:{}\n表名:{}\n类型:{}\n数据:{}", message.getDatabase(), message.getTable(), message.getType(), message.getData());
/**
* TODO 同步Mysql和Redis
*/
String type = message.getType();
Entity entity = Entity.create(message.getTable());
List<LinkedHashMap<String, String>> data = message.getData();
List<LinkedHashMap<String, String>> old = message.getOld();
if (ObjectUtil.isNotEmpty(old)) {
old.get(0).keySet().forEach(column ->
entity.set(column, data.get(0).get(column))
);
} else {
data.get(0).keySet().forEach(column ->
entity.set(column, data.get(0).get(column))
);
}
String keyName = (String) message.getPkNames().get(0);
String keyId = data.get(0).get(keyName);
Entity where = Entity.create(message.getTable()).set(keyName, keyId);
try {
session.beginTransaction();
//判断执行哪个DML操作
if ("INSERT".equals(type)) {
session.insert(entity);
} else if ("UPDATE".equals(type)) {
session.update(entity, where);
} else if ("DELETE".equals(type)) {
session.del(where);
}
session.commit();
} catch (SQLException throwables) {
//redis计数器,判断达到最大次数手动进行ack
if(redisService.judgeMaxRequestTimes(config.getName() + message.getTable() + message.getId(), ERRORTIMES)){
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
}
session.quietRollback();
}
}
}
CanalMessage
public class CanalMessage<T> {
@JsonProperty("type")
private String type;
@JsonProperty("table")
private String table;
@JsonProperty("data")
private List<T> data;
@JsonProperty("database")
private String database;
@JsonProperty("es")
private Long es;
@JsonProperty("id")
private Integer id;
@JsonProperty("isDdl")
private Boolean isDdl;
@JsonProperty("old")
private List<T> old;
@JsonProperty("pkNames")
private List<String> pkNames;
@JsonProperty("sql")
private String sql;
@JsonProperty("ts")
private Long ts;
canal.instance.filter.druid.ddl = true
canal.instance.filter.query.ddl = true
还有关于redis计数器的代码在之前的文章中有介绍,就是短信那一篇,下方会给链接。
Listener method could not be invoked with the incoming messageEndpoint handler details:Method
这个我们只需要加个converter增添对实体类的转换
@Component
public class MsgConverter {
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
}
于是我们决定采用rabbitmq的发布、订阅模式去实现一主多从的数据同步更新,具体改造如下,记得重启
配置改造
canal.properties(换成发布、订阅模式)
rabbitmq.deliveryMode =fanout
instance.properties(将绑定的路由key置为空)
canal.mq.topic=
类的改造
@Component
public class FanoutAListener {
//省略共同代码,需要改造的就是注解换成只绑定queue队列即可
@RabbitListener(queues = "canal.a")
public void handleDataChange(@Header(AmqpHeaders.CHANNEL) Channel channel, Message msg, @Payload CanalMessage message) throws IOException {
......
}
}
@Component
public class FanoutBListener {
//省略共同代码,需要改造的就是注解换成只绑定queue队列即可
@RabbitListener(queues = "canal.b")
public void handleDataChange(@Header(AmqpHeaders.CHANNEL) Channel channel, Message msg, @Payload CanalMessage message) throws IOException {
......
}
}
Redis就没什么好说的了,把参数message直接换成String类型配对key更新就完了
以上就是本文的全部内容了,能力有限,理性对待
如果感觉还不错的话,欢迎点赞和关注
分享经验,贴近项目,crud永不为奴!!!
欢迎大家关注我的公众号,公众号也会实时发布Java项目相关的文章!!!
1.一文搭建本地git服务器
2.Java实现短信验证码