搜索
首页数据库RedisSpringBoot AOP Redis如何实现延时双删功能

SpringBoot AOP Redis如何实现延时双删功能

May 29, 2023 am 08:53 AM
redisaopspringboot

    一、业务场景

    在多线程并发情况下,假设有两个数据库修改请求,为保证数据库与redis的数据一致性,
    修改请求的实现中需要修改数据库后,级联修改Redis中的数据。
    请求一:A修改数据库数据 B修改Redis数据
    请求二:C修改数据库数据 D修改Redis数据
    并发情况下就会存在A —> C —> D —> B的情况
    (一定要理解线程并发执行多组原子操作执行顺序是可能存在交叉现象的)

    1、此时存在的问题

    A修改数据库的数据最终保存到了Redis中,C在A之后也修改了数据库数据。

    此时出现了Redis中数据和数据库数据不一致的情况,在后面的查询过程中就会长时间去先查Redis,从而出现查询到的数据并不是数据库中的真实数据的严重问题。

    2、解决方案

    在使用Redis时,需要保持Redis和数据库数据的一致性,最流行的解决方案之一就是延时双删策略。
    注意:要知道经常修改的数据表不适合使用Redis,因为双删策略执行的结果是把Redis中保存的那条数据删除了,以后的查询就都会去查询数据库。所以Redis使用的是读远远大于改的数据缓存。
    延时双删方案执行步骤

    1> 删除缓存
    2> 更新数据库
    3> 延时500毫秒 (根据具体业务设置延时执行的时间)
    4> 删除缓存

    3、为何要延时500毫秒?

    我们需要在第二次Redis删除之前完成数据库的更新操作。假象一下,如果没有第三步操作时,有很大概率,在两次删除Redis操作执行完毕之后,数据库的数据还没有更新,此时若有请求访问数据,便会出现我们一开始提到的那个问题。

    4、为何要两次删除缓存?

    如果我们没有第二次删除操作,此时有请求访问数据,有可能是访问的之前未做修改的Redis数据,删除操作执行后,Redis为空,有请求进来时,便会去访问数据库,此时数据库中的数据已是更新后的数据,保证了数据的一致性。

    二、代码实践

    1、引入Redis和SpringBoot AOP依赖

    <!-- redis使用 -->
    <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- aop -->
    <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    2、编写自定义aop注解和切面

    ClearAndReloadCache延时双删注解

    /**
     *延时双删
     **/
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Target(ElementType.METHOD)
    public @interface ClearAndReloadCache {
        String name() default "";
    }

    ClearAndReloadCacheAspect延时双删切面

    @Aspect
    @Component
    public class ClearAndReloadCacheAspect {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    /**
    * 切入点
    *切入点,基于注解实现的切入点  加上该注解的都是Aop切面的切入点
    *
    */
    
    @Pointcut("@annotation(com.pdh.cache.ClearAndReloadCache)")
    public void pointCut(){
    
    }
    /**
    * 环绕通知
    * 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
    * 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
    * @param proceedingJoinPoint
    */
    @Around("pointCut()")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
        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();//获取自定义注解的方法对象的参数即name
        Set<String> keys = stringRedisTemplate.keys("*" + name + "*");//模糊定义key
        stringRedisTemplate.delete(keys);//模糊删除redis的key值
    
        //执行加入双删注解的改动数据库的业务 即controller中的方法业务
        Object proceed = null;
        try {
            proceed = proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    
        //开一个线程 延迟1秒(此处是1秒举例,可以改成自己的业务)
        // 在线程中延迟删除  同时将业务代码的结果返回 这样不影响业务代码的执行
        new Thread(() -> {
            try {
                Thread.sleep(1000);
                Set<String> keys1 = stringRedisTemplate.keys("*" + name + "*");//模糊删除
                stringRedisTemplate.delete(keys1);
                System.out.println("-----------1秒钟后,在线程中延迟删除完毕 -----------");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    
        return proceed;//返回业务代码的值
        }
    }

    3、application.yml

    server:
      port: 8082
    
    spring:
      # redis setting
      redis:
        host: localhost
        port: 6379
    
      # cache setting
      cache:
        redis:
          time-to-live: 60000 # 60s
    
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test
        username: root
        password: 1234
    
    # mp setting
    mybatis-plus:
      mapper-locations: classpath*:com/pdh/mapper/*.xml
      global-config:
        db-config:
          table-prefix:
      configuration:
        # log of sql
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
        # hump
        map-underscore-to-camel-case: true

    4、user_db.sql脚本

    用于生产测试数据

    DROP TABLE IF EXISTS `user_db`;
    CREATE TABLE `user_db`  (
      `id` int(4) NOT NULL AUTO_INCREMENT,
      `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of user_db
    -- ----------------------------
    INSERT INTO `user_db` VALUES (1, &#39;张三&#39;);
    INSERT INTO `user_db` VALUES (2, &#39;李四&#39;);
    INSERT INTO `user_db` VALUES (3, &#39;王二&#39;);
    INSERT INTO `user_db` VALUES (4, &#39;麻子&#39;);
    INSERT INTO `user_db` VALUES (5, &#39;王三&#39;);
    INSERT INTO `user_db` VALUES (6, &#39;李三&#39;);

    5、UserController

    /**
     * 用户控制层
     */
    @RequestMapping("/user")
    @RestController
    public class UserController {
        @Autowired
        private UserService userService;
    
        @GetMapping("/get/{id}")
        @Cache(name = "get method")
        //@Cacheable(cacheNames = {"get"})
        public Result get(@PathVariable("id") Integer id){
            return userService.get(id);
        }
    
        @PostMapping("/updateData")
        @ClearAndReloadCache(name = "get method")
        public Result updateData(@RequestBody User user){
            return userService.update(user);
        }
    
        @PostMapping("/insert")
        public Result insert(@RequestBody User user){
            return userService.insert(user);
        }
    
        @DeleteMapping("/delete/{id}")
        public Result delete(@PathVariable("id") Integer id){
            return userService.delete(id);
        }
    }

    6、UserService

    /**
     * service层
     */
    @Service
    public class UserService {
    
        @Resource
        private UserMapper userMapper;
    
        public Result get(Integer id){
            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(User::getId,id);
            User user = userMapper.selectOne(wrapper);
            return Result.success(user);
        }
    
        public Result insert(User user){
            int line = userMapper.insert(user);
            if(line > 0)
                return Result.success(line);
            return Result.fail(888,"操作数据库失败");
        }
    
        public Result delete(Integer id) {
            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(User::getId, id);
            int line = userMapper.delete(wrapper);
            if (line > 0)
                return Result.success(line);
            return Result.fail(888, "操作数据库失败");
        }
    
        public Result update(User user){
            int i = userMapper.updateById(user);
            if(i > 0)
                return Result.success(i);
            return Result.fail(888,"操作数据库失败");
        }
    }

    三、测试验证

    1、ID=10,新增一条数据

    SpringBoot AOP Redis如何实现延时双删功能

    2、第一次查询数据库,Redis会保存查询结果

    SpringBoot AOP Redis如何实现延时双删功能

    3、第一次访问ID为10

    SpringBoot AOP Redis如何实现延时双删功能

    4、第一次访问数据库ID为10,将结果存入Redis

    SpringBoot AOP Redis如何实现延时双删功能

    5、更新ID为10对应的用户名(验证数据库和缓存不一致方案)

    SpringBoot AOP Redis如何实现延时双删功能

    数据库和缓存不一致验证方案:

    打个断点,模拟A线程执行第一次删除后,在A更新数据库完成之前,另外一个线程B访问ID=10,读取的还是旧数据。

    SpringBoot AOP Redis如何实现延时双删功能

    SpringBoot AOP Redis如何实现延时双删功能

    利用第二次删除,在根据业务场景设定合适的延迟时间后,待两次删除缓存成功后,Redis的输出结果将为空。读取的都是数据库真实数据,不会出现读缓存和数据库不一致情况。

    SpringBoot AOP Redis如何实现延时双删功能

    四、代码工程

    核心代码红色方框所示

    SpringBoot AOP Redis如何实现延时双删功能

    以上是SpringBoot AOP Redis如何实现延时双删功能的详细内容。更多信息请关注PHP中文网其他相关文章!

    声明
    本文转载于:亿速云。如有侵权,请联系admin@php.cn删除
    REDIS:提高应用程序性能和可扩展性REDIS:提高应用程序性能和可扩展性Apr 17, 2025 am 12:16 AM

    Redis通过缓存数据、实现分布式锁和数据持久化来提升应用性能和可扩展性。1)缓存数据:使用Redis缓存频繁访问的数据,提高数据访问速度。2)分布式锁:利用Redis实现分布式锁,确保在分布式环境中操作的安全性。3)数据持久化:通过RDB和AOF机制保证数据安全性,防止数据丢失。

    REDIS:探索其数据模型和结构REDIS:探索其数据模型和结构Apr 16, 2025 am 12:09 AM

    Redis的数据模型和结构包括五种主要类型:1.字符串(String):用于存储文本或二进制数据,支持原子操作。2.列表(List):有序元素集合,适合队列和堆栈。3.集合(Set):无序唯一元素集合,支持集合运算。4.有序集合(SortedSet):带分数的唯一元素集合,适用于排行榜。5.哈希表(Hash):键值对集合,适合存储对象。

    REDIS:对其数据库方法进行分类REDIS:对其数据库方法进行分类Apr 15, 2025 am 12:06 AM

    Redis的数据库方法包括内存数据库和键值存储。1)Redis将数据存储在内存中,读写速度快。2)它使用键值对存储数据,支持复杂数据结构,如列表、集合、哈希表和有序集合,适用于缓存和NoSQL数据库。

    为什么要使用redis?利益和优势为什么要使用redis?利益和优势Apr 14, 2025 am 12:07 AM

    Redis是一个强大的数据库解决方案,因为它提供了极速性能、丰富的数据结构、高可用性和扩展性、持久化能力以及广泛的生态系统支持。1)极速性能:Redis的数据存储在内存中,读写速度极快,适合高并发和低延迟应用。2)丰富的数据结构:支持多种数据类型,如列表、集合等,适用于多种场景。3)高可用性和扩展性:支持主从复制和集群模式,实现高可用性和水平扩展。4)持久化和数据安全:通过RDB和AOF两种方式实现数据持久化,确保数据的完整性和可靠性。5)广泛的生态系统和社区支持:拥有庞大的生态系统和活跃社区,

    了解NOSQL:Redis的关键特征了解NOSQL:Redis的关键特征Apr 13, 2025 am 12:17 AM

    Redis的关键特性包括速度、灵活性和丰富的数据结构支持。1)速度:Redis作为内存数据库,读写操作几乎瞬时,适用于缓存和会话管理。2)灵活性:支持多种数据结构,如字符串、列表、集合等,适用于复杂数据处理。3)数据结构支持:提供字符串、列表、集合、哈希表等,适合不同业务需求。

    REDIS:确定其主要功能REDIS:确定其主要功能Apr 12, 2025 am 12:01 AM

    Redis的核心功能是高性能的内存数据存储和处理系统。1)高速数据访问:Redis将数据存储在内存中,提供微秒级别的读写速度。2)丰富的数据结构:支持字符串、列表、集合等,适应多种应用场景。3)持久化:通过RDB和AOF方式将数据持久化到磁盘。4)发布订阅:可用于消息队列或实时通信系统。

    REDIS:流行数据结构指南REDIS:流行数据结构指南Apr 11, 2025 am 12:04 AM

    Redis支持多种数据结构,具体包括:1.字符串(String),适合存储单一值数据;2.列表(List),适用于队列和栈;3.集合(Set),用于存储不重复数据;4.有序集合(SortedSet),适用于排行榜和优先级队列;5.哈希表(Hash),适合存储对象或结构化数据。

    redis计数器怎么实现redis计数器怎么实现Apr 10, 2025 pm 10:21 PM

    Redis计数器是一种使用Redis键值对存储来实现计数操作的机制,包含以下步骤:创建计数器键、增加计数、减少计数、重置计数和获取计数。Redis计数器的优势包括速度快、高并发、持久性和简单易用。它可用于用户访问计数、实时指标跟踪、游戏分数和排名以及订单处理计数等场景。

    See all articles

    热AI工具

    Undresser.AI Undress

    Undresser.AI Undress

    人工智能驱动的应用程序,用于创建逼真的裸体照片

    AI Clothes Remover

    AI Clothes Remover

    用于从照片中去除衣服的在线人工智能工具。

    Undress AI Tool

    Undress AI Tool

    免费脱衣服图片

    Clothoff.io

    Clothoff.io

    AI脱衣机

    AI Hentai Generator

    AI Hentai Generator

    免费生成ai无尽的。

    热门文章

    R.E.P.O.能量晶体解释及其做什么(黄色晶体)
    1 个月前By尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O.最佳图形设置
    1 个月前By尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O.如果您听不到任何人,如何修复音频
    1 个月前By尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O.聊天命令以及如何使用它们
    1 个月前By尊渡假赌尊渡假赌尊渡假赌

    热工具

    记事本++7.3.1

    记事本++7.3.1

    好用且免费的代码编辑器

    Atom编辑器mac版下载

    Atom编辑器mac版下载

    最流行的的开源编辑器

    适用于 Eclipse 的 SAP NetWeaver 服务器适配器

    适用于 Eclipse 的 SAP NetWeaver 服务器适配器

    将Eclipse与SAP NetWeaver应用服务器集成。

    SecLists

    SecLists

    SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

    VSCode Windows 64位 下载

    VSCode Windows 64位 下载

    微软推出的免费、功能强大的一款IDE编辑器