Java实战指南|幂等性-公共幂等组件实现

这是我参与更文挑战的第9天,活动详情查看: 更文挑战

前言:

幂等性指的是多次操作,结果是一致的,其任意多次执行所产生的影响均与一次执行的影响相同,例如我们多个线程去查询数据库,多次操作数据库数据要保证一致。现在企业级的事项目大部分都是分布式的,而在分布式环境下幂等是非常常见也是我们必须要解决的问题;

常见的幂等解决方案:

  • 唯一键索引
  • 分布式锁 (redis(jedis、redisson)或zookeeper实现)
  • tonken机制 (流水号,防止页面重复提交)
  • 悲观锁(获取数据的时候加锁(锁表或锁行))
  • 乐观锁 (基于版本号version实现, 在更新数据那一刻校验数据)
  • 状态机 (状态变更, 更新数据时判断状态)

我们今天主要说一个代码层面通过注解的形式来实现一个通用幂等组件

其大体流程如下图:
图片.png

设计注解(ElementType.METHOD方法层面):
(默认值expireTime为过期时间也就是一定时间内要求 当前用户当前任务只能执行一次)


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 幂等校验
 *
 * @author taoze
 * @version 1.0
 * @date 4/6/21 11:00 AM
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
    /**
     * 过期时间  默认14400秒
     *
     * @return
     */
    int expireTime() default 14400;
}
复制代码

扫描注解AOP(可以根据自己的业务去实现自己的注解扫描验证条件):

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 幂等效验Aop
 * (BaseController只是统一返回结果集及项目个性化要求,可以不写)
 * @author taoze
 * @version 1.0
 * @date 4/6/21 11:06 AM
 */
@Aspect
@Component
@Slf4j
public class IdempotentAspect extends BaseController {
    @Autowired
    private IdempotentHandler idempotentHandler;

    @Around(value = "@annotation(com.a.b.service.api.idempotent.ApiIdempotent)")
    public Object IdempotentAspectMethod(ProceedingJoinPoint point) throws Throwable {
        long startExecute = System.currentTimeMillis();
        MethodSignature signature = (MethodSignature)point.getSignature();
        ApiIdempotent annotation = signature.getMethod().getAnnotation(ApiIdempotent.class);
        int expireTime = annotation.expireTime();
        Object[] args = point.getArgs();
        //获取header
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes)ra;
        HttpServletRequest request = sra.getRequest();
        String requestId = request.getHeader("requestId");
        if (StringUtils.isBlank(requestId)) {
            return this.success();
        }
        Integer userId = getCurrentUserId();
        if(null==userId){
            return this.success();
        }
        String url = request.getRequestURL().toString();
        int startIndex = StringUtils.ordinalIndexOf(url, "/", 3);
        String key = url.substring(startIndex + 1) + userId;

        log.info("IdempotentAspect -> IdempotentAspectMethod url = [{}] | @ApiIdempotent = [{}] | param = [{}]", url,
            expireTime, args);
        if (!idempotentHandler.checkToken(key, requestId, expireTime)) {
            return this.success();
        }
        Object proceed = point.proceed();
        log.info("IdempotentAspect -> IdempotentAspectMethod url = [{}] 结束,耗时: {}", url,
            (System.currentTimeMillis() - startExecute));
        return proceed;
    }
}
复制代码

幂等校验业务实现(小编用的Redis,大家也可以根据自己的情况使用别的):

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 幂等校验
 *
 * @author taoze
 * @version 1.0
 * @date 4/2/21 11:46 AM
 */
@Component
@Slf4j
public class IdempotentHandler {
    @Autowired
    private RedisConnection<String, Object> redisConnectionCustom;
    @Autowired
    private PeppaCoreRedisClientConfig redisClientConfig;

    /**
     * 效验幂等
     *
     * @param key
     * @param value      请求Id
     * @param expireTime 过期时间
     * @return
     */
    public boolean checkToken(String key, String value, int expireTime) {
        if (StringUtils.isBlank(value) || StringUtils.isBlank(key)) {
            throw new DomainException(ClassroomServiceErrorEnum.ASSET_PARAM_ERROR);
        }

        key = key.replace("/", "-");
        Long addNum = redisClientConfig.sAdd(RedisConstant.IDEMPOTENT_TOKEN_KEY + key, value);
        if (addNum == 1) {
            redisClientConfig.expire(RedisConstant.IDEMPOTENT_TOKEN_KEY + key, expireTime);
            return true;
        }
        return false;
    }

    /**
     * 效验幂等 默认过期时间 14400秒
     *
     * @param key
     * @param value
     * @return
     */
    public boolean checkToken(String key, String value) {
        return checkToken(key, value, 14400);
    }
}

复制代码

ok!一个简单的幂等组件实现了,当前代码只是普通的幂等实现,大家也可以根据自己的需求去做优化优化,希望可以对大家有帮助,有不对的地方希望大家可以提出来的,共同成长;

整洁成就卓越代码,细节之中只有天地

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享