一、业务场景
在多线程并发情况下,假设有两个数据库修改请求,为保证数据库与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为空,有请求进来时,便会去访问数据库,此时数据库中的数据已是更新后的数据,保证了数据的一致性。
基于 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,新增一条数据
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
❞
-
数据库
+关注
关注
7文章
3750浏览量
64217 -
Redis
+关注
关注
0文章
370浏览量
10825 -
SpringBoot
+关注
关注
0文章
173浏览量
159
原文标题:SpringBoot AOP + Redis 延时双删功能实战
文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论