文章目录
SpringBoot整合Canal实现缓存更新功能
1、前提条件
1.1 canal详细安装教程
1.2 理论依据
Canal是基于mysql的主从同步实现,简单的说就是Canal假装成mysql的一个子节点,开启一个线程不断的读取mysql的binlog日志(此日志记录的是mysql的执行命令),将binlog日志的内容拷贝到中继日志(relay log),然后再开启一个线程去不断重放中继日志中的执行命令,以此达到数据一致。放在代码中就是,只要继承了EntryHandler并指定数据库表,那么对应表添加或者修改一行数据的时候会在binlog上生成一条日志,canal读取到这条日志之后会产生一条消息,并封装成EntryHandler中指定的实体类,这时候再针对这条实体类数据进行处理。
2、导入相关依赖并配置
默认数据库、redis的基础环境已搭建好,持久层使用的是mybatisPlus
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-starter</artifactId>
<version>1.2.1-RELEASE</version>
</dependency>
canal:
# 此处的destination是创建canal时指定的,也可以在canal.properties文件中调整
destination: test
server: 192.168.56.10:11111
3、demo示例代码
①主要业务实现:消息拦截器
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;
/**
* @Author Jzs
* @Date 2023/3/9 21:08
*/
@CanalTable("tb_customer")
@Component
public class CustomerHandler implements EntryHandler<Customer>, InitializingBean {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private CustomerService customerService;
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final String CUSTOMER_KEY = "customer:";
@Override
public void afterPropertiesSet() throws Exception {
//提前加载数据库中的数据到redis中
customerService.list().forEach(this::saveCustomer);
}
@Override
public void insert(Customer customer) {
saveCustomer(customer);
}
@Override
public void update(Customer before, Customer after) {
saveCustomer(after);
}
@Override
public void delete(Customer customer) {
//删除对应的redis缓存
stringRedisTemplate.delete(CUSTOMER_KEY + customer.getId());
}
private void saveCustomer(Customer customer){
try {
//写数据到redis缓存
String json = MAPPER.writeValueAsString(customer);
stringRedisTemplate.opsForValue().set(CUSTOMER_KEY + customer.getId(),json);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
②实体类
@Data
@TableName("tb_customer")
public class Customer {
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 手机号码
*/
private String phone;
/**
* 密码,加密存储
*/
private String password;
/**
* 昵称,默认是随机字符
*/
private String nickName;
/**
* 用户头像
*/
private String icon = "";
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
③业务层略
④controller层
@CrossOrigin
@RestController
@RequestMapping("/customer")
public class CustomerController {
@Resource
private CustomerService service;
@PostMapping("/save")
public void save(@RequestBody Customer customer){
service.save(customer);
}
@PutMapping("/update")
public void update(@RequestBody Customer customer){
service.updateById(customer);
}
@DeleteMapping("/delete/{id}")
public void delete(@PathVariable("id") Integer id){
service.removeById(id);
}
}
4、遇到的问题
以插入数据为例,整个canal的调用链上使用到的类大概有以下流程,最后处理完消息的数据之后才会调用步骤2图片中的entryHandler.insert(object),也就是CustomerHandler 中的insert方法
①部分字段的数据丢失
获取数据是通过反射获取到类中所有字段作为key,由于类中部分字段跟数据库的字段对不上导致获取不到数据,可以看到步骤4中,可以通过Column注解进行指定。
②添加好字段之后,数据转化出现问题,这个主要是由于步骤6中,字符串转对应类型的时候StringConvertUtil的convertType方法中并没有这个LocalDateTime的判断转化,此处比较简单的一个修改方案是直接将LocalDateTime换成Date
③简单修改后的相关字段如下:
@Column(name = "nick_name")
private String nickName;
@Column(name = "create_time")
private Date createTime;
@Column(name = "update_time")
private Date updateTime;
③比较建议直接下载canal-spring-boot-starter的源码,进行对应修改之后再打包成对应的依赖进行导入,这样子相对来说比较可控,也可以定制化处理一些特殊的问题,修改的话可以大概参照图片的流程。
总结
其实canal的使用并不复杂,上面只是一个简单的例子,部分方法可以单独抽出来放在各自的工具类中,出于方便只展示了redis一种方式,其实还可以替换成其他类型的缓存中间件进行使用。