赞
踩
通过token机制来保证幂等是一种非常常见的解决方案,同时也适合绝大部分场景。该方案需要前后端进行一定程度的交互来完成。
1)服务端提供获取token接口,供客户端进行使用。服务端生成token后,如果当前为分布式架构,将token存放于redis中,如果是单体架构,可以保存在jvm缓存中。
2)当客户端获取到token后,会携带着token发起请求。
3)服务端接收到客户端请求后,首先会判断该token在redis中是否存在。如果存在,则完成进行业务处理,业务处理完成后,再删除token。如果不存在,代表当前请求是重复请求,直接向客户端返回对应标识。
在高并发下,很有可能出现第一次访问时token存在,完成具体业务操作。但在还没有删除token时,客户端又携带token发起请求,此时,因为token还存在,第二次请求也会验证通过,执行具体业务操作。
对于这个问题的解决方案的思想就是并行变串行。会造成一定性能损耗与吞吐量降低。
第一种方案:对于业务代码执行和删除token整体加线程锁。当后续线程再来访问时,则阻塞排队。
第二种方案:借助redis单线程和incr是原子性的特点。当第一次获取token时,以token作为key,对其进行自增。然后将token进行返回,当客户端携带token访问执行业务代码时,对于判断token是否存在不用删除,而是对其继续incr。如果incr后的返回值为2。则是一个合法请求允许执行,如果是其他值,则代表是非法请求,直接返回。
先删除token再执行业务,其实也会存在问题,假设具体业务代码执行超时或失败,没有向客户端返回
明确结果,那客户端就很有可能会进行重试,但此时之前的token已经被删除了,则会被认为是重复请求,不再进
行业务处理。
这种方案无需进行额外处理,一个token只能代表一次请求。一旦业务执行出现异常,则让客户端重新获取令牌,重新发起一次访问即可。推荐使用先删除token方案
。
但是无论先删token还是后删token,都会有一个相同的问题。每次业务请求都回产生一个额外的请求去获取token。
但是,业务失败或超时,在生产环境下,一万个里最多也就十个左右会失败,那为了这十来个请求,让其他九千九百多个请求都产生额外请求,就有一些得不偿失了。虽然redis性能好,但是这也是一种资源的浪费。
前端先通过获取token接口生成一个token,同时该token存入redis,设置过期时间。
@GetMapping("/genToken")
public String genToken(){
String token = String.valueOf(IdUtils.nextId());
redisTemplate.opsForValue().set(token,0,30, TimeUnit.MINUTES);
return token;
}
import feign.RequestInterceptor; import feign.RequestTemplate; 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; import java.util.Enumeration; @Component public class FeignInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { //传递令牌 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes != null){ HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); if (request != null){ Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()){ String hearName = headerNames.nextElement(); // 从请求header中获取token,并且传递 if ("token".equals(hearName)){ String headerValue = request.getHeader(hearName); //传递token template.header(hearName,headerValue); } } } } } }
// 定义feign拦截器
@Bean
public FeignInterceptor feignInterceptor(){
return new FeignInterceptor();
}
/** * 生成订单 * @param order * @return */ @PostMapping("/genOrder") public String genOrder(@RequestBody Order order, HttpServletRequest request){ //获取令牌 String token = request.getHeader("token"); //校验令牌 try { if (redisTemplate.delete(token)){ //令牌删除成功,代表不是重复请求,执行具体业务 order.setId(String.valueOf(idWorker.nextId())); order.setCreateTime(new Date()); order.setUpdateTime(new Date()); // 生成订单 int result = orderService.addOrder(order); if (result == 1){ System.out.println("success"); return "success"; }else { System.out.println("fail"); return "fail"; } }else { //删除令牌失败,重复请求 System.out.println("repeat request"); return "repeat request"; } }catch (Exception e){ throw new RuntimeException("系统异常,请重试"); } }
通过postman获取令牌,将令牌放入请求头中。开启两个postman tab页面。同时添加订单,可以发现一个执行成功,另一个重复请求。
直接把token实现嵌入到方法中会造成大量重复代码的出现。因此可以通过自定义注解将上述代码进行改造。在需要保证幂等的方法上,添加自定义注解即可。
/**
* 幂等性注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idemptent {
}
import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; public class IdemptentInterceptor implements HandlerInterceptor { @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(); // 如果添加了该注解 Idemptent annotation = method.getAnnotation(Idemptent.class); if (annotation != null){ //进行幂等性校验 checkToken(request); } return true; } @Autowired private RedisTemplate redisTemplate; //幂等性校验 private void checkToken(HttpServletRequest request) { String token = request.getHeader("token"); if (StringUtils.isEmpty(token)){ throw new RuntimeException("非法参数"); } boolean delResult = redisTemplate.delete(token); if (!delResult){ //删除失败 throw new RuntimeException("重复请求"); } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
注册拦截器:
@Bean
public IdemptentInterceptor idemptentInterceptor() {
return new IdemptentInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//幂等拦截器
registry.addInterceptor(idemptentInterceptor());
super.addInterceptors(registry);
}
@Idemptent
@PostMapping("/genOrder")
public String genOrder(@RequestBody Order order){
order.setId(String.valueOf(IdUtils.nextId()));
order.setCreateTime(new Date());
order.setUpdateTime(new Date());
int result = orderService.addOrder(order);
if (result == 1){
System.out.println("success");
return "success";
}else {
System.out.println("fail");
return "fail";
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。