Redis-缓存一致性

缓存双写一致性

更新策略探讨

面试题

缓存设计要求

缓存分类:

  • 只读缓存:(脚本批量写入,canal 等)
  • 读写缓存
    • 同步直写:vip数据等即时数据
    • 异步缓写:允许延时(仓库,物流),异常出现,有可能需要使用 kafka, rabbitmq 进行弥补,重试重写
双检加锁

如果 qps 过高,会打高 mysql

数据库和缓存更新的几种策略

=》实现最终一致性

可以停机->单线程操作

四种更新策略

先更新数据库,再更新缓存

异常问题1->最后更新redis异常,出现脏数据

异常问题2->多线程更新快慢问题,无法保证第二步写redis的顺序

先更新缓存,再更新数据库
业务上,mysql是底单数据
仍然是多线程更新的问题

自己的理解,只要是写缓存的方式,都会存在线程写入先后导致的数据不一致

先删除缓存,再更新数据库

第一个进来的线程当数据库写未提交或者其他异常情况的时候,仍然可能存在脏数据问题,因为无法保证缓存会被一定及时删除,可能中途被其他线程回写,这段要自己脑子里跑一遍,延迟双删来确保旧的缓存会被删除掉

读缓存会写入旧数据

延时双删

先更新数据库,再删除缓存->主流

存在问题->没来得及更新完缓存,会读取到旧值,但伤害较小

只要涉及到数据库和缓存的双写,百分百存在一致性问题

如何实现最终一致性

只能实现最终一致性

如何取舍&总结

双写一致性落地案例

监听 binlog

mysql -> canal -> redis

Home

canal 选择版本 1.6.1

mysql: 5.7 docker环境安装

1.mysql 准备

docker run -d -p 3306:3306 --privileged=true -v /root/mysql/log:/var/log/mysql -v /root/mysql/data:/var/lib/mysql -v /root/mysql/conf:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=123456  --name mysql mysql:5.7


docker exec -it e8b72b11df2a /bin/bash    //e8b72b11df2a 就是容器id
    
mysql -uroot -p        // 密码 123456

# 查看是否开启 binlog
SHOW VARIABLES LIKE 'log_bin';

# 添加 canal 操作账号
DROP USER IF EXISTS 'canal'@'%';
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';  
GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' IDENTIFIED BY 'canal';  
FLUSH PRIVILEGES;
 
SELECT * FROM mysql.user;

# 创建一张表
CREATE TABLE `t_user` 
( 
`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT, 
`userName` VARCHAR ( 100 ) NOT NULL, 
PRIMARY KEY ( `id` ) 
) 
ENGINE = INNODB AUTO_INCREMENT = 10 DEFAULT CHARSET = utf8mb4
2.canal 准备

下载 canal.deployer-1.1.6.tar.gz

修改配置文件

canal.instance.master.address=xxx:xxx:xxx:xxx:3306

# username/password (如果配置的是 canal/canal 则默认无需修改)
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
./bin/startup.sh

查看启动日志,检查运行状态

3.编写 canal-client

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.10</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

<dependencies>
        <!--canal-->
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.1.0</version>
        </dependency>
        <!--SpringBoot与Redis整合依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

配置文件(请按需修改)

server:
  port: 5555

spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
#    password: 123456

config 文件和 springboot 案例中一致

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        redisTemplate.setConnectionFactory(lettuceConnectionFactory);

        // 设置key的序列化方式string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置value的序列化方式json
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        // 设置hash的key的序列化方式string
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        // 设置hash的value的序列化方式json
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}

biz 操作封装

@Component
public class RedisCanalClient {

    @Resource
    RedisTemplate redisTemplate;

    /**
     * 读取 canal 数据写入redis
     * @param columns 行
     */
    public void redisInsert(List<CanalEntry.Column> columns) {
        JSONObject jsonObject = new JSONObject();
        for (CanalEntry.Column column : columns) {
            System.out.println(column.getName() + " : " + column.getValue() + "  update=" + column.getUpdated());
            jsonObject.put(column.getName(), column.getValue());
        }
        if (columns.size()>0) {
            redisTemplate.opsForValue().set(columns.get(0).getValue(), jsonObject.toJSONString());
        }
    }

    /**
     * 读取 canal 数据删除 redis 行
     * @param columns 行
     */
    public void redisDelete(List<CanalEntry.Column> columns) {
        JSONObject jsonObject = new JSONObject();
        for (CanalEntry.Column column : columns) {
            System.out.println(column.getName() + " : " + column.getValue() + "  update=" + column.getUpdated());
            jsonObject.put(column.getName(), column.getValue());
        }
        if (columns.size()>0) {
            redisTemplate.delete(columns.get(0).getValue());
        }
    }

    /**
     * 读取 canal 数据修改 redis 行
     * @param columns 行
     */
    public void redisUpdate(List<CanalEntry.Column> columns) {
        JSONObject jsonObject = new JSONObject();
        for (CanalEntry.Column column : columns) {
            System.out.println(column.getName() + " : " + column.getValue() + "  update=" + column.getUpdated());
            jsonObject.put(column.getName(), column.getValue());
        }
        if (columns.size()>0) {

            redisTemplate.opsForValue().set(columns.get(0).getValue(), jsonObject.toJSONString());
            System.out.println("----------------update after: " + redisTemplate.opsForValue().get(columns.get(0).getValue()));

        }
    }

    /**
     * 读取 canal 数据并操作
     * @param entrys 行
     */
    public void printEntry(List<CanalEntry.Entry> entrys) {
        for (CanalEntry.Entry entry : entrys) {
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
                continue;
            }

            CanalEntry.RowChange rowChage = null;
            try {
                rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
                        e);
            }

            CanalEntry.EventType eventType = rowChage.getEventType();
            System.out.println(String.format("================&gt; binlog[%s:%s] , name[%s,%s] , eventType : %s",
                    entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
                    entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
                    eventType));

            for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {
                if (eventType == CanalEntry.EventType.DELETE) {
                    redisDelete(rowData.getBeforeColumnsList());
                } else if (eventType == CanalEntry.EventType.INSERT) {
                    redisInsert(rowData.getAfterColumnsList());
                } else {
                    redisUpdate(rowData.getAfterColumnsList());
                }
            }
        }
    }

}

test 测试类

@SpringBootTest
public class CanalClientTest {

    public static final Integer _60SECONDS = 60;

    @Resource
    RedisCanalClient redisCanalClient;

    /**
     * 测试 canal client 监听 server 实现 mysql -> redis
     */
    @Test
    public void startClient() {
        System.out.println("--------------initCanal main()方法------------");

        // ======================================================================================
        CanalConnector connector = CanalConnectors.newSingleConnector(
                new InetSocketAddress("127.0.0.1", 11111), // canal server 地址
                "example",
                "",
                "");

        int batchSize = 1000;
        int emptyCount = 0;
        System.out.println("---------------------canal init OK,开始监听mysql变化------");
        try {
            connector.connect();
//            connector.subscribe(".*\\..*");
            // 设置监听的表

            connector.subscribe("canal.t_user");

            connector.rollback();
            int totalEmptyCount = 10 * _60SECONDS;
            while (emptyCount < totalEmptyCount) {
                System.out.println("我是 canal, 每秒一次正在监听: " + UUID.randomUUID().toString());
                Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
                long batchId = message.getId();
                int size = message.getEntries().size();
                if (batchId == -1 || size == 0) {
                    emptyCount++;
                    System.out.println("empty count : " + emptyCount);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                } else {
                    // 计数器置0
                    emptyCount = 0;
                    // System.out.printf("message[batchId=%s,size=%s] \n", batchId, size);
                    redisCanalClient.printEntry(message.getEntries());
                }

                connector.ack(batchId); // 提交确认
                // connector.rollback(batchId); // 处理失败, 回滚数据
            }

            System.out.println("已经监听了"+totalEmptyCount+"秒,无任何消息,请重启重试");
        } finally {
            connector.disconnect();
        }
    }
}

启动测试类

4.测试
  • 测试新增

手动添加一条记录 id: 7 name:666

查看 redis

  • 测试修改

修改mysql id:7 的 name 为 777

  • 测试删除

删除 id:7 的这条数据

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C语言练习

题目&#xff1a; 1.编写一段C语言&#xff0c;向下边这样输入2个整数&#xff0c;如果他们的差值小于等于10&#xff0c;则显示“它们的差值小于等于10”&#xff0c;否则显示“它们的差大于等于11”. 请输入两个整数&#xff1a; 整数A&#xff1a;12 整数B&#xff1a;7…

SQL分类中的DDL

DDL&#xff08;Data Definition Language):数据定义语言&#xff0c;用来定义数据库对象&#xff08;数据库&#xff0c;表&#xff0c;字段&#xff09;。 一、DDL语句操作数据库 1、查询所有数据库&#xff1a;show databases&#xff1b;&#xff08;一般用大写&#xff…

spring |Spring Security安全框架 —— 认证流程实现

文章目录 开头简介环境搭建入门使用1、认证1、实体类2、Controller层3、Service层3.1、接口3.2、实现类3.3、实现类&#xff1a;UserDetailsServiceImpl 4、Mapper层3、自定义token认证filter 注意事项小结 开头 Spring Security 官方网址&#xff1a;Spring Security官网 开…

React路由 基本使用 嵌套路由 动态路由 获取路由参数 异步路由 根据配置文件来生成路由

文章目录 React-router的三个版本react-router使用嵌套路由动态路由 获取路由参数Params参数Query参数Location信息 控制跳转地址异步路由根据配置文件生成路由 React-router的三个版本 React-router 服务端渲染使用React-router-dom 浏览器端渲染使用React-router-native Rea…

API项目3:API签名认证

问题引入 我们为开发者提供了接口&#xff0c;却对调用者一无所知 假设我们的服务器只能允许 100 个人同时调用接口。如果有攻击者疯狂地请求这个接口&#xff0c;那是很危险的。一方面这可能会损害安全性&#xff0c;另一方面耗尽服务器性能&#xff0c;影响正常用户的使用。…

若依前后端分离版本el-select下拉框字典如何设置默认值。

在若依前后端分离框架中&#xff0c;如何给下拉框设置默认值&#xff0c;刚入门的小伙伴&#xff0c;可能会不知道如何去做。 本章教程&#xff0c;主要以用户管理模块中的添加用户举例说明如何设置用户性别默认值为男。 解决思路 首先&#xff0c;我们需要找到打开新增页面的方…

【工具】前端js数字金额转中文大写金额

【工具】前端js数字金额转中文大写金额 代码 <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>金额转…

多功能点击器(文末附Gitee源码)——光遇自动弹奏

之前提到的多功能点击器&#xff0c;使用场景比较多&#xff0c;之前玩光遇喜欢在里面弹琴&#xff0c;想到用这个点击器也能自动弹琴&#xff0c;跟别的自动弹琴脚本不一样&#xff0c;这个比较简单容易操作。 借这个光遇自动弹琴使用教程再讲解一下这个多功能点击头的使用方法…

ModelMapper的常见用法 ,号称是beanUtils.copyProp....的升级版??,代码复制粘贴即可复现效果,so convenient

官网案例 以下将官网案例做一个解释 1&#xff09;快速入门 递归遍历源对象的属性拷贝给目标对象 拷贝对象下对象的属性值 Data class Order {private Customer customer;private Address billingAddress; }Data class Customer {private Name name; }Data class Name {pr…

在三维可视化项目中,B/S和C/S架构该如何选择?

一、什么是B/S和C/S 在3D数据可视化中&#xff0c;有两种常见的架构模式&#xff1a;BS&#xff08;Browser/Server&#xff09;和CS&#xff08;Client/Server&#xff09; B/S模式 B/S模式是指将3D数据可视化的逻辑和处理放在服务器端&#xff0c;而在客户端使用浏览器进行…

智能汽车智能网联

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 屏蔽力是信息过载时代一个人的特殊竞争力&#xff0c;任何消耗你的人和事&#xff0c;多看一眼都是你的不…

rom定制系列------小米6x_MIUI14_安卓13刷机包修改写入以及功能定制 界面预览

在接待一些定制化系统中。有很多工作室或者一些特殊行业的友友需要在已有固件基础上简略修改其中的功能。方便使用。例如usb调试默认开启。usb安装设置以及usb安装与内置删减一些app的定制服务。今天给友友预览其中小米6X此款机型定制相关的一些界面与功能演示。 定制机型以及…

公司新来的00后测试开发,让我对“跨界”二字有了全新认识

最近&#xff0c;我们部门迎来了一位新面孔——一个00后的年轻人&#xff0c;阿沅。初见他时&#xff0c;我以为他只是众多新入职员工中的普通一员&#xff0c;毕竟他的专业背景与我们的IT行业似乎相去甚远——广告学。然而&#xff0c;他的到来&#xff0c;却如同一阵清风&…

IDEA中的Postfix Completion与Live Templates功能详解

目录 前言1. Postfix Completion&#xff08;后缀补全&#xff09;1.1 什么是Postfix Completion1.2 使用Postfix Completion的步骤1.3 Postfix Completion的具体应用1.4 自定义Postfix Completion 2. Live Templates&#xff08;实时模板&#xff09;2.1 什么是Live Templates…

聊聊 Facebook Audience Network 绑定收款账号的问题

大家好&#xff0c;我是牢鹅&#xff01;本篇是Facebook开发者系列的第五篇&#xff0c;最近看见好多群友在群里问这个&#xff0c;说Facebook的变现账户在绑定国内的银行账户时&#xff08;有些用户反馈就算不是国内的卡也会出现该问题&#xff09;&#xff0c;显示“无法绑定…

【WRF工具】QGis插件GIS4WRF:根据嵌套网格生成namelist.wps文件

【WRF工具】QGis插件GIS4WRF:根据嵌套网格生成namelist.wps文件 准备:WRF嵌套网格QGis根据嵌套网格生成namelist.wps文件检查:根据namelist.wps绘制模拟区域参考GIS4WRF 是一个免费且开源的 QGIS 插件,旨在帮助研究人员和从业者进行高级研究天气研究与预报(WRF)模型的建模…

利用可解释性技术增强制造质量预测模型

概述 论文地址&#xff1a;https://arxiv.org/abs/2403.18731 本研究提出了一种利用可解释性技术提高机器学习&#xff08;ML&#xff09;模型性能的方法。该方法已用于铣削质量预测&#xff0c;这一过程首先训练 ML 模型&#xff0c;然后使用可解释性技术识别不需要的特征并去…

Lucene 倒排索引

倒排索引是什么&#xff1f; 【定义】倒排索引&#xff08;Inverted Index&#xff09;是一种用于信息检索的数据结构&#xff0c;尤其适用于文本搜索。它与传统索引的主要区别在于&#xff0c;传统索引是根据文档来查找词语的位置&#xff0c;而倒排索引则是根据词语来查找文…

vmware虚拟机 报错:客户机操作系统已禁用 CPU,请关闭或重置虚拟机 的解决方法

打开cpu虚拟化全部进行勾选 ctrl e 进行关机 勾选上打开就好了 如果没有那个选项 关机>打开虚拟机>管理>更改硬件兼容性> 往小处改改> >更改此虚拟机

MySQL连接查询:联合查询

先看我的表结构 emp表 联合查询的关键字&#xff08;union all, union&#xff09; 联合查询 基本语法 select 字段列表 表A union all select 字段列表 表B 例子&#xff1a;将薪资低于5000的员工&#xff0c; 和 年龄大于50 岁的员工全部查询出来 第一种 select * fr…