数据库与缓存数据同步

本文介绍了缓存同步的三种策略,重点讲解了Canal的安装和配置,以及如何利用Canal实现异步数据同步,包括MySQL主从同步设置、Canal容器部署和Java客户端实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

缓存同步

  • 数据同步策略
  • 安装Canal
  • 监听Canal

一、数据同步策略

缓存数据同步的常见方式有三种:

  • 设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新
    • 优势:简单、方便
    • 缺点:时效性差,缓存过期之前可能不一致
    • 场景:更新频率较低,时效性要求低的业务
  • 同步双写:在修改数据库的同时,直接修改缓存
    • 优势:时效性强,缓存与数据库强一致
    • 缺点:有代码侵入,耦合度高;
    • 场景:对一致性、时效性要求较高的缓存数据
  • 异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
    • 优势:低耦合,可以同时通知多个缓存服务
    • 缺点:时效性一般,可能存在中间不一致状态
    • 场景:时效性要求一般,有多个服务需要同步

1.基于MQ的异步通知

在这里插入图片描述

2.基于Canal的异步通知

在这里插入图片描述

二、安装Canal

安装和配置Canal

1.开启mysql的主从同步机制,让Canal来模拟salve

Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。
这里以之前用Docker运行的mysql为例:

1.1 开启binlog

打开mysql容器挂载的日志文件,我的在/tmp/mysql/conf目录:
在这里插入图片描述
修改文件:

vi /tmp/mysql/conf/my.cnf

添加内容:

log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima

配置解读:

  • log-bin=/var/lib/mysql/mysql-bin:设置binary log文件的存放地址和文件名,叫做mysql-bin
  • binlog-do-db=heima:指定对哪个database记录binary log events,这里记录heima这个库(监听heima这个数据库的数据变化)

最终效果:

[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima
1.2 设置用户权限

接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限。

create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;

重启mysql容器即可

docker restart mysql

测试设置是否成功:在mysql控制台,或者Navicat中,输入命令:

show master status;

在这里插入图片描述

2.安装Canal
2.1 创建网络

我们需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:

docker network create heima

让mysql加入到这个网络:

docker network connect heima mysql
2.2 安装canal

将 canal.tar 上传到虚拟机,然后通过命令导入:

docker load -i canal.tar

然后运行命令创建canal容器:

docker run -p 11111:11111 --name canal \
-e canal.destinations=heima \
-e canal.instance.master.address=mysql:3306  \
-e canal.instance.dbUsername=canal  \
-e canal.instance.dbPassword=canal  \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false  \
-e canal.instance.filter.regex=heima\\..* \
--network heima \
-d canal/canal-server:v1.1.5

说明:

  • -p 11111:11111:这是canal的默认监听端口
  • -e canal.instance.master.address=mysql:3306:数据库地址和端口,如果不知道mysql容器地址,可以通过docker inspect 容器id来查看。由于mysql和canal处在同一网络,因此地址可以直接使用:mysql容器名:端口号
  • -e canal.instance.dbUsername=canal:数据库用户名
  • -e canal.instance.dbPassword=canal :数据库密码
  • -e canal.instance.filter.regex=:要监听的表名称

三、监听Canal

Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。
在这里插入图片描述

Canal客户端

Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。不过这里我们会使用GitHub上的第三方开源的canal-starter。地址:https://github.com/NormanGyllenhaal/canal-client

1.引入依赖
<dependency>
    <groupId>top.javatool</groupId>
    <artifactId>canal-spring-boot-starter</artifactId>
    <version>1.2.1-RELEASE</version>
</dependency>

2.在Yml配置文件中配置Canal

canal:
  destination: heima                # canal实例名称,要跟canal-server运行时设置的destination一致
  server: 192.168.30.125:11111      # canal地址 

3.Canal客户端

编写监听器,监听Canal消息:

在这里插入图片描述

Canal推送给canal-client的是被修改的这一行数据(row),而我们引入的canal-client则会帮我们把行数据封装到Item实体类中。这个过程中需要知道数据库与实体的映射关系,要用到JPA的几个注解:

在这里插入图片描述

4.代码

/**
 * canal数据监听-数据同步
 */
@Component
@CanalTable("tb_item")
public class ItemHandler implements EntryHandler<Item> {

    @Resource
    private RedisHandler redisHandler;
    @Resource
    private Cache<Long, Item> itemCache;

    /**
     * 监听数据插入的方法
     * @param item
     */
    @Override
    public void insert(Item item) {
        // 写数据到JVM进程缓存
        itemCache.put(item.getId(), item);
        // 写数据到redis
        redisHandler.saveItem(item);
    }

    /**
     * 监控数据更新的方法
     * @param before 更新前的数据
     * @param after  更新后的数据
     */
    @Override
    public void update(Item before, Item after) {
        // 写数据到JVM进程缓存
        itemCache.put(after.getId(), after);
        // 写数据到redis
        redisHandler.saveItem(after);
    }

    /**
     * 监控数据删除的方法
     * @param item
     */
    @Override
    public void delete(Item item) {
        // 删除JVM进程缓存
        itemCache.invalidate(item.getId());
        // 删除redis缓存
        redisHandler.deleteItemById(item.getId());
    }
}
@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Resource
    private IItemService itemService;
    @Resource
    private IItemStockService itemStockService;

    private static final ObjectMapper MAPPER = new ObjectMapper();

    /**
     * 初始化缓存-缓存预热
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        // 1.查询商品信息
        List<Item> itemList = itemService.list();
        // 2.放入缓存
        for (Item item : itemList) {
            // 2.1 将item序列化为json字符串再缓存
            String itemJson = MAPPER.writeValueAsString(item);
            // 存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), itemJson);
        }

        // 3.查询库存信息
        List<ItemStock> itemStockList = itemStockService.list();
        // 3.1 放入缓存
        for (ItemStock itemStock : itemStockList) {
            // 3.1.1 将itemStock序列化为字符串在缓存
            String itemStockJson = MAPPER.writeValueAsString(itemStock);
            // 3.1.2 存入redis
            redisTemplate.opsForValue().set("item:stock:id:" + itemStock.getId(), itemStockJson);
        }
    }

    /**
     * 向redis中写数据
     * @param item
     */
    public void saveItem(Item item) {
        try {
            // 将实体类序列化为json格式存入缓存
            String itemJson = MAPPER.writeValueAsString(item);
            // 写入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), itemJson);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 删除redis中的某条数据
     * @param id
     */
    public void deleteItemById(Long id) {
        // 根据key删除redis中的数据
        redisTemplate.delete("item:id:" + id);
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值