缓存数据同步的常见方式有三种:
设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新
同步双写:在修改数据库的同时,直接修改缓存
异步通知: 修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
而异步实现又可以基于MQ
或者Canal
来实现:
1)基于MQ
的异步通知:
解读:
MQ
中。MQ
消息,然后完成对缓存的更新依然有少量的代码侵入。
2)基于Canal
的通知
解读:
Canal
监听MySQL
变化,当发现变化后,立即通知缓存服务canal
通知,更新缓存代码零侵入
Canal
Canal [kə'næl]
,译意为水道/管道/沟渠,canal
是阿里巴巴旗下的一款开源项目,基于Java
开发。基于数据库增量日志解析,提供增量数据订阅&消费。GitHub
的地址:https://github.com/alibaba/canal
Canal
是基于mysql
的主从同步来实现的,MySQL
主从同步的原理如下:
MySQL master
将数据变更写入二进制日志(binary log
),其中记录的数据叫做binary log events
MySQL slave
将master
的binary log events
拷贝到它的中继日志(relay log
)MySQL slave
重放relay log
中事件,将数据变更反映它自己的数据而Canal
就是把自己伪装成MySQL
的一个slave
节点,从而监听master
的binary log
变化。再把得到的变化信息通知给Canal
的客户端,进而完成对其它数据库的同步。
Canal
Canal
提供了各种语言的客户端,当Canal
监听到binlog
变化时,会通知Canal
的客户端。
我们可以利用Canal
提供的Java
客户端,监听Canal
通知消息。当收到变化的消息时,完成对缓存的更新。
不过这里我们会使用GitHub
上的第三方开源的canal-starter
客户端。地址:https://github.com/NormanGyllenhaal/canal-client
与SpringBoot
完美整合,自动装配,比官方客户端要简单好用很多。
<dependency>
<groupId>top.javatoolgroupId>
<artifactId>canal-spring-boot-starterartifactId>
<version>1.2.1-RELEASEversion>
dependency>
canal:
destination: dcxuexi # canal的集群名字,要与安装canal时设置的名称一致
server: 192.168.150.101:11111 # canal服务地址
Item
实体类通过@Id
、@Column
、等注解完成Item
与数据库表字段的映射:
package com.dcxuexi.item.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import javax.persistence.Column;
import java.util.Date;
@Data
@TableName("tb_item")
public class Item {
@TableId(type = IdType.AUTO)
@Id
private Long id;//商品id
@Column(name = "name")
private String name;//商品名称
private String title;//商品标题
private Long price;//价格(分)
private String image;//商品图片
private String category;//分类名称
private String brand;//品牌名称
private String spec;//规格
private Integer status;//商品状态 1-正常,2-下架
private Date createTime;//创建时间
private Date updateTime;//更新时间
@TableField(exist = false)
@Transient
private Integer stock;
@TableField(exist = false)
@Transient
private Integer sold;
}
通过实现EntryHandler
接口编写监听器,监听Canal
消息。注意两点:
@CanalTable("tb_item")
指定监听的表信息EntryHandler
的泛型是与表对应的实体类package com.dcxuexi.item.canal;
import com.github.benmanes.caffeine.cache.Cache;
import com.dcxuexi.item.config.RedisHandler;
import com.dcxuexi.item.pojo.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;
@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler<Item> {
@Autowired
private RedisHandler redisHandler;
@Autowired
private Cache<Long, Item> itemCache;
@Override
public void insert(Item item) {
// 写数据到JVM进程缓存
itemCache.put(item.getId(), item);
// 写数据到redis
redisHandler.saveItem(item);
}
@Override
public void update(Item before, Item after) {
// 写数据到JVM进程缓存
itemCache.put(after.getId(), after);
// 写数据到redis
redisHandler.saveItem(after);
}
@Override
public void delete(Item item) {
// 删除数据到JVM进程缓存
itemCache.invalidate(item.getId());
// 删除数据到redis
redisHandler.deleteItemById(item.getId());
}
}
在这里对Redis
的操作都封装到了RedisHandler
这个对象中,是我们之前做缓存预热时编写的一个类,内容如下:
package com.dcxuexi.item.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.dcxuexi.item.pojo.Item;
import com.dcxuexi.item.pojo.ItemStock;
import com.dcxuexi.item.service.IItemService;
import com.dcxuexi.item.service.IItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class RedisHandler implements InitializingBean {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private IItemService itemService;
@Autowired
private IItemStockService stockService;
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void afterPropertiesSet() throws Exception {
// 初始化缓存
// 1.查询商品信息
List<Item> itemList = itemService.list();
// 2.放入缓存
for (Item item : itemList) {
// 2.1.item序列化为JSON
String json = MAPPER.writeValueAsString(item);
// 2.2.存入redis
redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
}
// 3.查询商品库存信息
List<ItemStock> stockList = stockService.list();
// 4.放入缓存
for (ItemStock stock : stockList) {
// 2.1.item序列化为JSON
String json = MAPPER.writeValueAsString(stock);
// 2.2.存入redis
redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
}
}
public void saveItem(Item item) {
try {
String json = MAPPER.writeValueAsString(item);
redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public void deleteItemById(Long id) {
redisTemplate.delete("item:id:" + id);
}
}