AOP+Redisson 延时队列,实现缓存延时双删策略

一、缓存延时双删

关于缓存和数据库中的数据保持一致有很多种方案,但不管是单独在修改数据库之前,还是之后去删除缓存都会有一定的风险导致数据不一致。而延迟双删是一种相对简单并且收益比较高的实现最终一致性的方式,即在删除缓存之后,间隔一个短暂的时间后再删除缓存一次。这样可以避免并发更新时,假如缓存在第一次被删除后,被其他线程读到旧的数据更新到了缓存,第二次删除还可以补救,从而时间最终一致性。

实现延时双删的方案也有很多,有本地用 Thread.sleep(); 睡眠的方式做延时,也有借助第三方消息中间件做延时消息等等,本文基于 Redisson 中的延时队列进行实验。

Redisson 中提供了 RDelayedQueue 可以迅速实现延时消息,本文所使用的 Redisson 版本为 3.19.0

二、Redisson 实现延时消息

新建 SpringBoot 项目,在 pom 中加入下面依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.19.0</version>
</dependency>

yum 配置中,增加 redis 的信息:

yml 复制代码
spring:
  redis:
    timeout: 6000
    password:
    cluster:
      max-redirects:
      nodes:
        - 192.168.72.120:7001
        - 192.168.72.121:7001
        - 192.168.72.122:7001

声明 RedissonClient

java 复制代码
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient getRedisson(RedisProperties redisProperties) {
        Config config = new Config();
        String[] nodes = redisProperties.getCluster().getNodes().stream().filter(StringUtils::isNotBlank).map(node -> "redis://" + node).collect(Collectors.toList()).toArray(new String[]{});
        ClusterServersConfig clusterServersConfig = config.useClusterServers().addNodeAddress(nodes);
        if (StringUtils.isNotBlank(redisProperties.getPassword())) {
            clusterServersConfig.setPassword(redisProperties.getPassword());
        }
        clusterServersConfig.setConnectTimeout((int) (redisProperties.getTimeout().getSeconds() * 1000));
        clusterServersConfig.setScanInterval(2000);
        return Redisson.create(config);
    }
}

延时队列实现延时消息:

java 复制代码
@Slf4j
@Component
public class MsgQueue {

    @Resource
    RedissonClient redissonClient;

    public static final String QUEUE_KEY = "DELAY-QUEUE";

    // 发送消息
    public void send(String msg, Long time, TimeUnit unit) {
        // 获取队列
        RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(QUEUE_KEY);
        // 延时队列
        RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
        // 添加数据
        delayedQueue.offer(msg, time, unit);
    }

    // 消息监听
    @PostConstruct
    public void listen() {
        CompletableFuture.runAsync(() -> {
            RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(MsgQueue.QUEUE_KEY);
            log.info("延时消息监听!");
            while (true) {
                try {
                    consumer(blockingQueue.take());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    // 消费消息
    public void consumer(String msg) {
        log.info("收到延时消息: {} , 当前时间: {} ", msg, LocalDateTime.now().toString());
    }
    
}

测试延时消息:

java 复制代码
@Slf4j
@RestController
@RequestMapping("/msg")
public class MsgController {
    @Resource
    MsgQueue queue;

    @GetMapping("/test")
    public void test() {
        String msg = "你好";
        queue.send(msg, 5L, TimeUnit.SECONDS);
    }

}

上面发送了延时5秒的消息,运行后可以看到日志:

三、AOP+延时队列,实现延时双删策略

缓存延时删除队列:

java 复制代码
@Slf4j
@Component
public class CacheQueue {

    @Resource
    RedissonClient redissonClient;

    public static final String QUEUE_KEY = "CACHE-DELAY-QUEUE";

    // 延时删除
    public void delayedDeletion(String key, Long time, TimeUnit unit) {
        RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(QUEUE_KEY);
        RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(blockingQueue);
        log.info("延时删除key: {} , 当前时间: {} ", key, LocalDateTime.now().toString());
        delayedQueue.offer(key, time, unit);
    }

    // 消息监听
    @PostConstruct
    public void listen() {
        CompletableFuture.runAsync(() -> {
            RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(CacheQueue.QUEUE_KEY);
            while (true) {
                try {
                    consumer(blockingQueue.take());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }

    // 消费消息
    public void consumer(String key) {
        log.info("删除key: {} , 当前时间: {} ", key, LocalDateTime.now().toString());
        redissonClient.getBucket("key").delete();
    }
}

定义缓存和删除缓存注解:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface Cache {
    String name() default "";
}
java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface DeleteCache {
    String name() default "";
}

缓存AOP逻辑:

java 复制代码
@Aspect
@Component
public class CacheAspect {

    @Resource
    RedissonClient redissonClient;

    private final Long validityTime = 2L;


    @Pointcut("@annotation(com.bxc.retrydemo.anno.Cache)")
    public void pointCut() {

    }

    @Around("pointCut()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        Cache ann = ((MethodSignature) pjp.getSignature()).getMethod().getDeclaredAnnotation(Cache.class);
        if (Objects.nonNull(ann) && StringUtils.isNotBlank(ann.name())) {
            Object proceed = redissonClient.getBucket(ann.name()).get();
            if (Objects.nonNull(proceed)){
                return proceed;
            }
        }
        Object proceed = pjp.proceed();
        if (Objects.nonNull(ann) && StringUtils.isNotBlank(ann.name())) {
            redissonClient.getBucket(ann.name()).set(proceed, validityTime, TimeUnit.HOURS);
        }
        return proceed;
    }
}

延时双删 AOP 逻辑:

java 复制代码
@Aspect
@Component
public class DeleteCacheAspect {

    @Resource
    RedissonClient redissonClient;

    @Resource
    CacheQueue cacheQueue;

    private final Long delayedTime = 3L;


    @Pointcut("@annotation(com.bxc.retrydemo.anno.DeleteCache)")
    public void pointCut() {

    }

    @Around("pointCut()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        // 第一次删除缓存
        DeleteCache ann = ((MethodSignature) pjp.getSignature()).getMethod().getDeclaredAnnotation(DeleteCache.class);
        if (Objects.nonNull(ann) && StringUtils.isNotBlank(ann.name())) {
            redissonClient.getBucket(ann.name()).delete();
        }
        // 执行业务逻辑
        Object proceed = pjp.proceed();
        //延时删除
        if (Objects.nonNull(ann) && StringUtils.isNotBlank(ann.name())) {
            cacheQueue.delayedDeletion(ann.name(), delayedTime, TimeUnit.SECONDS);
        }
        return proceed;
    }
}

伪业务逻辑使用:

java 复制代码
@Slf4j
@Service
public class MsgServiceImpl implements MsgService {

    @Cache(name = "you key")
    @Override
    public String find() {
        // 数据库操作
        // ....
        // 数据库操作结束
        return "数据结果";
    }


    @DeleteCache(name = "you key")
    @Override
    public void update() {
        // 数据库操作
        // ....
        // 数据库操作结束
    }
}
相关推荐
fcopy1 小时前
Golang项目:实现一个内存缓存系统
缓存·golang
凡人的AI工具箱2 小时前
40分钟学 Go 语言高并发:Pipeline模式(一)
开发语言·后端·缓存·架构·golang
爬山算法3 小时前
Tomcat(36)Tomcat的静态资源缓存
java·缓存·tomcat
LightOfNight3 小时前
Redis设计与实现第14章 -- 服务器 总结(命令执行器 serverCron函数 初始化)
服务器·数据库·redis·分布式·后端·缓存·中间件
田本初5 小时前
浏览器缓存与协商缓存
前端·javascript·缓存
小小李程序员12 小时前
LRU缓存
java·spring·缓存
Clown9514 小时前
go-zero(十) 数据缓存和Redis使用
redis·缓存·golang
CopyLower17 小时前
深入理解 MyBatis 的缓存机制:一级缓存与二级缓存
spring·缓存·mybatis
nbsaas-boot21 小时前
秒杀系统三层架构设计:缓存、消息队列与数据库
数据库·缓存
NiNg_1_2341 天前
Redis中的zset底层实现
数据库·redis·缓存