前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis进阶学习09---缓存同步

Redis进阶学习09---缓存同步

作者头像
大忽悠爱学习
发布2022-05-12 14:26:53
6990
发布2022-05-12 14:26:53
举报
文章被收录于专栏:c++与qt学习c++与qt学习

Redis进阶学习09---缓存同步

Canal

认识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的客户端,进而完成对其它数据库的同步。

安装和配置Canal

下面我们就开启mysql的主从同步机制,让Canal来模拟salve

开启MySQL主从

Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。

这里以之前用Docker运行的mysql为例:

开启binlog

打开mysql容器挂载的日志文件,我的在/tmp/mysql/conf目录:

代码语言:javascript
复制
log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=item

配置解读:

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

最终效果:

代码语言:javascript
复制
[mysqld]
#跳过域名解析
skip-name-resolve
#指定服务器级别的字符集
character_set_server=utf8
#指定数据存放目录
datadir=/var/lib/mysql
#MySQL服务的ID
server-id=1000
#mysql二进制文件
log-bin=/var/lib/mysql/mysql-bin
#需要复制的数据库
binlog-do-db=item

设置用户权限

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

代码语言:javascript
复制
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;

replication slave 的级别是global,所以不能只作用于某一数据库,而是全局

如果权限缩小为某一个数据库,那么会报错: Incorrect usage of DB GRANT and GLOBAL PRIVILEGES

重启mysql容器即可

代码语言:javascript
复制
docker restart mysql

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

代码语言:javascript
复制
show master status;

安装Canal

创建网络

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

代码语言:javascript
复制
docker network create item

让mysql加入这个网络:

代码语言:javascript
复制
docker network connect item mysql

查看当前网络细节:

代码语言:javascript
复制
docker network inspect item 

安装Canal

拉取docker镜像:

代码语言:javascript
复制
 docker pull canal/canal-server

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

代码语言:javascript
复制
docker run -p 11111:11111 --name canal \
-e canal.destinations=item \
-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=item\\..* \
--network item \
-d canal/canal-server

注意:mysql:3306,这里3306是容器中开放的端口,也就是容器中mysql启动时的端口,而不是映射到宿主机上的端口,这点必须注意,因为通过容器名直接访问,是容器与容器之间直接进行通信,不需要通过宿主机。

说明:

  • -p 11111:11111:这是canal的默认监听端口
  • -e canal.instance.master.address=mysql:3306:数据库地址和端口,如果不知道mysql容器名字,可以通过docker inspect 容器id来查看,因为这里是在同一个自定义网络下,因此可以用容器名字进行通信
  • -e canal.instance.dbUsername=canal:数据库用户名
  • -e canal.instance.dbPassword=canal :数据库密码
  • -e canal.instance.filter.regex=:要监听的表名称

表名称监听支持的语法:

代码语言:javascript
复制
mysql 数据解析关注的表,Perl正则表达式.
多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\) 
常见例子:
1.  所有表:.*   or  .*\\..*
2.  canal schema下所有表: canal\\..*
3.  canal下的以canal打头的表:canal\\.canal.*
4.  canal schema下的一张表:canal.test1
5.  多个规则组合使用然后以逗号隔开:canal\\..*,mysql.test1,mysql.test2 

监听Canal

Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。

我们可以利用Canal提供的Java客户端,监听Canal通知消息。当收到变化的消息时,完成对缓存的更新。

不过这里我们会使用GitHub上的第三方开源的canal-starter客户端。地址:https://github.com/NormanGyllenhaal/canal-client

与SpringBoot完美整合,自动装配,比官方客户端要简单好用很多。

将项目拉取下来,然后安装到本地仓库中

引入依赖:

代码语言:javascript
复制
  <dependency>
      <groupId>top.javatool</groupId>
      <artifactId>canal-spring-boot-starter</artifactId>
      <version>1.2.6-RELEASE</version>
  </dependency>

编写配置:

代码语言:javascript
复制
canal:
  destination: item # canal的集群名字,要与安装canal时设置的名称一致
  server: 192.168.150.101:11111 # canal服务地址

修改Item实体类

通过@Id、@Column、等注解完成Item与数据库表字段的映射:

代码语言:javascript
复制
@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<T>接口编写监听器,监听Canal消息。注意两点:

  • 实现类通过@CanalTable("tb_item")指定监听的表信息
  • EntryHandler的泛型是与表对应的实体类
代码语言:javascript
复制
@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这个对象中,是我们之前做缓存预热时编写的一个类,内容如下:

代码语言:javascript
复制
@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);
    }
}

超详细canal入门,看这篇就够了

注意

如果canal出现了问题,通过进入canal容器内部,查看输出日志,来排查问题:

代码语言:javascript
复制
docker exec -it canal bash

查看日志:

进入容器内部canal-server/logs目录下,查看item目录下的,item.log日志

我在这里碰到一个问题是,无法识别mysql的主机地址:

原因: 一开始我运行canal容器的时候,标注的mysql地址为mysql:3307

我的mysql容器内端口为3306,宿主机映射端口为3307,我错误的将mysql宿主机映射端口写了上去,这是错误的,因为我们这里采用自定义网络进行通信,通过mysql容器名直接或容器内ip直接完成两个容器之间的通信,不需要经过宿主机,因此不能用宿主机映射端口

Docker重学系列之高级网络篇

多级缓存总结

对于不共享的本地缓存,通常采用的做法是,通过hash路由,让固定的uri去访问固定某台服务器,确保缓存命中,并且不至于缓存多分份

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-05-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Redis进阶学习09---缓存同步
  • Canal
    • 认识Canal
      • 安装和配置Canal
        • 开启MySQL主从
        • 开启binlog
        • 设置用户权限
      • 安装Canal
        • 创建网络
        • 安装Canal
        • 监听Canal
        • 引入依赖:
        • 编写配置:
        • 修改Item实体类
        • 编写监听器
    • 注意
    • 多级缓存总结
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档