当前位置: 首页 > news >正文

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 的这条数据

相关文章:

Redis-缓存一致性

缓存双写一致性 更新策略探讨 面试题 缓存设计要求 缓存分类&#xff1a; 只读缓存&#xff1a;&#xff08;脚本批量写入&#xff0c;canal 等&#xff09;读写缓存 同步直写&#xff1a;vip数据等即时数据异步缓写&#xff1a;允许延时&#xff08;仓库&#xff0c;物流&a…...

SAP学习笔记 - 豆知识13 - Msg 番号 NR751 - Object RF_BELEG R100、番号範囲間隔 49 不存在 FBN1

其实这种就是自动採番的番号没弄。 比如跨年了&#xff0c;那该新年度的番号范围没弄啊&#xff0c;就会出这种错误。 把番号范围给加一下就可以了。 1&#xff0c;现象 比如点 VL02N 出荷传票变更 画面&#xff0c;点 出库确认 就会出如下错误&#xff1a; Object RF_BEL…...

美摄科技云服务解决方案,方案成熟,接入简单

美摄科技作为视频处理领域的先锋&#xff0c;凭借其强大的技术实力和深厚的行业经验&#xff0c;推出了成熟的云服务解决方案&#xff0c;为轻量化视频制作开辟了全新的道路。 一、成熟方案&#xff0c;接入无忧 美摄科技云服务解决方案的最大亮点在于其成熟度和易用性。我们…...

【bug】paddleocr draw_ocr_box_txt ValueError: incorrect coordinate type

【bug】paddleocr draw_ocr_box_txt ValueError: incorrect coordinate type 环境 python 3.10.15pillow 10.4.0 paddleocr 2.8.1错误详情 错误文本 Traceback (most recent call last):....draw_left.polygon(box, fillcolor)ValueError: inco…...

python的多线程和多进程

首先需要明确的是&#xff0c;多进程和其他语言的一样&#xff0c;能够利用多核cpu&#xff0c;但是python由于GIL的存在&#xff0c;多线程在执行的时候&#xff0c;实际上&#xff0c;每一时刻只有一个线程在执行。相当于是单线程。然而多线程在某些情况下&#xff0c;还是能…...

基于SpringBoot+Vue+uniapp的时间管理小程序的详细设计和实现(源码+lw+部署文档+讲解等)

详细视频演示 请联系我获取更详细的演示视频 项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不…...

HMAC-MD5参数签名算法

更多中电联在线工具 HMAC-MD5 是一种基于 MD5 哈希函数的消息认证码&#xff08;MAC&#xff09;算法。它用于确保消息的完整性和认证&#xff0c;通常用于数据传输和 API 请求。其基本步骤如下&#xff1a; 密钥准备&#xff1a;选择一个密钥&#xff08;K&#xff09;&#…...

【word】文章里的表格边框是双杠

日常小伙伴们遇到word里插入的表格&#xff0c;边框是双杠的&#xff0c;直接在边框和底纹里修改边框的样式就可以&#xff0c;但我今天遇到的这个有点特殊&#xff0c;先看看表格在word里的样式是怎么样&#xff0c;然后我们聊聊如何解决。 这个双杠不是边框和底纹的设置原因…...

我常用的两个单例模式写法 (继承Mono和不继承Mono的)

不继承Mono 不继承Mono代表不用挂载到场景物体上面&#xff0c;因此直接饿汉式 加 合并空运算符判空创建实例 >(lambda表达式)的意思是get&#xff0c;就是将instance赋给Instance属性 //单例private static JsonDataManager instance new JsonDataManager();public stati…...

Android 自定义Toast显示View

1、创建一个tosat显示的布局文件&#xff1a;toast_custom.xml <?xml version"1.0" encoding"utf-8"?> <com.hjq.shape.layout.ShapeLinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width&…...

SCRM呼叫中心高保真Axure原型 源文件分享

在数字化时代&#xff0c;客户关系管理&#xff08;CRM&#xff09;对于企业的成功至关重要。SCRM呼叫中心后台作为一款专为CRM设计的软件原型&#xff0c;致力于为企业提供高效、智能的客户沟通解决方案。本文将详细介绍该产品的核心功能及其对企业提升客户满意度和销售业绩的…...

Ubuntu(Linux)tcpdump使用方法详解

tcpdump命令 1.从所有网卡获取数据包 tcpdump -i any2.从指定网卡获取数据包 tcpdump -i eth03.指定网卡&#xff0c;IP&#xff0c;写文件 tcpdump -i eth0 host 192.168.16.101 -w ./tcp.dat //host 后面是发送方的地址4.指定网卡&#xff0c;源IP且目的IP&#xff0c;写入…...

Centos安装Nginx 非Docker

客户的机器属于 Centos7 系列&#xff0c;由于其较为陈旧&#xff0c;2024开始众多镜像和软件源都已失效。此篇文章将详细记录在 Centos7 操作系统上从零开始安装 Nginx 的整个流程。 本文Nginx是安装在/usr/local/nginx下 详细步骤如下&#xff1a; 准备Nginx安装包&#x…...

免费版的音频剪辑软件:这四款有没有你的菜?

随着音频编辑需求的日益增长&#xff0c;免费的音频剪辑软件逐渐成为许多创作者、学生和普通用户的心头好。今天&#xff0c;就让我为大家介绍几款热门的免费音频剪辑软件&#xff0c;并分享一下我的使用感受吧&#xff01; 一、福昕音频剪辑 直通车&#xff08;复制粘贴到网站…...

Facebook的隐私之战:数据保护的挑战与未来

在数字化时代&#xff0c;隐私保护成为了公众关注的焦点&#xff0c;尤其是在社交媒体巨头Facebook身上。随着用户数据泄露事件的频发&#xff0c;Facebook面临着日益严峻的隐私挑战。这些挑战不仅涉及法律法规的遵循&#xff0c;还影响着用户信任、公司声誉以及未来的发展方向…...

自定义注解和组件扫描在Spring Boot中动态注册Bean(二)

在Spring Boot中&#xff0c;自定义注解和组件扫描是实现动态注册Bean的两种重要手段。通过它们&#xff0c;开发者可以灵活地管理Spring容器中的Bean&#xff0c;提高开发效率和代码的可维护性。本文将详细讲解自定义注解和组件扫描在Spring Boot中如何动态注册Bean。 自定义…...

常见网络协议的介绍、使用场景及 Java 代码样例

以下是几种常见网络协议的介绍、使用场景及 Java 代码样例&#xff1a; 一、HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09; 介绍&#xff1a; HTTP 是用于在 Web 浏览器和 Web 服务器之间传输超文本的应用层协议。基于请求 - 响应模式…...

音视频好文总结

RTSP play同步 ffplay播放器研究分析 ffplay播放器 暂停、逐帧、音量、快进快退seek功能分析 RTSP RTP RTCP SDP基础知识 flv格式分析与解复用 TS格式详解 m3u8格式详解 FLV格式详解 MP4格式详解 HLS协议详解 RTMP协议详解 HTTP-FLV协议详解 H.264 SPS、PPS详解 H…...

云服务器磁盘满了,清理docker无用缓存、容器等清理

docker system prune 命令用于清理 Docker 系统中的各种未使用资源。根据你提供的警告信息&#xff0c;这条命令将会移除以下内容&#xff1a; 所有已停止的容器&#xff08;all stopped containers&#xff09; 所有未被至少一个容器使用的网络&#xff08;all networks no…...

Flutter flutter_native_splash 使用指南

Flutter flutter_native_splash 使用指南 视频 https://youtu.be/dGq6LbipvXA https://www.bilibili.com/video/BV1d52tYFEzz/ 前言 原文 使用 flutter_native_splash 优化 Flutter 启动画面体验 本文详细介绍了如何在 Flutter 中使用 flutter_native_splash 插件自定义启动…...

后进先出(LIFO)详解

LIFO 是 Last In, First Out 的缩写&#xff0c;中文译为后进先出。这是一种数据结构的工作原则&#xff0c;类似于一摞盘子或一叠书本&#xff1a; 最后放进去的元素最先出来 -想象往筒状容器里放盘子&#xff1a; &#xff08;1&#xff09;你放进的最后一个盘子&#xff08…...

MongoDB学习和应用(高效的非关系型数据库)

一丶 MongoDB简介 对于社交类软件的功能&#xff0c;我们需要对它的功能特点进行分析&#xff1a; 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具&#xff1a; mysql&#xff1a;关系型数据库&am…...

ESP32读取DHT11温湿度数据

芯片&#xff1a;ESP32 环境&#xff1a;Arduino 一、安装DHT11传感器库 红框的库&#xff0c;别安装错了 二、代码 注意&#xff0c;DATA口要连接在D15上 #include "DHT.h" // 包含DHT库#define DHTPIN 15 // 定义DHT11数据引脚连接到ESP32的GPIO15 #define D…...

解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错

出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上&#xff0c;所以报错&#xff0c;到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本&#xff0c;cu、torch、cp 的版本一定要对…...

AI编程--插件对比分析:CodeRider、GitHub Copilot及其他

AI编程插件对比分析&#xff1a;CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展&#xff0c;AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者&#xff0c;分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

Map相关知识

数据结构 二叉树 二叉树&#xff0c;顾名思义&#xff0c;每个节点最多有两个“叉”&#xff0c;也就是两个子节点&#xff0c;分别是左子 节点和右子节点。不过&#xff0c;二叉树并不要求每个节点都有两个子节点&#xff0c;有的节点只 有左子节点&#xff0c;有的节点只有…...

AI病理诊断七剑下天山,医疗未来触手可及

一、病理诊断困局&#xff1a;刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断"&#xff0c;医生需通过显微镜观察组织切片&#xff0c;在细胞迷宫中捕捉癌变信号。某省病理质控报告显示&#xff0c;基层医院误诊率达12%-15%&#xff0c;专家会诊…...

Netty从入门到进阶(二)

二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架&#xff0c;用于…...

[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】&#xff0c;分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...

Selenium常用函数介绍

目录 一&#xff0c;元素定位 1.1 cssSeector 1.2 xpath 二&#xff0c;操作测试对象 三&#xff0c;窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四&#xff0c;弹窗 五&#xff0c;等待 六&#xff0c;导航 七&#xff0c;文件上传 …...