当前位置: 首页 > article >正文

架构设计之自定义延迟双删缓存注解(上)

架构设计之自定义延迟双删缓存注解(上)

小薛博客官方架构设计之自定义延迟双删缓存注解(上)地址

1、业务场景问题

在多线程并发情况下,假设有两个数据库修改请求,为保证数据库与redis的数据一致性,修改请求的实现中需要修改数据库后,级联修改Redis中的数据。

  • 请求一:A修改数据库数据 B修改Redis数据
  • 请求二:C修改数据库数据 D修改Redis数据
  • 正常情况:A —> B—>C —> D

**并发情况下就会存在A —> C —> D —> B的情况,**问题在哪里?

要理解线程并发执行多组原子操作执行顺序是可能存在交叉现象的

A修改数据库的数据最终保存到了Redis中,但是在A没有修改完成是不会同步进mysql的,

C在AB操作之间来查询,缓存redis数据没有删除,那A已经修改新数据了但是没有同步进B步骤进redis

导致C查看的还是老数据,此时出现了Redis中数据和数据库数据不一致的情况,从而出现查询到的数据并不是数据库中的真实数据的严重问题。

2、解决方案

在使用Redis时,需要保持Redis和数据库数据的一致性,最流行的解决方案之一就是延时双删策略。

**注意:**经常修改的数据表不适合使用Redis,因为双删策略执行的结果是把Redis中保存的那条数据删除了,以后的查询短时间内就都会去查询数据库,导致mysql侧压力瞬间增大,所以Redis使用的是读远远大于改的数据缓存。

  • 删除缓存
  • 更新数据库
  • 延时500毫秒 (根据具体业务设置延时执行的时间,保证数据库更新或者删除业务操作完成即可,看你自己实际情况)
  • 删除缓存

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

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

因为mysql做了update操作,

导致数据不一致

查都是先查redis,一旦redis和mysql数据不一致,我返回的是redis里面的老数据,导致了这个生产bug

那一不做二不休,直接删掉redis,没有redis了,只有唯一的一个底单数据源mysql,数据源现在在唯一了

大家都来自mysql

更新动作完成之前,redis没有值,都从mysql一个数据源来

4、新建xx-delay-double-del

1、SQL

USE
`xx_db2025`;

DROP TABLE IF EXISTS `user_db`;

CREATE TABLE `user_db`
(
    `id`       int                                                    NOT NULL AUTO_INCREMENT,
    `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

insert into `user_db`(`id`, `username`)
values (1, 'Version1');

2、pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.0</version>
        <relativePath/>
    </parent>

    <artifactId>xx-delay-double-del</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- spring boot的web开发所需要的起步依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <!-- mybatis-plus-boot3-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.7</version>
        </dependency>
        <!-- mybatis-plus-extension -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-extension</artifactId>
            <version>3.5.7</version>
        </dependency>
        <!--pagehelper-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        <!--SpringBoot集成druid连接池druid-spring-boot-starter -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.21</version>
        </dependency>
        <!-- mysql-connector-j -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.3.0</version>
        </dependency>
        <!--boot-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>

        <dependency>
            <groupId>com.xx</groupId>
            <artifactId>xx-common-core</artifactId>
            <version>1.6.1</version>
        </dependency>

        <!-- md5 加密包 -->
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <!-- commons -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

3、application.yml

server:
  port: 8001

spring:
  data:
    redis:
      database: 0
      host: 127.0.0.1
      password: 123456
      port: 6379
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    #    url: jdbc:mysql://127.0.0.1:3306/xx_db2023?connectTimeout=6000&socketTimeout=6000&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    url: jdbc:mysql://127.0.0.1:3306/xx_db2025?connectTimeout=6000&socketTimeout=6000&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
    username: root
    password: mac_root

# mybatis-plus相关配置
mybatis-plus:
  #  global-config:
  #    db-config:
  #      id-type: auto
  # xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
  mapper-locations: classpath:mapper/*Mapper.xml
  # type-enums-package: com.xx.enums
  configuration:
    # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
    map-underscore-to-camel-case: true
    # 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
    call-setters-on-nulls: true
    # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    cache-enabled: true

    db-config:
      logic-delete-field: del_flag  # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)


---
spring:
  sql:
    init:
      mode: always  # 总是初始化数据库
      schema-locations: classpath:db/init.sql  # 初始化SQL文件位置

#Mybatis输出sql日志
logging:
  level:
    com.xx.mapper: info

4、启动类

package com.xx;

import com.xx.utils.LocalIpUtil;
import com.xx.utils.ValidationUtil;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import org.springframework.util.StopWatch;

import java.net.UnknownHostException;

/**
 * @Author: xueqimiao
 * @Date: 2025/3/17 14:22
 */
@SpringBootApplication
@MapperScan("com.xx.mapper")
@Slf4j
public class DelayDoubleDelApplication {

    public static void main(String[] args) throws UnknownHostException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext application = SpringApplication.run(DelayDoubleDelApplication.class, args);
        stopWatch.stop();
        Environment env = application.getEnvironment();
        String ip = LocalIpUtil.getLocalIp();
        String port = env.getProperty("server.port");
        String path = env.getProperty("server.servlet.context-path");
        path = ValidationUtil.isEmpty(path) ? "" : path;
        log.info("\n--------------------------------------------------------\n\t" +
                "Application Manager is running! Access URLs:\n\t" +
                "Local: \t\thttp://127.0.0.1:" + port + path + "/\n\t" +
                "External: \thttp://" + ip + ":" + port + path + "/\n\t" +
                "Swagger文档: \thttp://" + ip + ":" + port + path + "/doc.html\n\t" +
                "服务启动完成,耗时: \t" + stopWatch.getTotalTimeSeconds() + "S\n" +
                "----------------------------------------------------------");
    }

}

5、RedisUtils

package com.xx.utils;

import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @Author: xueqimiao
 * @Date: 2025/3/17 14:19
 */
@Service
public class RedisUtils {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    /**
     * RedisAutoConfiguration
     * 写入缓存+过期时间
     *
     * @param key
     * @param value
     * @param expireTime
     * @param timeUnit
     * @return
     */
    public boolean set(String key, String value, Long expireTime, TimeUnit timeUnit) {
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        operations.set(key, value);
        redisTemplate.expire(key, expireTime, timeUnit);
        return true;
    }

    /**
     * 通过key获取value
     *
     * @param key
     * @return
     */
    public String get(String key) {
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        return operations.get(key);
    }

    /**
     * 批量删除 k-v
     *
     * @param keys
     * @return
     */
    public boolean del(final String... keys) {
        for (String key : keys) {
            if (redisTemplate.hasKey(key)) { //key存在就删除
                redisTemplate.delete(key);
            }
        }
        return true;
    }

    public boolean del(final Set<String> keys) {
        for (String key : keys) {
            if (redisTemplate.hasKey(key)) { //key存在就删除
                redisTemplate.delete(key);
            }
        }
        return true;
    }

    /**
     * 获取key集合
     * @param key
     * @return
     */
    public Set<String> keys(final String key) {
        Set<String> keys = redisTemplate.keys(key);
        return keys;
    }
}

6、RedisConfig

package com.xx.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @Author: xueqimiao
 * @Date: 2025/3/17 14:19
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Integer,Integer> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<Integer,Integer> redisTemplate = new RedisTemplate<>();

        // 设置连接工厂类
        redisTemplate.setConnectionFactory(factory);

        // 设置k-v的序列化方式
        // Jackson2JsonRedisSerializer 实现了 RedisSerializer接口
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

7、User

package com.xx.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @Author: xueqimiao
 * @Date: 2025/3/17 14:13
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user_db")
public class User implements Serializable {

    @TableId(type = IdType.AUTO)
    private Integer id;

    private String username;

}

8、UserMapper

package com.xx.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xx.entity.User;

/**
 * @Author: xueqimiao
 * @Date: 2025/3/17 14:14
 */
public interface UserMapper extends BaseMapper<User> {
}

9、UserService

package com.xx.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xx.common.Result;
import com.xx.entity.User;
import com.xx.mapper.UserMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;

/**
 * @Author: xueqimiao
 * @Date: 2025/3/17 14:17
 */
@Service
public class UserService {

    @Resource
    private UserMapper userMapper;
    
    public Result update(User user) {
        int i = userMapper.updateById(user);
        if (i > 0) {
            return Result.ok(i);
        }
        return Result.error(500, "操作数据库失败");
    }

    public Result get(Integer id) {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getId, id);
        User user = userMapper.selectOne(wrapper);
        return Result.ok(user);
    }
}

5、自定义缓存注解@Cache

package com.xx.cache;

import java.lang.annotation.*;

/**
 * @Author: xueqimiao
 * @Date: 2025/3/17 14:24
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {

    /**
     * 过期时间,默认60s
     * @return
     */
    long expire() default 5 * 60 * 1000;

    /**
     * 缓存标识name
     * @return
     */
    String name() default "";

}

6、CacheAspect

package com.xx.cache;

import com.alibaba.fastjson.JSON;
import com.xx.common.Result;
import com.xx.utils.RedisUtils;
import com.xx.utils.ValidationUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * @Author: xueqimiao
 * @Date: 2025/3/17 14:25
 */
@Component
@Aspect
@Slf4j
public class CacheAspect {

    @Resource
    private RedisUtils redisUtils;

    /**
     * aop切点
     * 拦截被指定注解修饰的方法
     */
    @Pointcut("@annotation(com.xx.cache.Cache)")
    public void cache() {
    }

    /**
     * 缓存操作
     *
     * @param pjp
     * @return
     */
    @Around("cache()")
    public Object toCache(ProceedingJoinPoint pjp) {

        try {
            // 思路: 设置存储的格式,获取即可
            Signature signature = pjp.getSignature();
            // 类名
            String className = pjp.getTarget().getClass().getSimpleName();
            // 方法名
            String methodName = signature.getName();
            // 参数处理
            Object[] args = pjp.getArgs();
            Class[] parameterTypes = new Class[args.length];
            String params = "";
            for (int i = 0; i < args.length; i++) {
                if (args[i] != null) {
                    parameterTypes[i] = args[i].getClass();
                    params += JSON.toJSONString(args[i]);
                }
            }
            if (!ValidationUtil.isEmpty(params)) {
                //加密 以防出现key过长以及字符转义获取不到的情况
                params = DigestUtils.md5Hex(params);
            }
            // 获取controller中对应的方法
            Method method = signature.getDeclaringType().getMethod(methodName, parameterTypes);

            // 获取Cache注解
            Cache annotation = method.getAnnotation(Cache.class);
            long expire = annotation.expire();
            String name = annotation.name();

            // 访问redis(先尝试获取,没有则访问数据库)
            String redisKey = name + "::" + className + "::" + methodName + "::" + params;
            String redisValue = redisUtils.get(redisKey);
            if (!ValidationUtil.isEmpty(redisValue)) {
                // 不为空返回数据
                Result result = JSON.parseObject(redisValue, Result.class);
                log.info("数据从redis缓存中获取,key: {}", redisKey);
                return result; // 跳出方法
            }
            Object proceed = pjp.proceed();// 放行
            redisUtils.set(redisKey, JSON.toJSONString(proceed), expire, TimeUnit.MILLISECONDS);
            log.info("数据存入redis缓存,key: {}", redisKey);
            return proceed;
        } catch (Throwable throwable) {
            return Result.error(500, "系统错误");
        }
    }

}

7、自定义延迟双删注解@ClearAndReloadCache

package com.xx.cache;

import java.lang.annotation.*;

/**
 * @Author: xueqimiao
 * @Date: 2025/3/17 14:26
 */
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface ClearAndReloadCache {
    String name() default "";
}

8、ClearAndReloadCacheAspect

package com.xx.cache;

import com.xx.utils.RedisUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Set;

/**
 * @Author: xueqimiao
 * @Date: 2025/3/17 14:26
 */
@Aspect
@Component
@Slf4j
public class ClearAndReloadCacheAspect {

    @Resource
    private RedisUtils redisUtils;
    
    /**
     * 切入点
     * 切入点,基于注解实现的切入点  加上该注解的都是Aop切面的切入点
     */
    @Pointcut("@annotation(com.xx.cache.ClearAndReloadCache)")
    public void pointCut() {

    }

    /**
     * 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
     *
     * @param proceedingJoinPoint
     */
    @Around("pointCut()")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
        log.info("----------- 环绕通知 -----------");
        log.info("环绕通知的目标方法名:" + proceedingJoinPoint.getSignature().getName());
        Signature signature = proceedingJoinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();//方法对象
        ClearAndReloadCache annotation = targetMethod.getAnnotation(ClearAndReloadCache.class);//反射得到自定义注解的方法对象

        String name = annotation.name();//获取自定义注解的方法对象的参数即name
        Set<String> keys = redisUtils.keys("*" + name + "*");//模糊定义key
        redisUtils.del(keys);//模糊删除redis的key值
        //执行加入双删注解的改动数据库的业务 即controller中的方法业务
        Object proceed = null;
        try {
            proceed = proceedingJoinPoint.proceed();//放行
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        //新开开一个线程延迟0.5秒(时间可以改成自己的业务需求),等着mysql那边业务操作完成
        //在线程中延迟删除  同时将业务代码的结果返回 这样不影响业务代码的执行
        new Thread(() -> {
            try {
                Thread.sleep(500);
                Set<String> keys1 = redisUtils.keys("*" + name + "*");//模糊删除
                redisUtils.del(keys1);
                log.info("-----------0.5秒后,在线程中延迟删除完毕 -----------");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        return proceed;//返回业务代码的值
    }

}

9、UserController

可以在 redisUtils.del(keys)的时候打断点调试

package com.xx.controller;

import com.xx.cache.Cache;
import com.xx.cache.ClearAndReloadCache;
import com.xx.common.Result;
import com.xx.entity.User;
import com.xx.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;

/**
 * @Author: xueqimiao
 * @Date: 2025/3/17 14:27
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @Resource
    private UserService userService;

    @PostMapping("/updateData")
    @ClearAndReloadCache(name = "get_user")
    public Result updateData(@RequestBody User user) {
        return userService.update(user);
    }

    @GetMapping("/get")
    @Cache(name = "get_user")
    public Result get(@RequestParam Integer id) {
        return userService.get(id);
    }
}

写在最后

Java是一种强大的语言,它赋予了我们创造无限可能的力量。无论你是在开发企业级应用、移动应用还是大数据处理系统,Java都在背后默默支持着你。在这个瞬息万变的技术时代,保持学习的热情和对新技术的敏感度是至关重要的。每一个新的Java版本发布,都带来了新的特性和改进,这是我们提升自己的绝佳机会。让我们一起在Java的道路上勇往直前,用代码构建更加美好的数字世界,因为你手中的代码,有可能成为下一个改变世界的伟大发明。


http://www.kler.cn/a/595457.html

相关文章:

  • 【C++基础】Lambda 函数 基础知识讲解学习及难点解析
  • vscode连接本地mysql数据库
  • 解决python配置文件类configparser.ConfigParser,插入、读取数据,自动转为小写的问题
  • LLM之向量数据库Chroma milvus FAISS
  • SOFAStack-00-sofa 技术栈概览
  • ip2region与express最佳实践
  • Linux 文件系统的日志模式与性能影响
  • RC6在线加密工具
  • PaddleSpeech-语音处理-安装【超简洁步骤】
  • 关于 Redis 缓存一致
  • 北京南文观点:AI掘金术激活算法中的“沉默用户”
  • python爬虫解析器bs4,xpath,pquery
  • 【如何打包docker大镜像】
  • 鸿蒙NEXT项目实战-百得知识库03
  • docker安装向量数据库Milvus及可视化工具 Attu
  • 复习HCIA
  • ngx_http_add_listen
  • 在R中读入h5ad文件,并转换为seurat对象
  • 闻所闻尽:穿透声音的寂静,照见生命的本真
  • 一文讲清 C++ CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)