SpringBoot AOP + Redis 延时双删保证数据一致性

SpringBoot AOP + Redis 延时双删保证数据一致性

一、业务场景

1.1 存在的问题

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

1.2 解决方案

  1. 在使用Redis时,需要保证Redis和数据库数据的一致性,目前已经有很多种解决方案,例如延时双删、canal+MQ等策略,这里我们使用延迟双删策略来实现。
  2. 注意:Redis使用的场景是读数据远远大于写数据的场景,因为双删策略执行的结果是把Redis中保存的那条数据删除了,后续的查询就都会去查询数据库,因此,经常修改的数据表不适合使用Redis

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

二、代码实现

2.1 引入RedisSpringBoot 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.2 自定义SpringBoot AOP注解

2.2.1 Cache注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface Cache {
    String name() default "";
}
2.2.2 Cache切面
package com.xxx.demo.aop;

import cn.hutool.core.lang.Assert;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.xxx.demo.modules.system.entity.User;
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.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

@Aspect
@Component
@Slf4j
public class CacheAspect {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 切入点
     */
    @Pointcut("@annotation(com.xxx.demo.aop.Cache)")
    public void pointCut(){

    }

    /**
     * 环绕通知
     */
    @Around("pointCut()")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
        log.info("----------- 环绕通知 -----------");
        log.info("环绕通知的目标方法名:{}", proceedingJoinPoint.getSignature().getName());

        Signature signature1 = proceedingJoinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature)signature1;
        //方法对象
        Method targetMethod = methodSignature.getMethod();
        //反射得到自定义注解的方法对象
        Cache annotation = targetMethod.getAnnotation(Cache.class);
        String userId = getUserId();
        Assert.notNull(userId, "id为null");
        //获取自定义注解的方法对象的参数即name
        String name = annotation.name();
        String redisKey = name + ":"+ userId;
        //模糊定义key
        String res = stringRedisTemplate.opsForValue().get(redisKey);
        if (!ObjectUtils.isEmpty(res)) {
            log.info("从缓存返回,数据为:{}",res);
            return JSONUtil.toBean(res, User.class);
        }
        Object proceed = null;
        try {
            proceed = proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        if (!ObjectUtils.isEmpty(proceed)) {
            stringRedisTemplate.opsForValue().set(redisKey, JSON.toJSONString(proceed));
        }
        log.info("从数据库返回,数据为:{}",proceed);
        return proceed;
    }

    private static String getUserId() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 获取请求路径中的所有路径参数
        String requestURI = request.getRequestURI();
        String[] pathSegments = requestURI.split("/");
        return pathSegments[3];
    }
}
2.2.3 ClearAndReloadCache注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface ClearAndReloadCache {
    String name() default "";
}
2.2.3 ClearAndReloadCache切面
package com.xxx.demo.aop;

import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.xxx.demo.modules.system.entity.User;
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.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;

@Aspect
@Component
@Slf4j
public class ClearAndReloadCacheAspect {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 切入点
     */
    @Pointcut("@annotation(com.xxx.demo.aop.ClearAndReloadCache)")
    public void pointCut() {

    }

    /**
     * 环绕通知
     */
    @Around("pointCut()")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
        log.info("----------- 环绕通知 -----------");
        log.info("环绕通知的目标方法名:{}", proceedingJoinPoint.getSignature().getName());
        Object[] args = proceedingJoinPoint.getArgs();
        String userString = JSON.toJSONString(args[0]);
        User bean = JSONUtil.toBean(userString, User.class);
        Signature signature1 = proceedingJoinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature1;
        // 方法对象
        Method targetMethod = methodSignature.getMethod();
        // 反射得到自定义注解的方法对象
        ClearAndReloadCache annotation = targetMethod.getAnnotation(ClearAndReloadCache.class);
        // 获取自定义注解的方法对象的参数即name
        String name = annotation.name();
        String redisKey = name + ":" + bean.getId();
        // 删除redis的key值
        stringRedisTemplate.delete(redisKey);
        // 执行加入双删注解的改动数据库的业务 即controller中的方法业务
        Object proceed = null;
        try {
            proceed = proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        // 开一个线程 延迟1秒(此处是1秒举例,可以改成自己的业务)
        // 在线程中延迟删除  同时将业务代码的结果返回 这样不影响业务代码的执行
        new Thread(() -> {
            try {
                Thread.sleep(1000);
                stringRedisTemplate.delete(redisKey);
                log.info("-----------1秒钟后,在线程中延迟删除完毕 -----------");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        // 返回业务代码的值
        return proceed;
    }
}

2.3 SQL脚本

DROP TABLE IF EXISTS sys_user;
CREATE TABLE sys_user  (
  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;

INSERT INTO sys_user VALUES (1, '皇子');
INSERT INTO sys_user VALUES (2, '猪妹');
INSERT INTO sys_user VALUES (3, '机器人');
INSERT INTO sys_user VALUES (4, '沙皇');
INSERT INTO sys_user VALUES (5, '格温');
INSERT INTO sys_user VALUES (6, '鳄鱼');

2.4 UserController.java

package com.xxx.demo.modules.system.api;

import com.xxx.demo.aop.Cache;
import com.xxx.demo.aop.ClearAndReloadCache;
import com.xxx.demo.modules.system.entity.User;
import com.xxx.demo.modules.system.service.IUserService;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
@Api(description = "测试-接口")
public class UserController {

    @Autowired
    private IUserService iUserService;

    @GetMapping("/get/{id}")
    @Cache(name = "getUser")
    public User get(@PathVariable("id") Integer id){
        return iUserService.get(id);
    }

    @PostMapping("/update")
    @ClearAndReloadCache(name = "getUser")
    public int updateData(@RequestBody User user){
        return iUserService.update(user);
    }
}

三、测试验证

3.1 获取id=1,缓存种不存在,去查数据库

在这里插入图片描述

3.2 获取id=1,缓存中存在,直接返回缓存数据

在这里插入图片描述

3.3 修改id=1,username=嘉文四世,删除缓存

在这里插入图片描述

3.4 再获取id=1,这时缓存种不存在,去查数据库

在这里插入图片描述

四、总结

  1. 为何要延时1000毫秒?
    答:这是为了在第二次删除Redis之前能完成数据库的更新操作。如果没有延时操作时,有很大概率,在两次删除Redis操作执行完毕之后,数据库的数据还没有更新,此时若有请求访问数据,还是会出现我们一开始提到的数据不一致问题。另外,这里延迟的时间需要根据自己系统的业务时间去设置。
  2. 为何要两次删除缓存?
    答:如果我们没有第二次删除操作,此时有请求访问数据,有可能是访问的之前未做修改的Redis数据,删除操作执行后,Redis为空,有请求进来时,便会去访问数据库,此时数据库中的数据已是更新后的数据,保证了数据的一致性。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值