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

news2024/11/19 11:28:07

一、缓存延时双删

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

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

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

二、Redisson 实现延时消息

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

<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 的信息:

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

@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);
    }
}

延时队列实现延时消息:

@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());
    }
    
}

测试延时消息:

@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+延时队列,实现延时双删策略

缓存延时删除队列:

@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();
    }
}

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

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

缓存AOP逻辑:

@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 逻辑:

@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;
    }
}

伪业务逻辑使用:

@Slf4j
@Service
public class MsgServiceImpl implements MsgService {

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


    @DeleteCache(name = "you key")
    @Override
    public void update() {
        // 数据库操作
        // ....
        // 数据库操作结束
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1417381.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HarmonyOS --@state状态装饰器

在声明式UI中&#xff0c;是以状态驱动视图更新。 状态&#xff08;state&#xff09;&#xff1a;指驱动视图更新的数据&#xff08;被装饰器标记的变量&#xff09;。 试图&#xff08;view&#xff09;&#xff1a;基于UI描述渲染得到用户界面 State装饰器标记的变量必须初…

【华为 ICT HCIA eNSP 习题汇总】——题目集11

1、某公司的内网用户采用 NAT 技术的 NO-pat 方式访问互联网&#xff0c;若所有的公网地址均被使用&#xff0c;则后续上网的内网用户会&#xff08;&#xff09;。 A、挤掉前一个用户&#xff0c;强制进行 NAT 转换上网 B、将报文同步到其他 NAT 转换设备上进行 NAT 转换 C、自…

从零学习Linux操作系统 第二十部分 mariadb数据库的管理

一、对于数据库的基本介绍 1.什么是数据库 数据库就是个高级的表格软件 2.常见数据库 Mysql Oracle mongodb db2 sqlite sqlserver … 3.Mysql (SUN -----> Oracle) 4.mariadb (Mysql的一种&#xff09; 数据库中的常用名词 1.字段 &#xff1a;表格中的表头 2.表 &…

Qt QPlainTextEdit高亮显示当前行

Qt QPlainTextEdit高亮显示当前行 文章目录 Qt QPlainTextEdit高亮显示当前行摘要错误的代码正确的代码QTextEdit::ExtraSelection 关键字&#xff1a; Qt、 QPlainTextEdit、 QTextBlock、 ExtraSelection、 GPT 摘要 今天要在说一下GPT&#xff0c;当下如果你还不会用G…

【RT-DETR有效改进】反向残差块网络EMO | 一种轻量级的CNN架构(轻量化网络,参数量下降约700W)

👑欢迎大家订阅本专栏,一起学习RT-DETR👑 一、本文介绍 本文给大家带来的改进机制是反向残差块网络EMO,其的构成块iRMB在之前我已经发过了,同时进行了二次创新,本文的网络就是由iRMB组成的网络EMO,所以我们二次创新之后的iEMA也可以用于这个网络中,再次形成二次…

04 Redis之命令(Hash型Value命令+List型Value命令+Set型Value命令+有序集合ZSET型Value命令)

3.4 Hash型Value命令 Hash 表就是一个映射表 Map&#xff0c;也是由键-值对构成&#xff0c;为了与整体的 key 进行区分&#xff0c;这里的键称为 field&#xff0c;值称为 value。注意&#xff0c;Redis 的 Hash 表中的 field-value 对均为 String 类型。 3.4.1 hset  格…

Arm AArch64 alignment(对齐)

数据和指令必须与合适的边界保持对齐(alignment)。访问是否对齐会影响ARM核的性能&#xff0c;并且在将代码从早期的体系结构移植到ARMv8-A时可能会出现可移植性问题。出于性能原因&#xff0c;或者在移植代码时&#xff0c;都值得去注意下对齐问题。本文将讲述了ARMv8-A AArch…

【electron】打包问题处理

目录 项目无法在win7执行场景尝试处理 项目无法在win7执行 场景 使用electron25.0.1、electron-builder24.2.1&#xff0c;打出来的项目在win7系统上跑不起来&#xff0c;报错无法定位程序输入点DiscardVirtualMemoty于动态链接库KERNEL32.dll上。 尝试处理 通过百度发现ele…

flask框架制作前端网页作为GUI

一、语法和原理 &#xff08;一&#xff09;、文件目录结构 需要注意的问题&#xff1a;启动文件命名必须是app.py。 一个典型的Flask应用通常包含以下几个基本文件和文件夹&#xff1a; app.py&#xff1a;应用的入口文件&#xff0c;包含了应用的初始化和配置。 requirem…

【漏洞复现】中移铁通禹路由器弱口令漏洞

Nx01 产品简介 中移禹路由器支持宽带拨号、动态IP和静态IP三种上网模式,一般中国移动宽带的光猫都是智能光猫也就是光猫带路由器功能,中移禹路由器作为二级路由使用。 Nx02 漏洞描述 中移禹路由器存在默认口令(admin)&#xff0c;攻击者可利用该漏洞获取敏感信息。 Nx03 产品…

用Python编写的简单双人对战五子棋游戏

本文是使用python创建的一个基于tkinter库的GUI界面&#xff0c;用于实现五子棋游戏。编辑器使用的是spyder&#xff0c;该工具。既方便做数据分析&#xff0c;又可以做小工具开发&#xff0c; 首先&#xff0c;导入tkinter库&#xff1a;import tkinter as tk&#xff0c;这…

Qt无边框窗口拖拽和阴影

先看下效果&#xff1a; 说明 自定义窗口控件的无边框,窗口事件由于没有系统自带边框,无法实现拖拽拉伸等事件的处理,一种方法就是重新重写主窗口的鼠标事件&#xff0c;一种时通过nativeEvent事件处理。重写事件相对繁琐,我们这里推荐nativeEvent处理。注意后续我们在做win平…

HarmonyOS4.0系统性深入开发29层叠布局

层叠布局&#xff08;Stack&#xff09; 概述 层叠布局&#xff08;StackLayout&#xff09;用于在屏幕上预留一块区域来显示组件中的元素&#xff0c;提供元素可以重叠的布局。层叠布局通过Stack容器组件实现位置的固定定位与层叠&#xff0c;容器中的子元素&#xff08;子组…

maven中的version加不加SNAPSHOT的区别

我们平时开发时经常看到maven的pom.xml文件里面的包有两种 因为maven的远程仓库一般分为public(Release)和SNAPSHOT&#xff0c;前者代表正式版本&#xff0c;后者代表快照版本。 具体有什么区别呢&#xff1a; 举例说明&#xff0c;你开发了一个基础功能&#xff0c;打包发布…

bxCAN 主要特性

bxCAN 主要特性 ● 支持 2.0 A 及 2.0 B Active 版本 CAN 协议 ● 比特率高达 1 Mb/s ● 支持时间触发通信方案 发送 ● 三个发送邮箱 ● 可配置的发送优先级 ● SOF 发送时间戳 接收 ● 两个具有三级深度的接收 FIFO ● 可调整的筛选器组&#xff1a; — CAN1 和…

Execution failed for task ‘:app:compileFlutterBuildDebug‘. 解决

前言 项目场景&#xff1a;在Flutter项目中 或 在嵌入Flutter模块的Android原生项目&#xff1b; 启动场景&#xff1a;在Android原生端 编译 或 运行 项目时&#xff0c;可能出现这个异常&#xff1b; 异常 Build窗口并没有追踪到&#xff0c;引发异常代码位置&#xff0c;…

微信小程序Skyline在手机端不渲染的问题之一及其解决方式

问题&#xff1a;电脑端是skyline渲染&#xff0c;手机端是webview渲染?如何解? 开发者工具 当前渲染模式&#xff1a;Skyline 当进行预览时手机端却是: 请注意看轮播图的显示情况 请注意看轮播图的显示情况 请注意看轮播图的显示情况 从轮播图上来看,手机端是webview渲染…

[设计模式Java实现附plantuml源码~结构型]树形结构的处理——组合模式

前言&#xff1a; 为什么之前写过Golang 版的设计模式&#xff0c;还在重新写Java 版&#xff1f; 答&#xff1a;因为对于我而言&#xff0c;当然也希望对正在学习的大伙有帮助。Java作为一门纯面向对象的语言&#xff0c;更适合用于学习设计模式。 为什么类图要附上uml 因为很…

聊聊Git合并和变基

一、 Git Merge 合并策略 1.1 Fast-Forward Merge&#xff08;快进式合并&#xff09; //在分支1下操作&#xff0c;会将分支1合并到分支2中 git merge <分支2>最简单的合并算法&#xff0c;它是在一条不分叉的两个分支之间进行合并。快进式合并是默认的合并行为&#…

【BUG】联想Y7000电池电量为0且无法充电解决方案汇总

因为最近火灾很多&#xff0c;所以昨天夜晚睡觉的时候把插线板电源关掉了&#xff0c;电脑也关机了。 各位一定要注意用电安全&#xff0c;网上的那些事情看着真的很难受qvq。 第二天早上起床的时候一看发现电脑直接没电了&#xff0c;插上电源后也是显示 你一定要冲进去啊(ू˃…