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.7docker exec -it e8b72b11df2a /bin/bash    //e8b72b11df2a 就是容器idmysql -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: 5555spring:redis:database: 0host: 127.0.0.1port: 6379
#    password: 123456

config 文件和 springboot 案例中一致

@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(lettuceConnectionFactory);// 设置key的序列化方式stringredisTemplate.setKeySerializer(new StringRedisSerializer());// 设置value的序列化方式jsonredisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());// 设置hash的key的序列化方式stringredisTemplate.setHashKeySerializer(new StringRedisSerializer());// 设置hash的value的序列化方式jsonredisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.afterPropertiesSet();return redisTemplate;}
}

biz 操作封装

@Component
public class RedisCanalClient {@ResourceRedisTemplate 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;@ResourceRedisCanalClient redisCanalClient;/*** 测试 canal client 监听 server 实现 mysql -> redis*/@Testpublic 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 {// 计数器置0emptyCount = 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.mzph.cn/diannao/56170.shtml

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

相关文章

C++: AVL树的实现

一.AVL树的旋转 AVL树是平衡搜索二叉树的一种。 平衡因子&#xff1a;节点右树的高度减左树的高度&#xff0c;AVL树规定平衡因子的绝对值小于2。若不在这个范围内&#xff0c;说明该树不平衡。 AVL树节点&#xff1a; struct AVLTreeNode {AVLTreeNode(const T& data …

数据结构--堆的深度解析

目录 引言 一、基本概念 1.1堆的概念 1.2堆的存储结构 1.3堆的特点 二、 堆的基本操作 2.1初始化 2.2创建堆 2.3插入元素 2.4删除元素 2.5堆化操作 2.6堆的判空 2.7获取堆顶元素 三、堆的常见应用 1. 优先队列 2. 堆排序 3. Top-k 问题 4. 图论中的应用 四…

rom定制系列------小米5x_miui12安卓11定制固件界面预览 小米5x第三方固件

&#x1f49d;&#x1f49d;&#x1f49d;此固件来源于客户卡刷固件定制。客户需要修改为线刷。并且修改账号锁功能。 可以让客户使用官方平台批量进行刷写。方便操作。 定制机型以及功能预览&#x1f49d;&#x1f49d;&#x1f49d; 小米5x版本miui12.5.8安卓11固件。此机型…

MySQL 连接

使用MySQL二进制方式连接 使用MySQL二进制方式进入到MySQL命令提示符下来连接MySQL数据库。 实例 以下是从命令行中连接MySQL服务器的简单实例&#xff1a; [roothost]# mysql -u root -p Enter password:******在登录成功后会出现 mysql> 命令提示窗口&#xff0c;你可以在…

Java 文件拷贝

1.小文件拷贝 实例代码&#xff1a; 上面程序运行的图示&#xff1a; 弊端&#xff1a;一次读一个字节&#xff0c;效率太慢。所以需要一次读取多个字节。 2.大文件拷贝 结果&#xff1a;

UE5运行时动态加载场景角色动画任意搭配-全流程代码(四)

UE5运行时动态加载场景、角色、角色动画、相机动画任意搭配,Android、iOS也可以跑,横竖屏兼容,手机竖屏: 1、场景切换UWorld处理 在通过OpenLevel进行场景切换的时候,UWorld会发生变化,需要我们获取正确的UWorld。 1、在GameInstance监听Level加载 void UMyGameInsta…

数据结构——复杂度

目录 数据结构前言 数据结构 算法 算法效率 时间复杂度 大O的渐进表示法 示例1 示例2 示例3 示例4 示例5 示例6 示例7 空间复杂度 示例1 示例2 示例3 示例4 常见复杂度对比 旋转数组 优化1 优化2 这一篇文章我们就开始数据结构知识的学习&#xff01; 数据…

stm32单片机个人学习笔记10(TIM编码器接口)

前言 本篇文章属于stm32单片机&#xff08;以下简称单片机&#xff09;的学习笔记&#xff0c;来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记&#xff0c;只能做参考&#xff0c;细节方面建议观看视频&#xff0c;肯定受益匪浅。 STM32入门教程-2023版 细…

Jetpack-Room

Room是Android Jetpack中的一个组件&#xff0c;它提供了一个抽象层&#xff0c;帮助开发者在本地数据库&#xff08;如SQLite&#xff09;上进行持久化数据存储。Room通过简化数据库操作&#xff0c;使得数据管理变得更加容易和高效。 Room重要的概念 实体&#xff08;Entit…

Python即时获取上证指数信息并发送邮件到指定邮箱

最近股市振荡&#xff0c;股民会时不时查看一下上证指数信息&#xff0c;但是繁忙的工作却时不是让人忘记了&#xff0c;错过了投资的最佳时机&#xff0c;这时就可以通过Python来爬取网站上的上证指数&#xff0c;并发送到指定的邮箱&#xff0c;这样就不用上指定网页也以获取…

网络断开导致SSH会话和服务端任务终止的原因及使用screen详解

在进行深度学习任务时&#xff0c;常常在本地通过ssh连接远程服务器进行炼丹任务。当我在pycharm上远程连接到服务器上进行训练时&#xff0c;由于网络不稳定使得SSH断开连接&#xff0c;我原以为服务器不会受影响。通过nvidia-smi命令发现GPU占用为0&#xff0c;这才发现任务终…

稀土阻燃协效剂 - 阻燃

金士镧稀土阻燃协效剂的技术衬垫&#xff1a; 金士镧KingCela 稀土阻燃协效剂KCL-FR-06xx系列产品&#xff0c;凭借独特的稀土4f电子层结构, 可协效磷氮阻燃剂&#xff0c;卤系阻燃剂阻燃剂在高分子材料&#xff08;橡胶&#xff0c;塑料&#xff0c;涂层&#xff0c;胶黏剂&a…

『网络游戏』服务器启动逻辑【16】

新建Visual Studio工程命名为NetGameServer 重命名为ServerStart.cs 创建脚本&#xff1a; 编写脚本&#xff1a;ServerRoot.cs 编写脚本&#xff1a;ServerStart.cs 新建文件夹 调整脚本位置 新建文件夹 新建文件夹网络服务 创建脚本&#xff1a;NetSvc.cs 编写脚本&#xff1…

使用 KVM 在 Xubuntu 上创建 Windows 10 虚拟机

目录 前言说明注意准备 iso官网思博主(嘻嘻)拖动到虚拟机里面启动 virt-manager创建虚拟机选择本地安装介质选择 iso配置 内存 和 CPU选择 创建的虚拟机 保存的位置启动虚拟机看到熟悉的 Win10界面前言 XUbuntu安装OpenSSH远程连接服务器 远程连接之MobaXterm安装使用 虚拟化技…

DS线性表之队列的讲解和实现(5)

文章目录 前言一、队列的概念及结构二、队列的实现队列节点和队列初始化销毁判断是否为空入队列出队列获取队头队尾数据获取队列元素个数 三、实际使用效果总结 前言 队列实现源代码   队列是我们遇到的第二个实用数据结构&#xff0c;栈和队列地位等同 一、队列的概念及结构…

一篇python常见的Pandas的数据功能的使用

当我们学习了如何使用 Pandas 进行数据的导入与导出,这为我们后续的数据处理打下了基础.此次我们将重点讨论数据选择与过滤.通过掌握这一部分的知识,你将能够轻松地从复杂的数据集中提取出所需的信息.接下来,我们将通过一些实例来逐步了解这些操作. 这里插播一条粉丝福利&#…

三、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操作…

Element中el-table组件设置max-height右侧出现空白列的解决方法

之前就出现过这个情况&#xff0c;没理过&#xff0c;因为不影响啥除了不美观...但今天看着实在是难受&#xff0c;怎么都不顺眼(可能是我自己烦躁--) 试了很多网上的方法&#xff0c;都不得行&#xff0c;后面发现了这篇文章&#xff0c;解决了! 感谢&#xff01; Element中t…

SpringBoot人事系统:企业人才资源的智能管家

6系统测试 6.1概念和意义 测试的定义&#xff1a;程序测试是为了发现错误而执行程序的过程。测试(Testing)的任务与目的可以描述为&#xff1a; 目的&#xff1a;发现程序的错误&#xff1b; 任务&#xff1a;通过在计算机上执行程序&#xff0c;暴露程序中潜在的错误。 另一个…

fastadmin 多商户模式下侧边栏跳转路径BUG

记录&#xff1a;仅作自己项目记录&#xff0c;在一个域名下部署多套项目时&#xff0c;若是多商户模式项目会出现跳转路径问题。 修改 \manystore\library\Auth.php 文件的 getSidebar 方法 // 1 改为&#xff1a; $v[url] isset($v[url]) && $v[url] ? $v[url] :…