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, 如果存在则拦截该请求;如下图所示:
定义注解@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;
}