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

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又会存入数据
  • 接着就是更新数据库的操作
  • 此时如果没有第二次删除缓存,就会出现数据不一致了
  • 所以第二次删除缓存是很有必要的

相关推荐

  1. springboot redission 定义注解实现分布式锁

    2024-01-16 21:02:05       41 阅读
  2. springboot aop 定义注解形式

    2024-01-16 21:02:05       56 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-01-16 21:02:05       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-01-16 21:02:05       100 阅读
  3. 在Django里面运行非项目文件

    2024-01-16 21:02:05       82 阅读
  4. Python语言-面向对象

    2024-01-16 21:02:05       91 阅读

热门阅读

  1. Redis面试题14

    2024-01-16 21:02:05       58 阅读
  2. Nginx Ingress轻松上手 | Kubernetes服务管理指南

    2024-01-16 21:02:05       57 阅读
  3. C++面试之线程池、智能指针、设计模式

    2024-01-16 21:02:05       49 阅读
  4. Redis面试题13

    2024-01-16 21:02:05       50 阅读
  5. 二叉树遍历C++

    2024-01-16 21:02:05       62 阅读
  6. Vue2:利用watch和localStorage存储数据案例

    2024-01-16 21:02:05       58 阅读
  7. JPA查询PostgreSQL行排序问题

    2024-01-16 21:02:05       50 阅读
  8. Socket-Worker模式

    2024-01-16 21:02:05       63 阅读