赞
踩
SpringBoot AOP + Redis
延时双删保证数据一致性在使用
Redis
作为缓存的时候,会出现Redis中数据和数据库数据不一致的情况,在后面的查询过程中就会长时间去先查Redis
, 从而出现查询到的数据并不是数据库中的真实数据的严重问题。
Redis
时,需要保证Redis
和数据库数据的一致性,目前已经有很多种解决方案,例如延时双删、canal+MQ
等策略,这里我们使用延迟双删策略来实现。Redis
使用的场景是读数据远远大于写数据的场景,因为双删策略执行的结果是把Redis
中保存的那条数据删除了,后续的查询就都会去查询数据库,因此,经常修改的数据表不适合使用Redis
。延时双删方案执行步骤
1> 删除缓存
2> 更新数据库
3> 延时1000毫秒 (根据具体业务设置延时执行的时间)
4> 删除缓存
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>
SpringBoot AOP
注解Cache
注解@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface Cache {
String name() default "";
}
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]; } }
ClearAndReloadCache
注解@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface ClearAndReloadCache {
String name() default "";
}
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; } }
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, '鳄鱼');
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); } }
id=1
,缓存种不存在,去查数据库id=1
,缓存中存在,直接返回缓存数据id=1,username=嘉文四世
,删除缓存id=1
,这时缓存种不存在,去查数据库
- 为何要延时1000毫秒?
答:这是为了在第二次删除Redis
之前能完成数据库的更新操作。如果没有延时操作时,有很大概率,在两次删除Redis
操作执行完毕之后,数据库的数据还没有更新,此时若有请求访问数据,还是会出现我们一开始提到的数据不一致问题。另外,这里延迟的时间需要根据自己系统的业务时间去设置。- 为何要两次删除缓存?
答:如果我们没有第二次删除操作,此时有请求访问数据,有可能是访问的之前未做修改的Redis
数据,删除操作执行后,Redis
为空,有请求进来时,便会去访问数据库,此时数据库中的数据已是更新后的数据,保证了数据的一致性。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。