0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

jf_ro2CN3Fa 来源:CSDN 2023-10-13 16:08 次阅读


一、业务场景

在多线程并发情况下,假设有两个数据库修改请求,为保证数据库与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为空,有请求进来时,便会去访问数据库,此时数据库中的数据已是更新后的数据,保证了数据的一致性。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

二、代码实践

1、引入Redis和SpringBoot AOP依赖



org.springframework.boot
spring-boot-starter-data-redis



org.springframework.boot
spring-boot-starter-aop

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

ClearAndReloadCache延时双删注解

/**
*延时双删
**/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public@interfaceClearAndReloadCache{
Stringname()default"";
}

ClearAndReloadCacheAspect延时双删切面

@Aspect
@Component
publicclassClearAndReloadCacheAspect{

@Autowired
privateStringRedisTemplatestringRedisTemplate;

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

@Pointcut("@annotation(com.pdh.cache.ClearAndReloadCache)")
publicvoidpointCut(){

}
/**
*环绕通知
*环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
*环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
*@paramproceedingJoinPoint
*/
@Around("pointCut()")
publicObjectaroundAdvice(ProceedingJoinPointproceedingJoinPoint){
System.out.println("-----------环绕通知-----------");
System.out.println("环绕通知的目标方法名:"+proceedingJoinPoint.getSignature().getName());

Signaturesignature1=proceedingJoinPoint.getSignature();
MethodSignaturemethodSignature=(MethodSignature)signature1;
MethodtargetMethod=methodSignature.getMethod();//方法对象
ClearAndReloadCacheannotation=targetMethod.getAnnotation(ClearAndReloadCache.class);//反射得到自定义注解的方法对象

Stringname=annotation.name();//获取自定义注解的方法对象的参数即name
Setkeys=stringRedisTemplate.keys("*"+name+"*");//模糊定义key
stringRedisTemplate.delete(keys);//模糊删除redis的key值

//执行加入双删注解的改动数据库的业务即controller中的方法业务
Objectproceed=null;
try{
proceed=proceedingJoinPoint.proceed();
}catch(Throwablethrowable){
throwable.printStackTrace();
}

//开一个线程延迟1秒(此处是1秒举例,可以改成自己的业务)
//在线程中延迟删除同时将业务代码的结果返回这样不影响业务代码的执行
newThread(()->{
try{
Thread.sleep(1000);
Setkeys1=stringRedisTemplate.keys("*"+name+"*");//模糊删除
stringRedisTemplate.delete(keys1);
System.out.println("-----------1秒钟后,在线程中延迟删除完毕-----------");
}catch(InterruptedExceptione){
e.printStackTrace();
}
}).start();

returnproceed;//返回业务代码的值
}
}

3、application.yml

server:
port:8082

spring:
#redissetting
redis:
host:localhost
port:6379

#cachesetting
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/>

#mpsetting
mybatis-plus:
mapper-locations:classpath*:com/pdh/mapper/*.xml
global-config:
db-config:
table-prefix:
configuration:
#logofsql
log-impl:org.apache.ibatis.logging.stdout.StdOutImpl
#hump
map-underscore-to-camel-case:true

4、user_db.sql脚本

用于生产测试数据

DROPTABLEIFEXISTS`user_db`;
CREATETABLE`user_db`(
`id`int(4)NOTNULLAUTO_INCREMENT,
`username`varchar(32)CHARACTERSETutf8COLLATEutf8_general_ciNOTNULL,
PRIMARYKEY(`id`)USINGBTREE
)ENGINE=InnoDBAUTO_INCREMENT=8CHARACTERSET=utf8COLLATE=utf8_general_ciROW_FORMAT=Dynamic;

------------------------------
--Recordsofuser_db
------------------------------
INSERTINTO`user_db`VALUES(1,'张三');
INSERTINTO`user_db`VALUES(2,'李四');
INSERTINTO`user_db`VALUES(3,'王二');
INSERTINTO`user_db`VALUES(4,'麻子');
INSERTINTO`user_db`VALUES(5,'王三');
INSERTINTO`user_db`VALUES(6,'李三');

5、UserController

/**
*用户控制层
*/
@RequestMapping("/user")
@RestController
publicclassUserController{
@Autowired
privateUserServiceuserService;

@GetMapping("/get/{id}")
@Cache(name="getmethod")
//@Cacheable(cacheNames={"get"})
publicResultget(@PathVariable("id")Integerid){
returnuserService.get(id);
}

@PostMapping("/updateData")
@ClearAndReloadCache(name="getmethod")
publicResultupdateData(@RequestBodyUseruser){
returnuserService.update(user);
}

@PostMapping("/insert")
publicResultinsert(@RequestBodyUseruser){
returnuserService.insert(user);
}

@DeleteMapping("/delete/{id}")
publicResultdelete(@PathVariable("id")Integerid){
returnuserService.delete(id);
}
}

6、UserService

/**
*service层
*/
@Service
publicclassUserService{

@Resource
privateUserMapperuserMapper;

publicResultget(Integerid){
LambdaQueryWrapperwrapper=newLambdaQueryWrapper<>();
wrapper.eq(User::getId,id);
Useruser=userMapper.selectOne(wrapper);
returnResult.success(user);
}

publicResultinsert(Useruser){
intline=userMapper.insert(user);
if(line>0)
returnResult.success(line);
returnResult.fail(888,"操作数据库失败");
}

publicResultdelete(Integerid){
LambdaQueryWrapperwrapper=newLambdaQueryWrapper<>();
wrapper.eq(User::getId,id);
intline=userMapper.delete(wrapper);
if(line>0)
returnResult.success(line);
returnResult.fail(888,"操作数据库失败");
}

publicResultupdate(Useruser){
inti=userMapper.updateById(user);
if(i>0)
returnResult.success(i);
returnResult.fail(888,"操作数据库失败");
}
}

三、测试验证

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

1ab9e8f0-699e-11ee-939d-92fbcf53809c.jpg

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

1acd49a4-699e-11ee-939d-92fbcf53809c.jpg

3、第一次访问ID为10

1ae5f3b4-699e-11ee-939d-92fbcf53809c.jpg

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

1b1661e8-699e-11ee-939d-92fbcf53809c.jpg

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

1b28ddb4-699e-11ee-939d-92fbcf53809c.jpg

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

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

1b33ac9e-699e-11ee-939d-92fbcf53809c.png1b415498-699e-11ee-939d-92fbcf53809c.jpg

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

1b48ce6c-699e-11ee-939d-92fbcf53809c.jpg

四、代码工程及地址

核心代码红色方框所示

https://gitee.com/jike11231/redisDemo.git

1b58a9d6-699e-11ee-939d-92fbcf53809c.png


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 数据库
    +关注

    关注

    7

    文章

    3591

    浏览量

    63373
  • Redis
    +关注

    关注

    0

    文章

    362

    浏览量

    10496
  • SpringBoot
    +关注

    关注

    0

    文章

    172

    浏览量

    106

原文标题:SpringBoot AOP + Redis 延时双删功能实战

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Spring AOP如何破解java应用

    预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,从另一视角扩展了对面向对象编程的形式。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度
    的头像 发表于 09-25 11:16 600次阅读
    Spring <b class='flag-5'>AOP</b>如何破解java应用

    求...

    本帖最后由 871881392 于 2014-11-28 08:21 编辑 求
    发表于 11-24 14:56

    Redis Stream应用案例

    的基本使用介绍和设计理念可以看我之前的一篇文章(Redis Stream简介)。Redis Stream本质上是在Redis内核上(非Redis Module)实现的一个消息发布订阅
    发表于 06-26 17:15

    SpringBoot知识总结

    SpringBoot干货学习总结
    发表于 08-01 10:40

    Spring boot中Redis的使用

    【本人秃顶程序员】springboot专辑:Spring boot中Redis的使用
    发表于 03-27 11:42

    Java程序员笔记之mybatis结合redis实战二级缓存

    Java程序员笔记——mybatis结合redis实战二级缓存
    发表于 06-10 09:15

    怎样去使用springboot

    怎样去使用springboot呢?学习springboot需要懂得哪些?
    发表于 10-25 07:13

    SpringBoot+Redis实现点赞功能的缓存和定时持久化(附源码)

    用户对浏览内容进行【点赞/取赞】,并发送【点赞/取赞】请求到后端,这些信息先存入Redis中缓存,再每隔两小时将Redis中的内容直接写入数据库持久化存储。
    的头像 发表于 02-09 16:38 3972次阅读

    基于SpringBoot+Redis的转盘抽奖

    基于SpringBoot+Redis等技术实现转盘抽奖活动项目,含前端、后台及数据库文件
    的头像 发表于 02-28 14:24 985次阅读
    基于<b class='flag-5'>SpringBoot+Redis</b>的转盘抽奖

    什么是 SpringBoot

    本文从为什么要有 `SpringBoot`,以及 `SpringBoot` 到底方便在哪里开始入手,逐步分析了 `SpringBoot` 自动装配的原理,最后手写了一个简单的 `start` 组件,通过
    的头像 发表于 04-07 11:28 1029次阅读
    什么是 <b class='flag-5'>SpringBoot</b>?

    如何在SpringBoot中解决Redis的缓存穿透等问题

    今天给大家介绍一下如何在SpringBoot中解决Redis的缓存穿透、缓存击穿、缓存雪崩的问题。
    的头像 发表于 04-28 11:35 522次阅读

    SpringBoot拦截器与统一功能处理实战

    Spring AOP是一个基于面向切面编程的框架,用于将横切性关注点(如日志记录、事务管理)与业务逻辑分离,通过代理对象将这些关注点织入到目标对象的方法执行前后、抛出异常或返回结果时等特定位置执行,从而提高程序的可复用性、可维护性和灵活性。
    的头像 发表于 08-27 10:44 629次阅读
    <b class='flag-5'>SpringBoot</b>拦截器与统一<b class='flag-5'>功能</b>处理<b class='flag-5'>实战</b>

    如何用Springboot整合Redis

    本篇文件我们来介绍如何用Springboot整合Redis。 1、Docker 安装 Redis 1.1 下载镜像 docker pull redis: 6 . 2 . 6 1.2 创
    的头像 发表于 10-08 14:56 328次阅读
    如何用<b class='flag-5'>Springboot</b>整合<b class='flag-5'>Redis</b>

    AOP要怎么使用

    AOP(Aspect-Oriented Programming)经常会出现在面试过程中,AOP到底有没有用,要怎么使用呢。本篇来一起拨开迷雾! 1 第一个AOP示例 我们会一次将所有的通知类型都覆盖
    的头像 发表于 10-09 16:18 336次阅读
    <b class='flag-5'>AOP</b>要怎么使用

    一个注解搞定SpringBoot接口防刷

    技术要点:springboot的基本知识,redis基本操作,
    的头像 发表于 11-28 10:46 221次阅读