SpringBoot AOP + Redis 延时双删功能实战
一、业务场景
在多线程并发情况下,假设有两个数据库修改请求,为保证数据库与redis的数据一致性,修改请求的实现中需要修改数据库后,级联修改Redis中的数据。
-
请求一:A修改数据库数据 B修改Redis数据
-
请求二:C修改数据库数据 D修改Redis数据
并发情况下就会存在A —> C —> D —> B的情况
❝
一定要理解线程并发执行多组原子操作执行顺序是可能存在交叉现象的
❞
1、此时存在的问题
A修改数据库的数据最终保存到了Redis中,C在A之后也修改了数据库数据。
此时出现了Redis中数据和数据库数据不一致的情况,在后面的查询过程中就会长时间去先查Redis, 从而出现查询到的数据并不是数据库中的真实数据的严重问题。
2、解决方案
在使用Redis时,需要保持Redis和数据库数据的一致性,最流行的解决方案之一就是延时双删策略。
注意:要知道经常修改的数据表不适合使用Redis,因为双删策略执行的结果是把Redis中保存的那条数据删除了,以后的查询就都会去查询数据库。所以Redis使用的是读远远大于改的数据缓存。
延时双删方案执行步骤
-
删除缓存
-
更新数据库
-
延时500毫秒 (根据具体业务设置延时执行的时间)
-
删除缓存
3、为何要延时500毫秒?
这是为了我们在第二次删除Redis之前能完成数据库的更新操作。假象一下,如果没有第三步操作时,有很大概率,在两次删除Redis操作执行完毕之后,数据库的数据还没有更新,此时若有请求访问数据,便会出现我们一开始提到的那个问题。
4、为何要两次删除缓存?
如果我们没有第二次删除操作,此时有请求访问数据,有可能是访问的之前未做修改的Redis数据,删除操作执行后,Redis为空,有请求进来时,便会去访问数据库,此时数据库中的数据已是更新后的数据,保证了数据的一致性。
二、代码实践
1、引入Redis和SpringBoot AOP依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、编写自定义aop注解和切面
ClearAndReloadCache延时双删注解
`*/** *延时双删 **/*
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public@interface ClearAndReloadCache {
String name()default "";
}
`
ClearAndReloadCacheAspect延时双删切面
`@Aspect
@Component
publicclassClearAndReloadCacheAspect{ @Autowired
private StringRedisTemplate stringRedisTemplate; */**
* 切入点
*切入点,基于注解实现的切入点 加上该注解的都是Aop切面的切入点
*
*/* @Pointcut("@annotation(com.pdh.cache.ClearAndReloadCache)")
publicvoidpointCut(){ }
*/**
* 环绕通知
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
* @param proceedingJoinPoint
*/*
@Around("pointCut()")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){ System.out.println("----------- 环绕通知 -----------"); System.out.println("环绕通知的目标方法名:" + proceedingJoinPoint.getSignature().getName()); Signature signature1 = proceedingJoinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature)signature1; Method targetMethod = methodSignature.getMethod();*//方法对象* ClearAndReloadCache annotation = targetMethod.getAnnotation(ClearAndReloadCache.class);*//反射得到自定义注解的方法对象* String name = annotation.name();*//获取自定义注解的方法对象的参数即name* Set<String> keys = stringRedisTemplate.keys("*" + name + "*");*//模糊定义key* stringRedisTemplate.delete(keys);*//模糊删除redis的key值* *//执行加入双删注解的改动数据库的业务 即controller中的方法业务* Object proceed = null;
try { proceed = proceedingJoinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } *//开一个线程 延迟1秒(此处是1秒举例,可以改成自己的业务)*
*// 在线程中延迟删除 同时将业务代码的结果返回 这样不影响业务代码的执行*
new Thread(() -> {
try { Thread.sleep(1000); Set<String> keys1 = stringRedisTemplate.keys("*" + name + "*");*//模糊删除* stringRedisTemplate.delete(keys1); System.out.println("-----------1秒钟后,在线程中延迟删除完毕 -----------"); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); return proceed;*//返回业务代码的值* }
}
`
3、application.yml
server: port: 8082 spring: # redis setting redis: host: localhost port: 6379 # cache setting cache: redis: time-to-live: 60000 # 60s datasource: driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test
username: root
password: 1234 > 基于 SpringCloudAlibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
>
> * 项目地址:<https://github.com/YunaiV/yudao-cloud>
> * 视频教程:<https://doc.iocoder.cn/video/> # mp setting
mybatis-plus: mapper-locations: classpath*:com/pdh/mapper/*.xml global-config: db-config: table-prefix: configuration: # log of sql log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # hump map-underscore-to-camel-case: true
`
4、user_db.sql脚本
用于生产测试数据
``DROP TABLE IF EXISTS `user_db`;
CREATE TABLE `user_db` ( `id` int(4) NOT NULL AUTO_INCREMENT, `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ----------------------------
-- Records of user_db
-- ----------------------------
INSERT INTO `user_db` VALUES (1, '张三');
INSERT INTO `user_db` VALUES (2, '李四');
INSERT INTO `user_db` VALUES (3, '王二');
INSERT INTO `user_db` VALUES (4, '麻子');
INSERT INTO `user_db` VALUES (5, '王三');
INSERT INTO `user_db` VALUES (6, '李三');
``
5、UserController
/** * 用户控制层 */*
@RequestMapping("/user")
@RestController
public class UserController { @Autowired private UserService userService; @GetMapping("/get/{id}") @Cache(name = "get method") *//@Cacheable(cacheNames = {"get"})* public Result get(@PathVariable("id") Integer id){ return userService.get(id); } @PostMapping("/updateData") @ClearAndReloadCache(name = "get method") public Result updateData(@RequestBody User user){ return userService.update(user); } @PostMapping("/insert") public Result insert(@RequestBody User user){ return userService.insert(user); } @DeleteMapping("/delete/{id}") public Result delete(@PathVariable("id") Integer id){ return userService.delete(id); }
}
6、UserService
/** * service层 */*
@Service
public class UserService { @Resource private UserMapper userMapper; public Result get(Integer id){ LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getId,id); User user = userMapper.selectOne(wrapper); return Result.success(user); } public Result insert(User user){ int line = userMapper.insert(user); if(line > 0) return Result.success(line); return Result.fail(888,"操作数据库失败"); } public Result delete(Integer id) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getId, id); int line = userMapper.delete(wrapper); if (line > 0) return Result.success(line); return Result.fail(888, "操作数据库失败"); } public Result update(User user){ int i = userMapper.updateById(user); if(i > 0) return Result.success(i); return Result.fail(888,"操作数据库失败"); }
}
`
三、测试验证
1、ID=10,新增一条数据

2、第一次查询数据库,Redis会保存查询结果

3、第一次访问ID为10

4、第一次访问数据库ID为10,将结果存入Redis

5、更新ID为10对应的用户名(验证数据库和缓存不一致方案)

数据库和缓存不一致验证方案:
打个断点,模拟A线程执行第一次删除后,在A更新数据库完成之前,另外一个线程B访问ID=10,读取的还是旧数据。


6、采用第二次删除,根据业务场景设置延时时间,两次删除缓存成功后,Redis结果为空。读取的都是数据库真实数据,不会出现读缓存和数据库不一致情况。

四、代码工程及地址
核心代码红色方框所示
❝
https://gitee.com/jike11231/redisDemo.git
❞

https://mp.weixin.qq.com/s/VBr3E086U58PyQkNdFfNzg
相关文章:
SpringBoot AOP + Redis 延时双删功能实战
一、业务场景 在多线程并发情况下,假设有两个数据库修改请求,为保证数据库与redis的数据一致性,修改请求的实现中需要修改数据库后,级联修改Redis中的数据。 请求一:A修改数据库数据 B修改Redis数据 请求二ÿ…...
【Java集合类面试七】、 JDK7和JDK8中的HashMap有什么区别?
文章底部有个人公众号:热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享? 踩过的坑没必要让别人在再踩,自己复盘也能加深记忆。利己利人、所谓双赢。 面试官:JDK7和JDK8中的HashMap有…...
el-tree 获取过滤后的树结构
正常来说element框架应该返回的,但实际上没有,只能自己处理了 递归处理,思路就是赋值,如果是自己过滤到的数据就push进去,不是就不要 let newCheckTree [] let tree get_tree(treeData,newCheckTree); //获取过滤…...
Windows连接SFTP服务
最近有个新需求需要通过SFTP方式连接到一个FTP中下载相关内容 1.使用命令行方式 在cmd中使用如下命令 sftp -P [port] [username]ip #示例 sftp -P 666 ftp123.123.123.123然后弹出的提示输入yes,再输入密码就可以了。 2.使用资源管理器方式 普通FTP可以使用资源…...
《红蓝攻防对抗实战》五.内网探测协议出网之DNS协议探测出网
DNS(Domain Name System)即域名解析系统,可将域名解析到对应访问IP。下面我们还是以系统自带命令为案例,进行演示DNS协议探测出网。 目录 一.Windows系统探测DNS协议出网 二.Linux系统探测DNS协议出网 1. Dig命令 2.Nslookup命…...
计算机算法分析与设计(18)---回溯法(介绍、子集和问题C++代码)
文章目录 一、回溯法介绍二、子集和问题2.1 知识概述2.2 代码编写 一、回溯法介绍 1. 回溯法(back tracking)是一种选优搜索法,又称为试探法,有“通用的解题法”之称,按选优条件向前搜索,以达到目标。但当探…...
[Hive] explode
在 Hive 中,explode 函数用于将数组(Array)或者Map类型的列拆分成多行, 每个元素或键值对为一行。这允许我们在查询中对数组或 Map 进行扁平化操作。 下面是使用 explode 函数的示例: 假设我们有一个包含数组字段的表…...
2023年10月22日找工作面试交流遇到的基本问题
交叉编译解决的痛点问题 不同硬件体系结构之间的编译问题。嵌入式系统开发需要在主机上编写代码。提高效率和节省时间。软件移植和管理依赖关系。 不同硬件体系结构之间的编译问题:例如,你开发了一个针对Intel x86架构的应用程序,但想要在Ra…...
如何判断要不要用振动技术来进行设备预测性维护
在现代工业设备运行过程中,及时发现设备故障并进行维修对于确保生产线的正常运行至关重要。振动分析技术作为一种先进的设备监测和预测性维护方法,通过实时监测和分析设备的振动信号,可以提前发现潜在故障,降低停机时间和维护成本…...
数据结构和算法——用C语言实现所有树形结构及相关算法
文章目录 前言树和森林基础概念二叉树二叉树的遍历二叉树的构造树和森林与二叉树之间的转化树和森林的遍历 满二叉树完全二叉树线索二叉树线索二叉树的构造寻找前驱和后继线索二叉树的遍历 最优二叉树(哈夫曼树)哈夫曼树的构造哈夫曼编码 二叉排序树&…...
OTA: Optimal Transport Assignment for Object Detection 论文和代码学习
OTA 原因步骤什么是最优传输策略标签分配的OT正标签分配负标签分配损失计算中心点距离保持稳定动态k的选取 整体流程代码使用 论文连接: 原因 1、全部按照一个策略如IOU来分配GT和Anchors不能得到全局最优,可能只能得到局部最优。 2、目前提出的ATSS和P…...
前后端交互—跨域与HTTP
跨域 代码下载 同源策略 同源策略(英文全称 Same origin policy)是浏览器提供的一个安全功能。 MDN 官方给定的概念:同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这 是一个用于隔离潜在恶意文件的重要安全机制。 通俗的理解:浏览器规定&a…...
Error和Exception的关系以及区别
在Java中,Error 和 Exception 是两种不同类型的异常类,它们都继承自 java.lang.Throwable,但在用途和处理方式上有重要区别。 Error: Error 表示在程序运行过程中,通常由于系统或环境的严重问题而引起的异常情况。这些问题通常是无…...
Hive SQL 函数高阶应用场景
HIVE作为数据仓库处理常用工具,如同RDBMS关系型数据库中标准SQL语法一样,Hive SQL也内置了不少系统函数,满足于用户在不同场景下的数据分析需求,以提高开发SQL数据分析的效率。 我们可以使用show functions查看当下版本支持的函数…...
linux下C++开发环境搭建
一.安装GCC,GDB 1.1 先更新软件包安装源 sudo apt update1.2 安装编译器和调试器 sudo apt install build-essential gdb"build-essential" 是编译代码所需要的工具。 "gdb" 是调试器。1. build-essential:- "build-essential" 是一个用于Ubu…...
报错问题解决办法:Decryption error sun.security.rsa.RSAPadding.unpadV15
报错问题解决办法:Decryption error sun.security.rsa.RSAPadding.unpadV15 出现的问题 javax.crypto.BadPaddingException: Decryption errorat sun.security.rsa.RSAPadding.unpadV15(RSAPadding.java:380) ~[na:1.8.0_131]at sun.security.rsa.RSAPadding.unpa…...
LVS+DR部署
LVS-DR的工作原理: 1.客户端会发送请求到vip 2.LVS的调度器接受请求之后,根据算法选择一台真实服务器,请求转发到后端RS,请求的报文的目的MAC地址,修改成后端真实服务器的MAC地址,转发。 3.后端真实服务器…...
C++项目——云备份-②-第三方库认识
文章目录 专栏导读1. json 认识1.1 JSON 数据结构的特点 2. jsoncpp库认识3. json实现序列化案例4. json实现反序列化案例5. bundle文件压缩库认识6. bundle库实现文件压缩案例7.bundle库实现文件解压缩案例8.httplib库认识9. httplib库搭建简单服务器案例10. httplib库搭建简单…...
Linux入门攻坚——4、shell编程初步、grep及正则表达式
bash的基础特性(续): 1、提供了编程环境: 编程风格:过程式:以指令为中心,数据服务于执行;对象式:以数据为中心,指令服务于数据 shell编程,编译执…...
TCP/IP(二十二)TCP 实战抓包分析(六)TCP 快速建立连接
一 TCP Fast Open 快速建立连接 说明: 之前讲解TCP 相关知识点遗漏了这个知识点,补充上 ① TFO简介 ② 请求 Fast Open Cookie过程 "原理图" ③ 真正开始 TCP Fast Open 重点: TFO 使 SYN包 可以包含payload 数据 ④ 抓包分析 1、…...
idea大量爆红问题解决
问题描述 在学习和工作中,idea是程序员不可缺少的一个工具,但是突然在有些时候就会出现大量爆红的问题,发现无法跳转,无论是关机重启或者是替换root都无法解决 就是如上所展示的问题,但是程序依然可以启动。 问题解决…...
vscode里如何用git
打开vs终端执行如下: 1 初始化 Git 仓库(如果尚未初始化) git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...
CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
AI Agent与Agentic AI:原理、应用、挑战与未来展望
文章目录 一、引言二、AI Agent与Agentic AI的兴起2.1 技术契机与生态成熟2.2 Agent的定义与特征2.3 Agent的发展历程 三、AI Agent的核心技术栈解密3.1 感知模块代码示例:使用Python和OpenCV进行图像识别 3.2 认知与决策模块代码示例:使用OpenAI GPT-3进…...
python/java环境配置
环境变量放一起 python: 1.首先下载Python Python下载地址:Download Python | Python.org downloads ---windows -- 64 2.安装Python 下面两个,然后自定义,全选 可以把前4个选上 3.环境配置 1)搜高级系统设置 2…...
学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...
Java - Mysql数据类型对应
Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...
相机从app启动流程
一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...
DBAPI如何优雅的获取单条数据
API如何优雅的获取单条数据 案例一 对于查询类API,查询的是单条数据,比如根据主键ID查询用户信息,sql如下: select id, name, age from user where id #{id}API默认返回的数据格式是多条的,如下: {&qu…...
关于 WASM:1. WASM 基础原理
一、WASM 简介 1.1 WebAssembly 是什么? WebAssembly(WASM) 是一种能在现代浏览器中高效运行的二进制指令格式,它不是传统的编程语言,而是一种 低级字节码格式,可由高级语言(如 C、C、Rust&am…...
