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*/@Overridepublic 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*/@Overridepublic boolean checkToken(HttpServletRequest request) throws Exception {String token = request.getHeader(RedisConstant.API_TOKEN);if (StringUtils.isEmpty(token)) {// header中不存在tokentoken = request.getParameter(RedisConstant.API_TOKEN);if (StringUtils.isEmpty(token)) {// parameter中也不存在tokenthrow 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 {@Autowiredprivate ITokenService tokenService;@Overridepublic 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 {@Beanpublic AuthInterceptor authInterceptor() {return new AuthInterceptor();}/*** 拦截器配置** @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(authInterceptor());
// .addPathPatterns("/ksb/**")
// .excludePathPatterns("/ksb/auth/**", "/api/common/**", "/error", "/api/*");super.addInterceptors(registry);}@Overridepublic 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 {@Autowiredprivate 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, 如果存在则拦截该请求;如下图所示:
定义注解@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()));//存入redisif (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工具类转换成Stringreturn 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;}