• 多级缓存之缓存同步


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

    设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新

    • 优势:简单、方便
    • 缺点:时效性差,缓存过期之前可能不一致
    • 场景:更新频率较低,时效性要求低的业务

    同步双写:在修改数据库的同时,直接修改缓存

    • 优势:时效性强,缓存与数据库强一致
    • 缺点:有代码侵入,耦合度高;
    • 场景:对一致性、时效性要求较高的缓存数据

    异步通知: 修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据

    • 优势:低耦合,可以同时通知多个缓存服务
    • 缺点:时效性一般,可能存在中间不一致状态
    • 场景:时效性要求一般,有多个服务需要同步

    而异步实现又可以基于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 slavemasterbinary log events拷贝到它的中继日志(relay log)
    • 3)MySQL slave重放relay log中事件,将数据变更反映它自己的数据

    Canal就是把自己伪装成MySQL的一个slave节点,从而监听masterbinary 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.javatoolgroupId>
        <artifactId>canal-spring-boot-starterartifactId>
        <version>1.2.1-RELEASEversion>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.2. 编写配置:

    canal:
      destination: dcxuexi # canal的集群名字,要与安装canal时设置的名称一致
      server: 192.168.150.101:11111 # canal服务地址
    
    • 1
    • 2
    • 3

    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)
        @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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2.4. 编写监听器

    通过实现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());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    在这里对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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
  • 相关阅读:
    两化融合贯标是指什么
    C++11新增特性lambda表达式
    vscode常用快捷键
    一天一块钱,月月换新包商业模式
    PMP每日一练 | 考试不迷路-7.6
    《mSystems》比较宏基因组学探究海洋和陆地生态系统中磷酸盐分解代谢途径
    算法实验题(涉外黄成老师!!!)
    ssdp协议搜索GB28181设备
    Python机器学习实战-特征重要性分析方法(1):排列重要性(附源码和实现效果)
    Elasticsearch映射操作(八)
  • 原文地址:https://blog.csdn.net/qq_37726813/article/details/134340647