多级缓存之缓存同步

news2024/12/26 23:06:45

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

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

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

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

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

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

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

而异步实现又可以基于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.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)
    @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;
}

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> {

    @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);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1193662.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

d3.js

D3&#xff1a;Data-Driven Documents • 通过D3提供的接口来基于数据操控文档的各个图元。 标题对于D3(本讲解)最为重要的标签&#xff0c;主要操作的对象(画布) HTML - 导入D3.js D3.js作为JavaScript的外库&#xff0c;必须先将其导入&#xff0c;如&#xff1a; Python的…

[autojs]用户界面GUI编程

用户界面: UI视图: View attr(name, value)attr(name)whidgravitylayout_gravitymarginmarginLeftmarginRightmarginTopmarginBottompaddingpaddingLeftpaddingRightpaddingToppaddingBottombgalphaforegroundminHeightminWidthvisibilityrotationtransformPivotXtransformPivo…

移位操作符 位操作符详解

hello hello&#xff0c;想我了吗? &#x1f604;&#x1f604;&#x1f604; 首先是移位操作符&#xff1a;<< 左移操作符 >> 右移操作符 注&#xff1a;移位操作符的操作数只能是整数。 << 左移操作符&#xff1a;移位规则&#xff1a; 左边抛弃、…

我的AIGC部署实践03

我的AIGC部署实践03 这会是AIGC部署实践的第三回&#xff0c;用免费的GPU部署自己的stable-diffusion下面我们就开始吧。 1.创建项目 创建项目的镜像及数据集如下&#xff1a; 选择完成后点击创建&#xff0c;代码选择暂不上传。 2.初始化开发环境实例 点击最右侧的“开发…

服务器往客户端发送字符串的网络编程

服务器主要就是能够打开命令行提供的网络端口&#xff0c;然后一有客户端连接上&#xff0c;就会向客户端发送Welcome to Our Server!这段话。 服务器代码serverSayWelcome.c的代码如下&#xff1a; #include <stdio.h> #include <stdlib.h> #include <string.…

【ARM Trace32(劳特巴赫) 使用介绍 3 - trace32 访问运行时的内存】

请阅读【ARM Coresight SoC-400/SoC-600 专栏导读】 文章目录 1.1 trace32 访问运行时的内存1.1.1 侵入式 运行时内存访问1.1.2 非侵入式运行时访问1.1.3 缓存一致性的非侵入式运行时访问 1.2 Trace32 侵入式和非侵入式 运行时访问1.2.1 侵入式访问1.2.2 非侵入式运行时访问 1…

C++:关联式容器map的使用

1、map的简介 map是关联容器&#xff0c;它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。 在map中&#xff0c;键值key通常用于排序和惟一地标识元素&#xff0c;而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同&#xff0c;并…

【数据结构】树与二叉树(八):二叉树的中序遍历(非递归算法NIO)

文章目录 5.2.1 二叉树二叉树性质引理5.1&#xff1a;二叉树中层数为i的结点至多有 2 i 2^i 2i个&#xff0c;其中 i ≥ 0 i \geq 0 i≥0。引理5.2&#xff1a;高度为k的二叉树中至多有 2 k 1 − 1 2^{k1}-1 2k1−1个结点&#xff0c;其中 k ≥ 0 k \geq 0 k≥0。引理5.3&…

【Linux系统化学习】冯诺依曼体系结构 | 操作系统

个人主页点击直达&#xff1a;小白不是程序媛 Linux专栏&#xff1a;Linux系统化学习 目录 冯诺依曼体系结构 组成介绍 CPU和内存 以使用微信发消息为例理解冯诺依曼体系结构 操作系统 冯诺依曼体系结构 随着世界上第一台计算机ENIAC&#xff08;埃尼阿克&#xff09;的…

2、鸿蒙开发工具首次运行时开发环境配置

请务必在第一次运行时配置好开发环境&#xff0c;如果取消了配置&#xff0c;后续再配置会比较麻烦 1、点击工具图标运行 2、在欢迎页中点击“Agree” 3、默认“Do not import setting”&#xff0c;点击“OK” 3、此片设置Nodejs和Ohpm的安装&#xff0c;其中&#xff0c; …

传来喜讯,优维又获奖了!!!

优维科技作为国内DevOps领域的行业领先企业&#xff0c;从诞生之日起&#xff0c;就一直致力于为中国企业提供一流的数字化运维服务&#xff0c;不断深耕核心技术&#xff0c;向客户提供专业强大的产品与服务。多年来&#xff0c;不仅获得了大量客户认可&#xff0c;更是屡次获…

Apache Airflow (三) :Airflow WebUI操作介绍

&#x1f3e1; 个人主页&#xff1a;IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 &#x1f6a9; 私聊博主&#xff1a;加入大数据技术讨论群聊&#xff0c;获取更多大数据资料。 &#x1f514; 博主个人B栈地址&#xff1a;豹哥教你大数据的个人空间-豹…

Android修行手册 - POI操作Excel常用样式(字体,背景,颜色,Style)

点击跳转>Unity3D特效百例点击跳转>案例项目实战源码点击跳转>游戏脚本-辅助自动化点击跳转>Android控件全解手册点击跳转>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&…

【媒体邀约】媒体宣传——企业成长的催化剂

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 媒体宣传是企业成长的催化剂&#xff0c;它在各种方面对企业的成功和发展起到了关键作用。 1. 曝光和知名度&#xff1a; 媒体宣传可以将企业和其产品或服务推向广泛的受众&#xff0c;…

SQL第四次上机实验

1.查询借阅了计算机类或者文学类图书的读者的借书证号 USE TSGL GO SELECT DISTINCT Reader.Lno FROM Book,Lend,Reader WHERE Book.ISBNLend.ISBN AND Lend.LnoReader.Lno AND Class 计算机类 OR Class 文学类2.查询同时借阅了计算机类和文学类图书的读者的借书证号 USE T…

C语言之文件操作(剩余部分)

上篇博客字数到极限了&#xff0c;给大家把内容补充在这一篇&#xff0c;我们还剩下文件读取结束的判定和文件缓冲区的内容没有介绍&#xff0c;让我们开始下面的学习吧&#xff01; 目录 1.文件读取结束的判定 1.1feof函数 1.2ferror函数 代码示例 2.文件缓冲区 2.1fflu…

制造行业怎么做?看低代码如何引领未来

随着科技的不断发展&#xff0c;制造行业正面临着巨大的变革和挑战。为了提高生产效率、降低成本并更好地适应快速变化的市场需求&#xff0c;越来越多的制造企业将目光投向了低代码开发平台。在众多低代码开发平台中&#xff0c;JNPF低代码快速开发平台凭借其卓越的性能和灵活…

“三门问题”解决方案:换不换?更换策略与贝叶斯策略?附 Java 验证代码

文章目录 前言一、什么是“三门问题”&#xff1f;二、“三门问题”解决策略详解2.1、错误策略&#xff1a;直觉策略与随机策略2.2、更换策略与事件分析计算2.3、贝叶斯策略及分析流程 三、Java 语言验证“三门问题”总结 前言 “三门问题”作为一道经典逻辑推理题&#xff0c;…

物联网AI MicroPython学习之语法 ustruct 打包和解压原始数据类型

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; ustruct 介绍 ustruct提供打包和解压原始数据类型的功能。 默认情况下&#xff0c;C类型以机器的本机格式和字节顺序表示&#xff0c;并在必要时通过跳过填充字节来正确对齐&#xff08;根据C编译器使用的规…