解决redis、MySQL数据不一致:延迟双删
背景
在并发环境下redis有数据一致性的问题:
在多线程并发情况下,假设有两个数据库修改请求,为保证数据库与redis的数据一致性,修改请求的实现中需要修改数据库后,级联修改redis中的数据。
| 请求一: | 1.1修改数据库数据 | 1.2 修改redis数据 |
| 请求二: | 2.1修改数据库数据 | 2.2 修改redis数据 |
并发情况下就会存在1.1 ---> 2.1 ---> 2.2 ---> 1.2的情况
(一定要理解线程并发执行多组原子操作执行顺序是可能存在交叉现象的)
分析
此时存在问题:
1.1修改数据库的数据最终保存到了redis中,2.1在1.1之后也修改了数据库数据。此时出现了redis中数据和数据库数据不一致的情况,在后面的查询过程中就会长时间去先查redis,从而出现查询到的数据并不是数据库中的真实数据的严重问题。
问题解决:
添加延时双删策略
| 请求一: | 1.1修改数据库数据 | 1.2 删除redis数据 | 1.3 延时3--5s再去删除redis中数据 |
| 请求二: | 2.1修改数据库数据 | 2.2 删除redis数据 | 2.3 延时3--5s再去删除redis中数据 |
| 请求三: | 3.1查询redis中数据 | 3.2 查询数据库数据 | 3.3 新查到的数据写入redis |
代码实现
采用AOP切面编程,以环绕通知进行延迟双删。
自定义注解:(DelayedDoubleDeletion.java)
import java.lang.annotation.*;/*** @author Wanghs* @create 2022/2/22* @description AOP实现延迟双删*/public DelayedDoubleDeletion {String name() default "";}
切面类:(DelayedDoubleDeletionAspect.java 包含定时任务内部类:TimerJob.java)
import com.bdsecurity.entity.dttm.constant.DatabaseGatewayConstant;import com.bdsecurity.utils.AspectMethodParamUtil;import com.sjtm.util.RedisUtil;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.util.*;/*** @author Wanghs* @create 2022/2/22* @description 切面类*/4jpublic class DelayedDoubleDeletionAspect {private RedisUtil redisUtil;/*** 切入点*/("@annotation(com.bdsecurity.annotation.DelayedDoubleDeletion)")public void pointCut() {}/*** 环绕通知* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型** @param proceedingJoinPoint*/("pointCut()")public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {log.info("----------- start delayed double deletion ~ ~ -----------");// 获取到方法参数,从而得到key,进行删除缓存操作Map<String, Object> params = AspectMethodParamUtil.getNameAndValue(proceedingJoinPoint);String[] key = (String[]) params.get("key");redisUtil.del(key);//执行加入双删注解的改动数据库的业务 即service中的方法业务Object proceed = null;try {proceed = proceedingJoinPoint.proceed();} catch (Throwable throwable) {log.error("原本更新数据库数据业务报错:{}", throwable);}/*** 使用 Timer 工具类延迟3秒(此处是3秒举例,可以根据自己的业务来改动)* 在Timer任务中中删除,之后将业务代码的结果返回 这样不影响业务代码的执行*/Timer timer = new Timer();timer.schedule(new TimerJob(timer, key, redisUtil), DatabaseGatewayConstant.THREE_SECOND_DELAYED_DOUBLE_DELETION_TIME);return proceed;//返回业务代码的值}}4jclass TimerJob extends TimerTask {Timer timer = null;String[] key = null;RedisUtil redisUtil = null;public TimerJob() {}public TimerJob(Timer timer, String[] key, RedisUtil redisUtil) {this.timer = timer;this.key = key;this.redisUtil = redisUtil;}public void run() {log.info("----------- start timer job to del key ~ ~ -----------");try {redisUtil.del(key);} catch (Exception e) {log.error("Timer job 执行异常:{}", e);}}}
获取方法参数工具类:(AspectMethodParamUtil.java)
import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.reflect.CodeSignature;import java.util.HashMap;import java.util.Map;/*** @author Wanghs* @create 2022/2/22* @description 工具类,获取方法的参数*/public class AspectMethodParamUtil {/*** 获取某个Method的参数名称及对应的值** @param joinPoint* @return Map<参数名称, 参数值></参数名称,参数值>*/public static Map<String, Object> getNameAndValue(ProceedingJoinPoint joinPoint) {Map<String, Object> param = new HashMap<>();Object[] paramValues = joinPoint.getArgs();String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();for (int i = 0; i < paramNames.length; i++) {param.put(paramNames[i], paramValues[i]);}return param;}}
使用:
public Result exampleMethod(String[] key) {// 处理原本的逻辑,更新数据库操作}
总结
采用切面方式完成延迟双删,增强了代码的可维护性和易读性。延时的时长根据业务的实际情况来决定,通常时长在1~5s。
