缓存数据同步的常见方式有三种:
设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新
- 优势:简单、方便
- 缺点:时效性差,缓存过期之前可能不一致
- 场景:更新频率较低,时效性要求低的业务
同步双写:在修改数据库的同时,直接修改缓存
- 优势:时效性强,缓存与数据库强一致
- 缺点:有代码侵入,耦合度高;
- 场景:对一致性、时效性要求较高的缓存数据
异步通知: 修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
- 优势:低耦合,可以同时通知多个缓存服务
- 缺点:时效性一般,可能存在中间不一致状态
- 场景:时效性要求一般,有多个服务需要同步
而异步实现又可以基于MQ
或者Canal
来实现:
1)基于MQ
的异步通知:
解读:
- 商品服务完成对数据的修改后,只需要发送一条消息到
MQ
中。 - 缓存服务监听
MQ
消息,然后完成对缓存的更新
依然有少量的代码侵入。
2)基于Canal
的通知
解读:
- 商品服务完成商品修改后,业务直接结束,没有任何代码侵入
Canal
监听MySQL
变化,当发现变化后,立即通知缓存服务- 缓存服务接收到
canal
通知,更新缓存
代码零侵入
1. 认识Canal
Canal [kə'næl]
,译意为水道/管道/沟渠,canal
是阿里巴巴旗下的一款开源项目,基于Java
开发。基于数据库增量日志解析,提供增量数据订阅&消费。GitHub
的地址:https://github.com/alibaba/canal
Canal
是基于mysql
的主从同步来实现的,MySQL
主从同步的原理如下:
- 1)
MySQL master
将数据变更写入二进制日志(binary log
),其中记录的数据叫做binary log events
- 2)
MySQL slave
将master
的binary log events
拷贝到它的中继日志(relay log
) - 3)
MySQL slave
重放relay log
中事件,将数据变更反映它自己的数据
而Canal
就是把自己伪装成MySQL
的一个slave
节点,从而监听master
的binary log
变化。再把得到的变化信息通知给Canal
的客户端,进而完成对其它数据库的同步。
2. 监听Canal
Canal
提供了各种语言的客户端,当Canal
监听到binlog
变化时,会通知Canal
的客户端。
我们可以利用Canal
提供的Java
客户端,监听Canal
通知消息。当收到变化的消息时,完成对缓存的更新。
不过这里我们会使用GitHub
上的第三方开源的canal-starter
客户端。地址:https://github.com/NormanGyllenhaal/canal-client
与SpringBoot
完美整合,自动装配,比官方客户端要简单好用很多。
2.1.引入依赖:
<dependency><groupId>top.javatool</groupId><artifactId>canal-spring-boot-starter</artifactId><version>1.2.1-RELEASE</version>
</dependency>
2.2. 编写配置:
canal:destination: dcxuexi # canal的集群名字,要与安装canal时设置的名称一致server: 192.168.150.101:11111 # canal服务地址
2.3. 修改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)@Idprivate 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)@Transientprivate Integer stock;@TableField(exist = false)@Transientprivate Integer sold;
}
2.4. 编写监听器
通过实现EntryHandler<T>
接口编写监听器,监听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> {@Autowiredprivate RedisHandler redisHandler;@Autowiredprivate Cache<Long, Item> itemCache;@Overridepublic void insert(Item item) {// 写数据到JVM进程缓存itemCache.put(item.getId(), item);// 写数据到redisredisHandler.saveItem(item);}@Overridepublic void update(Item before, Item after) {// 写数据到JVM进程缓存itemCache.put(after.getId(), after);// 写数据到redisredisHandler.saveItem(after);}@Overridepublic void delete(Item item) {// 删除数据到JVM进程缓存itemCache.invalidate(item.getId());// 删除数据到redisredisHandler.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 {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService stockService;private static final ObjectMapper MAPPER = new ObjectMapper();@Overridepublic void afterPropertiesSet() throws Exception {// 初始化缓存// 1.查询商品信息List<Item> itemList = itemService.list();// 2.放入缓存for (Item item : itemList) {// 2.1.item序列化为JSONString json = MAPPER.writeValueAsString(item);// 2.2.存入redisredisTemplate.opsForValue().set("item:id:" + item.getId(), json);}// 3.查询商品库存信息List<ItemStock> stockList = stockService.list();// 4.放入缓存for (ItemStock stock : stockList) {// 2.1.item序列化为JSONString json = MAPPER.writeValueAsString(stock);// 2.2.存入redisredisTemplate.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);}
}