基于redis实现数据幂等性注解

在分布式环境中,多个服务可能同时对同一个数据进行修改操作,这时候就需要考虑如何保证数据的幂等性,避免出现数据的重复修改或者数据修改的混乱情况。其中一个实现方案是使用redis作为分布式锁,实现对数据修改的幂等性注解。具体实现步骤如下:

1.在redis中创建一个哈希表,用于存储被修改的数据的主键和修改标识(可以是一个随机数或者时间戳)的对应关系。

2.在数据修改方法上添加注解,注解中包含待修改数据的主键和修改标识,如下所示:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    String key();  // 待修改数据的主键
    String value();  // 修改标识
}

3.在注解处理器中,首先根据注解中的主键和修改标识从redis中获取对应的值。如果值存在,说明此次修改已经执行过,直接返回结果;如果值不存在,则使用redis分布式锁对该主键进行加锁,避免其他服务同时对该数据进行修改操作。加锁成功后,再次从redis中获取修改标识的值,如果值仍然存在,则说明此次修改已经执行过,直接返回结果。否则,执行实际的数据修改操作,并将修改标识写入redis中,以便下一次判断。最后解锁该主键,释放锁资源。

4.在注解处理器中,还需要添加异常处理逻辑,当redis连接异常或者redis分布式锁加锁失败时,需要抛出异常,避免数据修改操作失败。

以上就是基于redis实现数据幂等性注解的具体步骤。需要注意的是,使用redis实现幂等性注解可能会带来一定的性能开销,需要根据具体情况进行评估和调优。

源码:

/**
 * 接口幂等性注解
 */
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {

    /**
     * 时间单位,默认为秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 间隔时间,默认为5秒
     */
    int interval() default 5;
}

实现

@Slf4j
@Aspect
@Component
public class IdempotentAspect {
    @Resource
    private RedisUtil redisUtil;

    @Pointcut("@annotation(com.common.business.annotation.Idempotent)")
    public void idempotentPointCut() {
    }

    @Around("idempotentPointCut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Method method = currentMethod(proceedingJoinPoint);
        //获取到方法的注解对象
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
        //单位 秒
        long interval = 60;
        if (idempotent.interval() > 0) {
            interval = idempotent.timeUnit().toSeconds(idempotent.interval());
        }
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = null;
        if (attributes != null) {
            request = attributes.getRequest();
        }
        String params = argsToString(proceedingJoinPoint.getArgs());
        String url = null;
        String token = null;
        if (request != null) {
            // 请求地址(作为存放cache的key值)
            url = request.getRequestURI();
            // 用户的唯一标识
            token = request.getHeader("Authorization");
        }
        // 唯一标识(url +  token  + params)
        String submitKey = "INTERFACE:" + MD5Util.toMD5(url + "_" + token + ":" + params);
        boolean flag = false;
        //判断缓存中是否有此key
        if (redisUtil.hasKey(submitKey)) {
            log.info("key={},interval={},重复提交", submitKey, interval);
        } else {
            //如果没有表示不是重复提交并设置key存活的缓存时间
            redisUtil.set(submitKey, "", interval);
            flag = true;
            System.out.println("非重复提交");
        }
        if (flag) {
            Object result;
            try {
                result = proceedingJoinPoint.proceed();
            } catch (Throwable e) {
                /*异常通知方法*/
                log.error("异常通知方法>目标方法名{},异常为:{}", method.getName(), e);
                throw e;
            } finally {
                redisUtil.del(submitKey);
            }
            return result;
        } else {
            throw new ServiceException(ApiError.ERROR_1014);
        }
    }

    /**
     * 根据切入点获取执行的方法
     */
    private Method currentMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        //获取目标类的所有方法,找到当前要执行的方法
        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        Method resultMethod = null;
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                resultMethod = method;
                break;
            }
        }
        return resultMethod;
    }

    /**
     * 参数拼装
     */
    private String argsToString(Object[] paramsArray) {
        StringBuilder params = new StringBuilder();
        if (paramsArray != null && paramsArray.length > 0) {
            for (Object o : paramsArray) {
                if (!ObjectUtils.isEmpty(o) && !isFilterObject(o)) {
                    try {
                        params.append(JSONObject.toJSONString(o)).append(" ");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return params.toString().trim();
    }

    /**
     * 判断是否是需要过滤的对象
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o) {
        Class<?> clazz = o.getClass();
        if (clazz.isArray()) {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            Collection collection = (Collection) o;
            for (Object value : collection) {
                return value instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            Map map = (Map) o;
            for (Object value : map.entrySet()) {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }
}

数据幂等性注解实现(支持批量操作)

/**
 * 数据幂等性注解 支持参数标记
 */
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataIdempotent {
    /**
     * 入参主键id的名称
     */
    String keyIdName() default "";

    /*** 上锁时长,默认设置时间 30秒
     *** @return
     **/
    long leaseTime() default -1L;

    /***
     * 尝试时间,设置时间内通过自旋一致尝试获取锁,
     * 默认 0秒
     * 通常时间要小于 leaseTime 时间**
     * @return
     * */
    long waitTime() default 0L;

    /**
     * 业务类型
     *
     * @return
     */
    String businessType() default "";
}

实现

@Slf4j
@Aspect
@Component
public class DataIdempotentAspect {
    @Resource
    private RedissonClient redissonClient;

    private static final ThreadLocal<List<RLock>> LOCK_THREAD = new ThreadLocal<>();

    @Pointcut("@annotation(com.common.business.annotation.DataIdempotent)")
    public void dataPointCut() {
    }

    @Around("dataPointCut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Method method = currentMethod(proceedingJoinPoint);
        //获取到方法的注解对象
        DataIdempotent idempotent = method.getAnnotation(DataIdempotent.class);
        //单位 秒
        long leaseTime = idempotent.leaseTime();
        long waitTime = idempotent.waitTime();
        String businessType = idempotent.businessType();
        //获取传参
        Object obj = proceedingJoinPoint.getArgs()[0];

        List<Object> objList = new ArrayList<>();
        if (StringUtils.isNotBlank(idempotent.keyIdName())) {
            if (obj instanceof String || obj instanceof Long || obj instanceof Integer) {
                objList.add(String.valueOf(obj));
            } else if (obj instanceof List) {
                objList = (List<Object>) obj;
            } else if (obj instanceof String[]) {
                objList = Arrays.asList((String[]) obj);
            } else if (Objects.nonNull(obj)) {
                Map map = JSONObject.parseObject(JSONObject.toJSONString(obj), Map.class);
                Object object = map.get(idempotent.keyIdName());
                //单个参数 或 list
                if (object instanceof String || object instanceof Long || object instanceof Integer) {
                    objList.add(String.valueOf(object));
                } else if (object instanceof String[]) {
                    objList = Arrays.asList((String[]) object);
                } else if (object instanceof List) {
                    objList = (List<Object>) object;
                }
            }
        }
        if (CollectionUtil.isNotEmpty(objList)) {
            List<RLock> rLocks = new ArrayList<>();
            objList.forEach(o -> {
                try {
                    String submitKey = "BUSINESS:" + o + "_" + businessType;
                    log.info("分布式锁上锁,key:{},lockTime:{}", submitKey, leaseTime);
                    RLock clientLock = redissonClient.getLock(submitKey);

                    //不设置 lockTime watch dog会 默认 锁定30s 10s重试
                    boolean locked = clientLock.tryLock(waitTime,leaseTime, TimeUnit.SECONDS);
                    if (!locked) {
                        log.error("{}上锁失败", submitKey);
                        throw new ServiceException(ApiError.ERROR_1026);
                    }
//                    clientLock.lock(lockTime, TimeUnit.SECONDS);
                    rLocks.add(clientLock);
                    log.info("分布式锁上锁成功,key:{},lockTime:{}", submitKey, leaseTime);
                } catch (Exception e) {
                    //存在不能上锁情况时 释放已上锁对象
                    if (CollectionUtil.isNotEmpty(rLocks)) {
                        // 无需判断锁是否存在,直接调用 unlock
                        rLocks.forEach(rLock -> {
                            if (rLock.isLocked()) {
                                rLock.unlock();
                            }
                        });
                    }
                    throw new ServiceException(ApiError.ERROR_1026);
                }
            });
            if (CollectionUtil.isNotEmpty(rLocks)) {
                LOCK_THREAD.set(rLocks);
            }
        }
        // 调用目标方法
        return proceedingJoinPoint.proceed();
    }

    /*** 处理完请求后执行
     *  @param joinPoint 切点
     */
    @AfterReturning(value = "dataPointCut()", returning = "apiResult")
    public void doAfterReturning(JoinPoint joinPoint, Object apiResult) {
        handleData();
    }

    /*** 拦截异常操作
     * ** @param joinPoint 切点
     * * @param e         异常
     * */
    @AfterThrowing(value = "dataPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleData();
    }

    /**
     * 根据切入点获取执行的方法
     */
    private Method currentMethod(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        //获取目标类的所有方法,找到当前要执行的方法
        Method[] methods = joinPoint.getTarget().getClass().getMethods();
        Method resultMethod = null;
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                resultMethod = method;
                break;
            }
        }
        return resultMethod;
    }

    private void handleData() {
        List<RLock> rLocks = LOCK_THREAD.get();
        if (CollectionUtil.isNotEmpty(rLocks)) {
            try {
                rLocks.forEach(rLock -> {
                    log.info("任务执行完成,当前锁状态:{}", rLock.isLocked());
                    // 无需判断锁是否存在,直接调用 unlock
                    if (rLock.isLocked()) {
                        rLock.unlock();
                        log.info("释放锁");
                    }
                });
            } catch (Exception exception) {
                exception.printStackTrace();
            } finally {
                LOCK_THREAD.remove();
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值