springboot自定义注解+aop+redis实现延时双删
redis作为用的非常多的缓存数据库,在多线程场景下,可能会出现数据库与redis数据不一致的现象
数据不一致的现象:https://blog.csdn.net/m0_73700925/article/details/133447466
这里采用aop+redis来解决这个方法:
- 删除缓存
- 更新数据库
- 延时一定时间,比如500ms
- 删除缓存
这里之所以要延时一段时间再删除,是为了避免多线程情况下,更新数据库的操作还没执行,就执行了第二次删除缓存的操作,此时如果有请求进来,就会读取数据库并将数据写入缓存,这时再更新数据库就会导致数据不一致的问题
两次删除缓存是因为第一次删除缓存后,这时如果有请求进来,得到了数据并写入redis,然后再更新数据库,就会导致数据不一致
- 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface ClearAndReloadCache {
String name() default "";
}
- 编写切面,以自定义注解作为切入点
@Aspect
@Component
public class ClearAndReloadCacheAspect {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Around("@annotation(clearAndReloadCache)")
public Object innerAround(ProceedingJoinPoint proceedingJoinPoint, ClearAndReloadCache clearAndReloadCache) throws Throwable {
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();
// 延时双删中的第一次删除缓存
Set<String> keys = stringRedisTemplate.keys("*" + name + "*");
stringRedisTemplate.delete(keys);
Object proceed = null;
// 执行业务层代码
proceed = proceedingJoinPoint.proceed();
// 执行延迟双删中的第二次删除缓存
// 开启新线程是为了避免主线程堵塞等待
new Thread(() -> {
try {
Thread.sleep(1000);
Set<String> keys2 = stringRedisTemplate.keys("*" + name + "*");
stringRedisTemplate.delete(keys2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
return proceed;
}
}
切面也可以写成这样,更方便理解
@Aspect
@Component
public class ClearAndReloadCacheAspect {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Pointcut("@annotation(com.toptolink.iot.permission.annotation.ClearAndReloadCache)")
public void pointCut(){
}
@Around("pointCut()")
public Object innerAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
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();
// 延时双删中的第一次删除缓存
Set<String> keys = stringRedisTemplate.keys("*" + name + "*");
stringRedisTemplate.delete(keys);
Object proceed = null;
// 执行业务层代码
proceed = proceedingJoinPoint.proceed();
// 执行延迟双删中的第二次删除缓存
// 开启新线程是为了避免主线程堵塞等待
new Thread(() -> {
try {
Thread.sleep(1000);
Set<String> keys2 = stringRedisTemplate.keys("*" + name + "*");
stringRedisTemplate.delete(keys2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
return proceed;
}
}
- 业务层代码
controller
@ApiOperation("小程序小程序平台通用-工单详情")
@GetMapping("/queryWorkOrderDetail")
@PreAuthorize(Permissions.YQZ_MAINTAIN)
public DataResponseBody queryWorkOrderDetail(@RequestParam Long id) {
return new DataResponseBody(iMaintainService.queryWorkOrderDetail(id));
}
/**
* @return
*/
@GetMapping("/TODOupdateById")
@PreAuthorize(Permissions.YQZ_MAINTAIN)
@ClearAndReloadCache(name = "getById")
public DataResponseBody TODOupdateById(Long id, String customerFullName) {
return new DataResponseBody(iMaintainService.TODOupdateById(id, customerFullName));
}
我在queryWorkOrderDetail()中将查到的数据存入了redis中,redisService.set("getById"+id, json);
- 测试
首先需要通过idea多开启一个程序,用于模拟多线程
然后通过打断点的方式
- 首先调用查询接口,此时会将数据存入redis中
- 然后调用修改接口,进入debug模式,当第一次删除缓存后,不要往下走
- 再次调用查询接口,用于模拟多线程情况下的数据不一致情况
- 这时redis又会存入数据
- 接着就是更新数据库的操作
- 此时如果没有第二次删除缓存,就会出现数据不一致了
- 所以第二次删除缓存是很有必要的
原文地址:https://blog.csdn.net/Xyouzi/article/details/135633299
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!