之前已经分享了一篇关于springboot + canal实现缓存更新的功能,但这种方案存在缺陷,当项目属于微服务架构时,一个服务可能有多个实例,即使多个实例都在监听canal的消息,但是只有一个实例能够消费该消息以此更新缓存,针对只是Redis做缓存的情况下没有问题,但是多级缓存Caffeine+Redis就有问题了,只有一个服务实例能够更新本地的Caffeine缓存,其他服务实例就无法同步更新。因此我想到可以RabbitMQ实现多服务实例的缓存同步更新操作。而canal本身就可以与RabbitMQ联合使用。
在Linux服务器上利用docker部署MySQL和RabbitMQ后,再部署canal,这次部署和之前有差异。
首先启动容器:
[root@iZ2vc97qcawcm6uqblda2fZ ~]# docker run -d canal/canal-server:v1.1.5
ddcce88c2050213f584849704ddb9401f6a8be1a923b1987f49729ea558e3a31
然后从容器中拷贝配置文件
[root@iZ2vc97qcawcm6uqblda2fZ canal]# docker cp ddcce88c2050:/home/admin/canal-server/conf/canal.properties ./conf/
docker cp ddcce88c2050:/home/admin/canal-server/conf/example/instance.properties ./conf/
修改canal.properties配置文件信息:
##################################################
######### RabbitMQ #############
##################################################
#不能加上端口号
rabbitmq.host =47.108.105.49
rabbitmq.virtual.host =/
rabbitmq.exchange =canal.exchange
rabbitmq.username =guest
rabbitmq.password =guest
rabbitmq.deliveryMode =fanout
修改instance.properties配置文件:
#myqsl主库地址
canal.instance.master.address=47.108.105.49:3306
#mysql的binlog文件名
canal.instance.master.journal.name=mysql-bin.000006
#正则表达式过滤需要监听的数据库
canal.instance.filter.regex=recl\\..*
重启canal-server:
[root@iZ2vc97qcawcm6uqblda2fZ canal]# docker run -d \
> -v /docker/canal/conf/instance.properties:/home/admin/canal-server/conf/example/instance.properties \
> -v /docker/canal/conf/canal.properties:/home/admin/canal-server/conf/canal.properties \
> -p 11111:11111 \
> --name canal \
> canal/canal-server:v1.1.5
303291f6de8d7b6b77928e2aece00e52573b199f90c5000ff3102b72c2736268
创建springboot项目,引入rabbitMQ起步依赖:
<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
application.properties中的配置信息:
# RabbitMQ配置
spring.rabbitmq.host=47.108.105.49
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
spring.rabbitmq.listener.direct.prefetch=1
创建canal消息监听类:
package com.ren.fm.listener;
import com.alibaba.fastjson.JSON;
import com.rabbitmq.client.Channel;
import com.ren.fm.constant.CachePrefix;
import com.ren.utils.canal.CanalBean;
import com.ren.utils.canal.CanalType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* @ClassName: CanalMQListener
* @Description: TODO
* @Author: RZY
* @DATE: 2022/7/17 9:13
* @Version: v1.0
*/
@Component
@Slf4j
public class CanalMQListener {
@Autowired
CachePrefix cachePrefix;
@RabbitListener(queues = "canal.queue.fm.8001")
public void CanalMessageHandler(Message message, Channel channel) {
//获取消息体内容
CanalBean canalBean = null;
try {
//序列化消息体
canalBean = JSON.parseObject(new String(message.getBody(), StandardCharsets.UTF_8), CanalBean.class);
log.info("来自队列 {} 的消息", 8001);
log.info("收到canal消息: {}", canalBean);
String tableName = canalBean.getTable();
if(CachePrefix.isContainsTableName(tableName) && !canalBean.isDdl()) {
//当为UPDATE时,更新缓存
if(canalBean.getType().equals(CanalType.UPDATE)) cachePrefix.updateCache(tableName, canalBean);
//当为DELETE时,删除缓存
if(canalBean.getType().equals(CanalType.DELETE)) cachePrefix.deleteCache(tableName, canalBean);
}
} catch (Exception e) {
log.info("canal消息处理失败: {}", canalBean);
throw new RuntimeException(e);
}
}
}
创建交换机和相关队列:
启动项目测试:
①修改数据库中的某条记录
②查看RabbitMQ控制台(收到了日志信息):
③启动springboot项目(缓存更新成功):