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使用的是读远远大于改的数据缓存。

延时双删方案执行步骤

  1. 删除缓存

  2. 更新数据库

  3. 延时500毫秒 (根据具体业务设置延时执行的时间)

  4. 删除缓存

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

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

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

相关文章

修炼k8s+flink+hdfs+dlink(六:学习k8s-pod)

一&#xff1a;增&#xff08;创建&#xff09;。 直接进行创建。 kubectl run nginx --imagenginx使用yaml清单方式进行创建。 直接创建方式&#xff0c;并建立pod。 kubectl create deployment my-nginx-deployment --imagenginx:latest 先创建employment&#xff0c;不…

CSS页面基本布局

前提回顾 1. 超文本标记语言&#xff08;HTML&#xff09;是一种标记语言&#xff0c;用来结构化我们的网页内容并赋予内容含义&#xff1b; &#xff08;超文本标记语言&#xff08;英语&#xff1a;HyperText Markup Language /ˈhaɪpətekst ˈmɑːkʌp ˈlŋɡwɪdʒ /…

由浅到深 : Self-Attention (自注意力机制)

Self-Attention 看到下面的第一个saw是动词&#xff0c;第二个saw是名词。 因为第一个saw和第二个saw在形式上没有任何差别。如果任务是进行词性的判断&#xff0c;把上面的词直接输入给神经网络&#xff0c;那么它肯定不能够正确分析。 想要正确分析词性&#xff0c;那么该…

CRM自动化意味着什么?企业如何从中受益?

客户关系管理&#xff08;CRM&#xff09;软件不再仅仅适用于大公司或销售周期长的行业&#xff0c;它越来越成为各种规模企业的重要工具。 在日常工作中&#xff0c;当你陷入流程的所有细节时&#xff0c;可能会产生不必要的工作。因此&#xff0c;如果你想要CRM提供的组织和…

Redis的持久化策略:RDB与AOF(面试题详解)

文章来源&#xff1a;Redis持久化的两种方式&#xff1a;RDB与AOF&#xff08;详解&#xff09;&#xff0c;订正了一些错误 一、概述&#xff1a; RDB和AOF持久化的由来&#xff1f; 因为Redis中的数据是基于内存的&#xff0c;所以如果出现服务器断电或者服务器宕机&#xf…

数据结构堆详解

[TOC]堆详解 一&#xff0c;堆 1.1堆的概念 堆的性质&#xff1a; 堆中某个节点的值总是不大于或不小于其父节点的值&#xff1b; 堆总是一棵完全二叉树。 1.2堆的存储模式 我们前面的文章提到过&#xff0c;二叉树的两种存储模式&#xff0c;一个是顺序存储&#xff0c;一…

【Java集合类面试八】、 介绍一下HashMap底层的实现原理

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a; 介绍一下HashMap底层的…

华为OD技术面试-最短距离矩阵(动态规划、广度优先)

背景 记录2023-10-21 晚华为OD三面的手撕代码题&#xff0c;当时没做出来&#xff0c;给面试官说了我的想法&#xff0c;评价&#xff1a;解法复杂了&#xff0c;只是简单的动态规范 或 广度优先算法&#xff0c;事后找资料记录实现方式。 题目 腐烂的橘子 问题描述&#xff…

怎样找外企/远程的工作

“如果你既不想卷&#xff0c;又不想参与职场的勾心斗角&#xff0c;也不算行业大牛&#xff0c;还不愿意冒太高风险&#xff0c;那还有一种渠道&#xff0c;就是找海外公司的远程工作&#xff0c;比如我有几个程序员朋友&#xff0c;都是拿着硅谷动辄 20w 刀的薪水&#xff0c…

头脑风暴之约瑟夫环问题

一 问题的引入 约瑟夫问题的源头完全可以命名为“自杀游戏”。本着和谐友爱和追求本质的目的&#xff0c;可以把问题描述如下&#xff1a; 现有n个人围成一桌坐下&#xff0c;编号从1到n&#xff0c;从编号为1的人开始报数。报数也从1开始&#xff0c;报到m人离席&#xff0c…

重生奇迹mu宠物带来不一样的体验

重生奇迹mu宠物有什么作用&#xff1f; 全新版本中更是推出了各种宠物&#xff0c;在玩游戏时还可以带着宠物&#xff0c;一起疯狂的刷怪等等&#xff0c;可以为玩家带来非常不错的游戏体验&#xff0c;那么下面就来给大家说说各种宠物适合做什么事情。 1、强化恶魔适合刷怪 …

爱创科技携手洽洽食品,探索渠道数字化最优解!

坚果的下半场&#xff0c;是从吃到喝。 消费升级大潮下&#xff0c;健康养生理念逐渐深入人心。以“天然健康”为核心的食品新消费潮流正加速形成&#xff0c;一个个打着“美味与营养”黄金设定的品类风口正被不断创建&#xff0c;其中人气有增无减的当属植物基饮品。据相关报告…

HarmonyOS鸿蒙原生应用开发设计- HarmonyOS Sans 字体

HarmonyOS设计文档中&#xff0c;为大家提供了独特的字体&#xff0c;开发者可以根据需要直接引用。 开发者直接使用官方提供的字体内容&#xff0c;既可以符合HarmonyOS原生应用的开发上架运营规范&#xff0c;又可以防止使用别人的字体侵权意外情况等&#xff0c;减少自主创…

【Linux驱动】Linux设备树(二)—— 添加设备树节点

了解了设备树的基本语法以后&#xff0c;就可以试着自己手动添加一个节点了&#xff0c;添加完节点以后&#xff0c;需要重新编译生成 .dtb 文件&#xff0c;然后保存到uboot的加载目录下。 目录 1、查看绑定信息文档 2、添加设备树节点 3、编译设备树文件(.dtb) 4、替换设…

JAVA基础(JAVA SE)学习笔记(七)面向对象编程(进阶)

前言 1. 学习视频&#xff1a; 尚硅谷Java零基础全套视频教程(宋红康2023版&#xff0c;java入门自学必备)_哔哩哔哩_bilibili 2023最新Java学习路线 - 哔哩哔哩 第二阶段&#xff1a;Java面向对象编程 6.面向对象编程&#xff08;基础&#xff09; 7.面向对象编程&…

IP协议(下)

目录 一、IP分片 1.为什么需要IP分片 2.IP报头信息 二、分片的组装 1.接收方怎么知道一个报文被分片了 2.同一个报文的分片怎么全部识别出来的 3.报文如何排序&#xff0c;如何得知报文有没有收全 4.怎么将各分片正确组装 5.怎么确定合成的报文是正确的 6.总结 三、…

SSM - Springboot - MyBatis-Plus 全栈体系(三十五)

第八章 项目实战 四、后台功能开发 2. 首页模块开发 2.1 查询首页分类 2.1.1 需求描述 进入新闻首页,查询所有分类并动态展示新闻类别栏位 2.1.2 接口描述 url 地址&#xff1a;portal/findAllTypes 请求方式&#xff1a;get 请求参数&#xff1a;无 响应数据&#xff…

【Python · PyTorch】数据基础

数据基础 1. 数据操作1.1 入门1.2 运算符1.3 广播机制1.4 索引和切片1.5 节省内存1.6 转化为其他Python对象 2. 数据预处理2.1 读取数据集2.2 处理缺失值2.3 转换为张量格式 本文介绍了PyTorch数据基础&#xff0c;Python版本3.9.0&#xff0c;代码于Jupyter Lab中运行&#xf…

红队专题-从零开始VC++C/S远程控制软件RAT-MFC-[5]客户端与服务端连接

红队专题 招募六边形战士队员端操作系统SystemInfo类获取系统信息发送系统信息头文件声明头文件调用 未找到来自 OleAcc.dll 的导入LINK 招募六边形战士队员 一起学习 代码审计、安全开发、web攻防、逆向等。。。 私信联系 端 发送连接->进入主线程->返回socket->…