API幂等设计

API幂等就是在新增或更新数据时,如果多次发起同一个请求,只能产生一个结果。如:同一个订单多次提交,只能在数据库产生一个订单数据。我了解的基于redis实现幂等的有两种方式:基于token和基于请求。

基于token认证

参考大神:

https://blog.csdn.net/id5555/article/details/105575435

  • 客户端获取服务端token, 服务端产生token之后将token放入redis中;
  • 客户端将获取的token放入请求头或请求参数中,发起提交请求;
  • 服务器端检验请求的token,如果没有就报错;如果有就查询redis中有无对应的token,如果没有就重复请求,如果有就删除token,放行请求;
  • 服务器重复请求时,由于之前的token被删除,请求被拦截,从而实现幂等
    在这里插入图片描述

这里介绍使用自定义注解+拦截器的方式实现幂等校验。Redis相关的配置见springboot集成redis

首先定义注解:

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

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}

接着定义token接口:

import javax.servlet.http.HttpServletRequest;

public interface ITokenService {
    /**
     * 创建token
     * @return token
     */
    public  String createToken();

    /**
     * 检验token
     * @param request
     * @return true/false
     */
    public boolean checkToken(HttpServletRequest request) throws Exception;
}

定义实现类:

import com.xxx.xlt.utils.constant.ApiResult;
import com.xxx.xlt.utils.constant.RedisConstant;
import com.xxx.xlt.utils.exception.CommonException;
import com.xxx.xlt.utils.redis.RedisUtil;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

/**
 * Token service
 */
@Service
public class TokenService implements ITokenService {
    /**
     * 创建token
     *
     * @return token
     */
    @Override
    public String createToken() {
        String token = UUID.randomUUID().toString().replace("-", "");
        try {
            String key = RedisConstant.PREFIX + RedisConstant.API_TOKEN;
            RedisUtil.set(key, token, 30L);
            if (!StringUtils.isEmpty(token)) {
                return token;
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    /**
     * 检验token
     *
     * @param request http request
     * @return true/false
     */
    @Override
    public boolean checkToken(HttpServletRequest request) throws Exception {

        String token = request.getHeader(RedisConstant.API_TOKEN);
        if (StringUtils.isEmpty(token)) {// header中不存在token
            token = request.getParameter(RedisConstant.API_TOKEN);
            if (StringUtils.isEmpty(token)) {// parameter中也不存在token
                throw new CommonException(ApiResult.BAD_ARGUMENT);
            }
        }

        String key = RedisConstant.PREFIX + RedisConstant.API_TOKEN;
        if (!RedisUtil.hasKey(key)) {
            throw new CommonException(ApiResult.REPETITIVE_REQUEST);
        }
       if(RedisUtil.get(key).equals(token)) {
           RedisUtil.del(token);
       } else {
           throw new CommonException(ApiResult.API_TOKEN_ERROR);
       }
        return true;
    }
}

定义认证拦截器

import com.xxx.xlt.utils.constant.ApiResult;
import com.xxx.xlt.utils.exception.CommonException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

@Slf4j
public class AuthInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private ITokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        //被ApiIdempotent标记的扫描
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {
            try {
                return tokenService.checkToken(request); // 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
            } catch (Exception ex) {
                throw new CommonException(ApiResult.REPETITIVE_REQUEST);
            }
        }
        return true;
    }
}

Web Mvc配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Bean
    public AuthInterceptor authInterceptor() {
        return new AuthInterceptor();
    }

    /**
     * 拦截器配置
     *
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor());
//                .addPathPatterns("/ksb/**")
//                .excludePathPatterns("/ksb/auth/**", "/api/common/**", "/error", "/api/*");
        super.addInterceptors(registry);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**").addResourceLocations(
                "classpath:/static/");
        registry.addResourceHandler("swagger-ui.html").addResourceLocations(
                "classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars/");
        super.addResourceHandlers(registry);
    }
}

测试类

import com.xxx.xlt.constant.Constant;
import com.xxx.xlt.model.BasicResponse;
import com.xxx.xlt.utils.idempotent.AutoIdempotent;
import com.xxx.xlt.utils.idempotent.ITokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试接口幂等
 */
@RestController
@RequestMapping("/idempotent")
public class BusinessController {


    @Autowired
    private ITokenService tokenService;

    @GetMapping("/get/token")
    public Object  getToken(){
        String token = tokenService.createToken();
        return  new BasicResponse(token,"200", Constant.Status.SUCCESS);
    }


    @AutoIdempotent
    @GetMapping("/test")
    public Object testIdempotence() {
        String token = "接口幂等性测试成功";
        return new BasicResponse(token,"200", Constant.Status.SUCCESS);
    }
}

基于请求参数

参考大神:https://blog.csdn.net/hanchao5272/article/details/92073405

这种方式采用注解+切面的方式实现,比前面的方式简单一些,不需要客户端提前获取token,然后再发起业务请求;它根据入参的情况产生幂等的key, 并将key存入redis中一段时间,标识该请求已经发生了,后面再发情同样参数的请求,会校验Redis中是否存在该幂等key, 如果存在则拦截该请求;如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jk3EbmtA-1610467009378)(D:%5C%E6%88%91%E7%9A%84%E6%96%87%E6%A1%A3%5Ctypora%E6%96%87%E6%A1%A3%5Cpictures%5CAPI%E5%B9%82%E7%AD%89%E8%AE%BE%E8%AE%A1%5Cimage-20210112231132923.png)]

定义注解@Idempotent:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
    /**
     * 幂等名称,作为redis缓存Key的一部分。
     */
    String value();

    /**
     * 幂等过期时间,即:在此时间段内,对API进行幂等处理。
     */
    long expireMillis();
}

定义切面处理类IdempotentAspect.java

@Aspect
@Component
@ConditionalOnClass(RedisTemplate.class)
public class IdempotentAspect {
    private static Logger logger = LoggerFactory.getLogger(IdempotentAspect.class);

    /**
     * 根据实际路径进行调整
     */
    @Pointcut("@annotation(com.xxx.xlt.utils.idempotent.method2.Idempotent)")
    public void executeIdempotent() {
    }

    @Around("executeIdempotent()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //获取幂等注解
        Idempotent idempotent = method.getAnnotation(Idempotent.class);
        //根据 key前缀 + @Idempotent.value() + 方法签名 + 参数 构建缓存键值
        //确保幂等处理的操作对象是:同样的 @Idempotent.value() + 方法签名 + 参数
        String key = String.format("idempotent_%s", idempotent.value() + "_" + KeyUtil.generate(method, joinPoint.getArgs()));
        //存入redis
        if (RedisUtil.hasKey(key)&&RedisUtil.get(key).equals(key)) {
            throw new IdempotentException("Repetitive request for "+key);
        }
        RedisUtil.set(key, key, idempotent.expireMillis());
        return joinPoint.proceed();
    }
}

工具类KeyUtil.java

public class KeyUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(KeyUtil.class);

    /**
     * 根据{方法名 + 参数列表}和md5转换生成key
     */
    public static String generate(Method method, Object... args) {
        StringBuilder sb = new StringBuilder(method.toString());
        for (Object arg : args) {
            sb.append(toString(arg));
        }
        return DigestUtils.md5DigestAsHex(sb.toString().getBytes());
    }

    private static String toString(Object object) {
        if (object == null) {
            return "null";
        }
        if (object instanceof Number) {
            return object.toString();
        }
        //调用json工具类转换成String
        return JsonUtil.toJson(object);
    }
}

/**
 * Json格式化工具
 *
 * @author Alex
 */
class JsonUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtil.class);
    private static final ObjectMapper MAPPER = new ObjectMapper();

    static {
        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

    /**
     * Java Object Maps To Json
     */
    public static String toJson(Object obj) {
        String result;
        if (obj == null || obj instanceof String) {
            return (String) obj;
        }
        try {
            result = MAPPER.writeValueAsString(obj);
        } catch (Exception e) {
            LOGGER.error("Java Object Maps To Json Error !");
            throw new RuntimeException("Java Object Maps To Json Error !", e);
        }
        return result;
    }
}

使用示例:

    @Override
    @Idempotent(value="OrderService.addNewOrderHead",expireMillis=100L)
    public CommonResponse<OrderHead> addNewOrderHead(OrderHead orderHead) {
        CommonResponse<OrderHead> response = new CommonResponse<>();
        if (StringUtils.isEmpty(orderHead.getOrderDate())) {
            throw new CommonException("orderDate is empty.");
        }
        if (StringUtils.isEmpty(orderHead.getOrderNo())) {
            throw new CommonException("orderNo is empty.");
        }
        Long orderId = SnowflakeIdGenerator.generateId();
        orderHead.setOrderHeadId(orderId);
        orderHeadMapper.insertOrderHead(orderHead);
        response.setData(Collections.singletonList(orderHead));
        return response;
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值