SpringBoot AOP + Redis 延时双删功能实战

news2024/10/4 15:33:18

一、业务场景

在多线程并发情况下,假设有两个数据库修改请求,为保证数据库与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.coloradmin.cn/o/1124846.html

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

相关文章

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

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

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

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

一文解读 SmartX 超融合虚拟化下的网络 I/O 虚拟化技术

随着技术的不断发展&#xff0c;不少行业应用都对网络性能和隔离性有着越来越高的要求。例如&#xff1a; 低延迟&#xff1a;一些期货行业用户选择在期货公司机房托管服务器并自行编写交易程序&#xff0c;以实现对市场波动的快速&#xff08;微秒级&#xff09;反应。尤其是在…

DYC算法开发与测试(基于ModelBase实现)

ModelBase是经纬恒润开发的车辆仿真软件&#xff0c;包含两个大版本&#xff1a;动力学版本、智能驾驶版本。动力学版包含高精度动力学模型&#xff0c;能很好地复现车辆在实际道路中运行的各种状态变化&#xff0c;可用于乘用车、商用车动力底盘系统算法开发、控制器仿真测试&…

2023深圳CPSE安博会亮点指引

一、深圳安博会开展预告 10月25-28日&#xff0c;第十九届中国国际社会公共安全博览会&#xff08;简称“CPSE安博会”&#xff09;即将在深圳会展中心&#xff08;福田&#xff09;拉开帷幕。110,000㎡展示面积&#xff0c;130,000名专业观众&#xff0c;1000参展企业&#x…

如何制作自己的数字人

如何制作自己的数字人呢&#xff1f;不用担心平台的使用授权&#xff0c;也不用担心哪一天自己自媒体被号被无故封杀&#xff0c;那么SadTalker将是你的首选&#xff0c;他是完全开源的数字人软件&#xff0c;现已达到Apache2的授权&#xff0c;完全自由的开源软件。作者自己试…

Java中,字符串有多个空格如何截取

笔者在开发中&#xff0c;遇到需要对三方接口返回的报文进行处理&#xff0c;将对方填充的所有空格干掉&#xff0c;截取成list 使用正则表达式&#xff1a;split(“\s”) public static void main(String[] args) {String str "1111 22 33 888";// …

离线直线度测量仪的自动检测之旅!

离线直线度测量仪更适用于产品的抽检&#xff0c;虽然是离线检测设备&#xff0c;但需人工操作的工作非常少&#xff0c;是智能化的检测设备&#xff0c;本来来简单的介绍一下该测量设备。 离线直线度测量仪采用光电测头的直径和位置测量原理进行测量&#xff0c;测量仪采用成9…

【QT开发(11)】QT 线程QThread

Qt的线程支持与平台无关的&#xff1a; 线程类、一个线程安全的发送事件方式跨线程的信号-槽的关联 这使得可以从分利用多处理器机器&#xff0c;有效解决不冻结一份应用程序用户界面的情况下&#xff0c;处理一个耗时操作的问题。 文章目录 1、QThread 一个与平台无关的线程…

JAVAEE初阶相关内容第十六弹--网络编程

写在前 这一节的内容首先是对十五弹&#xff08;UDP回显服务器&#xff09;进行简单的改进&#xff0c;在这基础上开始介绍TCP流套接字编程。 目录 写在前 1.改进回显服务器 1.1完整代码实现 1.2运行输出结果 2.TCP流套接字编程 2.1ServerSocketAPI 2.2SocketAPI 3.TC…

JZ23链表中环的入口结点

JZ23链表中环的入口结点 思路: 采用双指针&#xff0c;设定快指针fast_p是慢指针slow_p的2倍&#xff0c;如果有环&#xff0c;则当两指针第一次相遇时慢指针一定不可能在环中走超过一圈&#xff0c;因此假设头结点到环的开头距离为a,环开头到第一次相遇节点的距离为b&#xf…

【数据分享】2023年我国科技型中小企业数据(免费获取/Excel格式/Shp格式)

企业是经济活动的参与主体&#xff0c;一个城市的企业数量决定了这个城市的经济发展水平&#xff01;之前我们分享过2023年高新技术企业数据&#xff08;可查看之前的文章获悉详情&#xff09;&#xff0c;我国专精特新“小巨人”企业数据&#xff08;可查看之前的文章获悉详情…

基于深度学习网络的手势识别算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 clc; clear; close all; warning off; addpath(genpath(pwd)); rng(default)load gnet.mat[Pr…

32二叉树——DFS深度优先遍历

目录 深度优先算法&#xff08;Depth-First Search&#xff0c;DFS&#xff09; LeetCode之路——102. 二叉树的层序遍历 分析 深度优先算法&#xff08;Depth-First Search&#xff0c;DFS&#xff09; DFS是一种用于遍历或搜索树状数据结构的算法&#xff0c;其中它首先探…

解决“您点击的链接已过期”;The Link You Followed Has Expired的问题

今天WP碰到一个坑。无论发布文章还是更新插件、更换主题都是这么一种状态“您点击的链接已过期”&#xff1b;The Link You Followed Has Expired 百度出来的答案都是修改post_max_size 方法1. 通过functions.php文件修复 这种方法更容易&#xff0c;只需将以下代码添加到Wor…

程序可以创建多少个用户界面对象?

有人提到这样一个问题&#xff1a;”一个程序最多可以注册多少个窗口类?” 问题的答案不是一个具体的数字。因为大多数用户界面对象都来自一个共享的内存池&#xff0c;我们称之为”桌面堆内存”。尽管我们可以计算一个最大的理论值&#xff0c;但是在实际的场景中&#xff0…

模仿企业微信界面

备注&#xff1a;未实现相关功能&#xff0c;仅模仿界面&#xff0c;不能作为商业用途&#xff0c;若有侵权&#xff0c;请联系删除。 <Window x:Class"模仿企业微信界面.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"…

身为网络工程师必考证书:华为HCIP认证!

为了找到一份心仪的工作&#xff0c;有的人选择入职前先自我提升考取贴合岗位需求的从业相关证书&#xff0c;进而面试并开始工作&#xff0c;有的人选择先从“基层”开始积累经验为首&#xff0c;先进入行业内夯实基础&#xff0c;学习和考证作为了工作“平稳”后的计划。 很…

Qt Creater 设计的登录注册界面 使用SQLite数据库

Qt Creater 设计的登录注册界面 使用SQLite数据库 案例截图 登录页面 注册页面 项目目录结构截图 代码 main.cpp #include "mainwindow.h"#include <QApplication>int main(int argc, char *argv[]) {QApplication a(argc, argv);MainWindow w;//第一个是…