实战解析:SpringBoot AOP与Redis结合实现延时双删功能

news2024/11/22 16:13:53

目录

一、业务场景

1、此时存在的问题

2、解决方案

3、为何要延时500毫秒?

4、为何要两次删除缓存?

二、代码实践

1、引入Redis和SpringBoot AOP依赖

2、编写自定义aop注解和切面

3、application.yml

4、user.sql脚本

5、UserController

6、UserService

三、测试验证

1、ID=10,新增一条数据

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

3、第一次访问ID为10

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

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

6、采用第二次删除


一、业务场景

在多线程并发情况下,假设有两个数据库修改请求,为保证数据库与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依赖

<!-- redis使用 -->
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- aop -->
<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
public class ClearAndReloadCacheAspect {

@Autowired
private StringRedisTemplate stringRedisTemplate;

/**
* 切入点
*切入点,基于注解实现的切入点  加上该注解的都是Aop切面的切入点
*
*/

@Pointcut("@annotation(com.pdh.cache.ClearAndReloadCache)")
public void pointCut(){

}
/**
* 环绕通知
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
* 环绕通知第一个参数必须是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

# 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.sql脚本

用于生产测试数据

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `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
-- ----------------------------
INSERT INTO `user` VALUES (1, '张三');
INSERT INTO `user` VALUES (2, '李四');
INSERT INTO `user` VALUES (3, '王二');
INSERT INTO `user` VALUES (4, '麻子');
INSERT INTO `user` VALUES (5, '王三');
INSERT INTO `user` 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(3001,"操作数据库失败");
    }

    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(3001, "操作数据库失败");
    }

    public Result update(User user){
        int i = userMapper.updateById(user);
        if(i > 0)
            return Result.success(i);
        return Result.fail(3001,"操作数据库失败");
    }
}

三、测试验证

1、ID=10,新增一条数据

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

3、第一次访问ID为10

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

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

数据库和缓存不一致验证方案:

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

6、采用第二次删除

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

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

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

相关文章

计算机网络——TCP和UDP协议

目录 前言 前篇 引言 TCP与UDP之间的区别 TCP 三次握手 为什么要三次握手而不是两次握手&#xff1f; 丢包问题与乱序问题的解决 四次挥手 为什么客户端需要等待超时时间&#xff1f; UDP协议 TCP和UDP的主要区别 前言 本博客是博主用于复习计算机网络的博客&…

【Jenkins】Jenkins自动化工具介绍

目录 技术背景常规的手动打包步骤 Jenkins简介起源与发展Jenkins的核心价值1.自动化1.1代码构建1.2测试自动化1.3自动部署 2.持续集成与持续部署CI/CD的概念如何减少集成问题更快速地发布软件版本 Jenkins优势Jenkins的主要竞争对手Travis CI:CircleCI:GitLab CI: Jenkins与其他…

阿里云9元服务器租用收费价格表_免费云服务器领取

2024年最新阿里云服务器租用费用优惠价格表&#xff0c;轻量2核2G3M带宽轻量服务器一年61元&#xff0c;折合5元1个月&#xff0c;新老用户同享99元一年服务器&#xff0c;2核4G5M服务器ECS优惠价199元一年&#xff0c;2核4G4M轻量服务器165元一年&#xff0c;2核4G服务器30元3…

出海企业如何从海外云手机中受益?

随着全球化的推进&#xff0c;越来越多的企业开始将目光投向海外市场。然而&#xff0c;不同国家和地区的网络环境、政策限制&#xff0c;以及语言文化的差异&#xff0c;给出海企业的市场拓展带来了诸多挑战。在这一背景下&#xff0c;海外云手机作为一种新兴解决方案&#xf…

等保测评2.0——网络安全等级保护测评的初步了解

一、什么是网络安全等级保护测评&#xff1f; 二、网络安全等级保护&#xff0c;保护的是什么&#xff1f; 等级保护对象&#xff1a;网络安全等级保护工作直接作用的对象。&#xff08;注&#xff1a;主要包括信息系统、通信网络设施和数据资源等&#xff09; 计算机信息系统…

设计模式代码实战-抽象工厂模式

1、问题描述 小明家新开了两个工厂用来生产家具&#xff0c;一个生产现代风格的沙发和椅子&#xff0c;一个生产古典风格的沙发和椅子&#xff0c;现在工厂收到了一笔订单&#xff0c;请你帮他设计一个系统&#xff0c;描述订单需要生产家具的信息。 输入试例&#xff1a; 3 …

【设计模式】六大设计原则

设计原则 研究 23 种设计模式是困难的&#xff0c;甚至是没必要的六大设计原则零、单一职责原则开闭原则里氏代换原则依赖倒置原则接口隔离原则迪米特法则合成复用原则 研究 23 种设计模式是困难的&#xff0c;甚至是没必要的 设计模式有23种&#xff0c;我认为对普通人来说想…

【Python】读取时间判定操作次数问题和一些解决办法

几种类 datetime.striptime() 计算两个字符串之间的时间差 datetime.striptime()计算两个字符串之间的时间差 datatime类提供函数处理日期和时间 Striptime()分析字符串值以固定格式表示时间然后存储为函数参数 输出就是&#xff1a; time.sleep() time模块打印时间按照对…

python--递归算法篇

1、给定一个包含n1个整数的数组nums&#xff0c;其数字在1到n之间&#xff08;包含1和n&#xff09;&#xff0c; 可知至少存在一个重复的整数&#xff0c;假设只有一个重复的整数&#xff0c;请找出这个重复的数 def repeat(ls:list) -> list:#把个数超过1的数&#xff0c…

AutoCAD 2024 安装注册教程

前言 大家好&#xff0c;我是梁国庆。 本篇分享的安装包是 AutoCAD 的全新版本——AutoCAD 2024&#xff0c;下文安装教程中简称 AutoCAD。 时间&#xff1a;2024年4月8日。 获取 AutoCAD 安装包 我已将本篇所使用的安装包打包上传至百度云&#xff0c;扫描下方二维码关注…

014:vue3 van-list van-pull-refresh实现上拉加载,下拉刷新

文章目录 1. 实现上拉加载&#xff0c;下拉刷新效果2. van-list&#xff0c;van-pull-refresh组件详解2.1 van-list组件2.2 van-pull-refresh组件 3. 完整案例4. 坑点&#xff1a;加载页面会一直调用加载接口 1. 实现上拉加载&#xff0c;下拉刷新效果 通过下拉刷新加载下一页…

DMA的认识

DMA介绍 Q:什么是DMA&#xff1f; DMA( Direct Memory Access&#xff0c;直接存储器访问 ) 提供在 外设与内存 、 存储器和存储器 、 外设 与外设 之间的 高速数据传输 使用。它允许不同速度的硬件装置来沟通&#xff0c;而不需要依赖于 CPU &#xff0c;在这个时间中&am…

Matlab与ROS(1/2)---Message(三)

0. 简介 消息是ROS中交换数据的主要容器。主题和服务使用消息在节点之间传输数据。为了标识其数据结构&#xff0c;每条消息都有一个消息类型。例如&#xff0c;来自激光扫描仪的传感器数据通常以sensor_msgs/LaserScan类型的消息发送。每种消息类型标识消息中包含的数据元素。…

汽车制造业PMC组态应用最佳实践

01案例及行业介绍 汽车制造工业是我国国民经济的重要支柱产业&#xff0c;汽车制造工厂一般包含冲压、焊装、涂装、总装四大车间。每辆汽车的生产过程被分解成很多加工任务下发给各个车间进行完成。车辆从冲压车间开始到总装车间结束一直进行不同类型的工序加工。 PMC即生产控…

docker 上达梦导入dump文件报错:本地编码:PG GBK,导入女件编码:PGGB18030

解决方案&#xff1a; 第一步进入达梦数据容器内部 docker exec -it fc316f88caff /bin/bash 第二步&#xff1a;在容器中 /opt/dmdbms/bin目录下 执行命令 cd /opt/dmdbms/bin./dimp USERIDSYSDBA/SYSDBA001 FILE/opt/dmdbms/ZFJG_LJ20240407.dmp SCHEMASZFJG_LJUSERIDSYSD…

ChatGPT基础(二) ChatGPT的使用和调优

文章目录 ChatGPT的特性采用关键词进行提问给ChatGPT指定身份提升问答质量的策略1.表述方式上的优化2.用"继续"输出长内容3.营造场景4.由浅入深&#xff0c;提升问题质量5.预设回答框架和风格 ChatGPT的特性 1.能够联系上下文进行回答 ChatGPT回答问题是有上下文的&…

微服务学习 Eureka注册中心

服务调用时候出现问题&#xff0c;当服务者很多时候&#xff0c;比如不同的端口。消费者如何找到服务者的地址&#xff1f;又如何判断服务者是否健康。 Eureka基本原理&#xff1a; 总结:如果有多个服务提供者&#xff0c;消费者该如何选择&#xff1f; 搭建Eureka注册中心: 1.…

django基于python的法院执法案件管理系统

本课题使用Python语言进行开发。代码层面的操作主要在PyCharm中进行&#xff0c;将系统所使用到的表以及数据存储到MySQL数据库中&#xff0c;方便对数据进行操作本课题基于WEB的开发平台&#xff0c;设计的基本思路是&#xff1a; 框架&#xff1a;django/flask 后端&#xff…

【YOLOv9改进[损失函数]】使用MPDIou回归损失函数帮助YOLOv9模型更优秀

本文中&#xff0c;第一部分概述了各种回归损失函数&#xff0c;当然也包括了今天的主角MPDIou。第二部分内容为在YOLOv9中使用MPDIou回归损失函数的方法。 1 回归损失函数&#xff08;Bounding Box Regression Loss&#xff09; 边界框回归损失计算的方法包括GIoU、DIoU、CI…

纯css实现switch开关

代码比较简单&#xff0c;有需要直接在下边粘贴使用吧~ html: <div class"switch-box"><input id"switch" type"checkbox"><label></label></div> css&#xff1a; .switch-box {position: relative;height: 25px…