Redis-缓存一致性

news2024/11/24 12:57:02

缓存双写一致性

更新策略探讨

面试题

缓存设计要求

缓存分类:

  • 只读缓存:(脚本批量写入,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.coloradmin.cn/o/2208078.html

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

相关文章

el-carousel-item自动重复渲染,使用nanoid让重复的元素包含不同的id

<template><div class"page-container"><div class"m-title">轮播图</div><el-carousel height"400px" :autoplay"true"><el-carousel-item v-for"(item, index) in carouselList" :key&…

AOT漫谈专题(第二篇): 如何对C# AOT轻量级APM监控

一&#xff1a;背景 1. 讲故事 上一篇我们聊到了如何调试.NET Native AOT 程序&#xff0c;这是研究一个未知领域知识的入口&#xff0c;这篇我们再来看下如何对 Native AOT 程序进行轻量级的APM监控&#xff0c;当然这里的轻量级更多的是对 AOT 中的coreclr内容的挖掘。 二…

面试官最喜欢问的28道ZooKeeper面试题

前言 ZooKeeper 是一个分布式的&#xff0c;开放源码的分布式应用程序协调服务。它是一个为分布式应用提供一致性服务的软件&#xff0c;提供的功能包括&#xff1a;配置维护、域名服务、分布式同步、组服务等。 ZooKeeper 的目标就是封装好复杂易出错的关键服务&#xff0c;…

再也不怕面试官问我几百亿ip相关的问题了

首先要明确这一类的问题都是海量那个数据类型的问题&#xff0c;对于海量数据我们一般采用分而治之的思路去解决&#xff0c;考官考察的就是你有没有处理海量数据的经验。总结几个常见的海量数据相关的面试&#xff0c;供参考。 有一个存放10GB的ip地址文件&#xff0c;每行一…

2024年【安全生产监管人员】免费试题及安全生产监管人员模拟试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年安全生产监管人员免费试题为正在备考安全生产监管人员操作证的学员准备的理论考试专题&#xff0c;每个月更新的安全生产监管人员模拟试题祝您顺利通过安全生产监管人员考试。 1、【单选题】()以上地方人民政府…

基于SSM的民宿预订系统的设计与实现

文未可获取一份本项目的java源码和数据库参考。 一、毕业设计&#xff08;论文&#xff09;选题的目的和意义 由于现代人经济水平的不断提高&#xff0c;出门旅游已经成为人们放松和休息的一种生活方式。而出门在外&#xff0c;住宿也是首要问题&#xff0c;相比于传统的酒店…

五子棋项目自动化测试

目录 一、五子棋项目介绍 二、编写Web测试用例 三、自动化测试脚本开发 1、引入依赖 2、设计框架 3、Utils 4、LoginPage 5、RegisterPage 6、MatchPage 7、RunTest类 8、运行程序 一、五子棋项目介绍 五子棋项目是基于 WebSocket 实现的多人在线对战系统&#xff0…

【优选算法】(第三十六篇)

目录 ⼆叉树的锯⻮形层序遍历&#xff08;medium&#xff09; 题目解析 讲解算法原理 编写代码 ⼆叉树的最⼤宽度&#xff08;medium&#xff09; 题目解析 讲解算法原理 编写代码 ⼆叉树的锯⻮形层序遍历&#xff08;medium&#xff09; 题目解析 1.题目链接&#xf…

【高中生讲机器学习】21. 隐马尔可夫模型好难?看过来!(下篇)

创建时间&#xff1a;2024-10-09 首发时间&#xff1a;2024-10-12 最后编辑时间&#xff1a;2024-10-12 作者&#xff1a;Geeker_LStar 你好呀~这里是 Geeker_LStar 的人工智能学习专栏&#xff0c;很高兴遇见你~ 我是 Geeker_LStar&#xff0c;一名高一学生&#xff0c;热爱计…

SpringBoot购物推荐网站:设计与实现的最佳实践

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常适…

『深度分析』Kimi版o1来了!Kimi探索版全面解读!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;专注于分享AI全维度知识&#xff0c;包括但不限于AI科普&#xff0c;AI工…

28幅高清修复的英文版中国地图

我们在《183幅值得珍藏的全国地质图集》和《55幅值得珍藏的水文地质图集》两文中&#xff0c;为你分享过精美的全国地质图和水文地质图。 现在我们整理了28幅英文版中国地图&#xff0c;并经过高清修复后分享给大家&#xff0c;你可以在文末查看该数据的领取方法。 28幅英文版…

Channel模块 --- 管理套接字事件

目录 设计思想 实现 设计思想 Channel模块是用于对一个描述符所需要监控的事件以及事件触发之后要执行的回调函数进行管理的 具体来说&#xff0c;它里面会保存该文件描述符所监控的事件&#xff0c;该文件描述符所就绪的事件&#xff0c;以及该描述符的各种事件的处理回调…

4 机器学习之归纳偏好

通过学习得到的模型对应了假设空间中的一个假设。于是&#xff0c;图1.2的西瓜版本空间给我们带来一个麻烦&#xff1a;现在有三个与训练集一致的假设&#xff0c;但与它们对应的模型在面临新样本的时候&#xff0c;却会产生不同的输出。例如&#xff0c;对&#xff08;色泽青绿…

java算法oj(3)栈和队列

目录 1.前言 2.正文 2.1基础操作 2.2用栈实现队列 2.2.1题目 2.2.2示例 2.2.3代码 2.3用队列实现栈 2.3.1题目 2.3.2示例 2.3.3代码 2.4最小栈 2.4.1题目 2.4.2示例 2.4.3代码 3.小结 1.前言 哈喽大家好吖&#xff0c;今天来分享几道栈与队列的算法题&#…

三、AOP

文章目录 1. AOP&#xff08;概念&#xff09;2. AOP&#xff08;底层原理&#xff09;2.1 AOP 底层使用动态代理2.2 AOP&#xff08;JDK动态代理&#xff09; 3. AOP&#xff08;术语&#xff09;3.1 连接点3.2 切入点3.3 通知&#xff08;增强&#xff09;3.4 切面 4. AOP操作…

数据丢失不再怕!四款数磁盘数据恢复据恢复工具实测心得

在办公室行政的工作中&#xff0c;数据恢复工具是我们不可或缺的帮手。无论是不小心删除了重要文件&#xff0c;还是硬盘突然罢工&#xff0c;这些工具总能在关键时刻帮我们力挽狂澜。今天&#xff0c;我就来分享一下我使用福昕数据恢复、转转大师数据恢复、超级兔子数据恢复和…

MySQL 【数字】函数大全(二)

MODPIPOWPOWERRANDROUNDSIGNSQRTTRUNCATE 1、MOD MOD(number1, number2) &#xff1a;返回一个数字除以另一个数字的余数。 语法&#xff1a; 1、MOD(number1, number2) 2、number1 MOD number2 3、number1 % number2 number1&#xff1a;被除数。 number2&#xff1a;除数。…

Qt学习系列之设计模式的小记录

Qt学习系列之设计模式的小记录 前言Qt中的设计模式使用情况数据模型视图组建代理 小tips虚函数&#xff1a;基类的不同具体使用界面设计后台显示 报错解决 总结 前言 在软件设计师中的设计模式有提到设计模式有三种类型&#xff1a; 创建型&#xff1a;工厂方法模式、抽象工厂…

上海AI实验室CVT-Occ时间融合利用视差搜索刷新occ3DWaymo SOTA

上海AI实验室提出CVT-Occ&#xff1a;通过时间融合利用视差搜索刷新occ3D-Waymo SOTA Abstract 基于视觉的3D占据预测由于单目视觉在深度估计上的固有局限性而面临显著挑战。本文介绍了CVT-Occ&#xff0c;一种新颖的方法&#xff0c;通过时间上的体素几何对应进行时间融合&a…