springboot自定义注解+aop+redis实现延时双删

发布时间:2024年01月16日

redis作为用的非常多的缓存数据库,在多线程场景下,可能会出现数据库与redis数据不一致的现象

数据不一致的现象:https://blog.csdn.net/m0_73700925/article/details/133447466

这里采用aop+redis来解决这个方法:

  1. 删除缓存
  2. 更新数据库
  3. 延时一定时间,比如500ms
  4. 删除缓存

这里之所以要延时一段时间再删除,是为了避免多线程情况下,更新数据库的操作还没执行,就执行了第二次删除缓存的操作,此时如果有请求进来,就会读取数据库并将数据写入缓存,这时再更新数据库就会导致数据不一致的问题

两次删除缓存是因为第一次删除缓存后,这时如果有请求进来,得到了数据并写入redis,然后再更新数据库,就会导致数据不一致

  1. 自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface ClearAndReloadCache {
    String name() default "";
}
  1. 编写切面,以自定义注解作为切入点
@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;
    }
}
  1. 业务层代码
    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);

  1. 测试
    首先需要通过idea多开启一个程序,用于模拟多线程
    在这里插入图片描述
    在这里插入图片描述
    然后通过打断点的方式
    在这里插入图片描述
  • 首先调用查询接口,此时会将数据存入redis中
  • 然后调用修改接口,进入debug模式,当第一次删除缓存后,不要往下走
  • 再次调用查询接口,用于模拟多线程情况下的数据不一致情况
  • 这时redis又会存入数据
  • 接着就是更新数据库的操作
  • 此时如果没有第二次删除缓存,就会出现数据不一致了
  • 所以第二次删除缓存是很有必要的
文章来源:https://blog.csdn.net/Xyouzi/article/details/135633299
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。