First, the concept
Idempotency, popular to say that an interface with a request to initiate multiple times, must ensure that the operation can only be performed once
such as:
Orders interfaces, you can not create multiple orders
Payment interface, double payments for the same order only once deducted money
Alipay callback interface, multiple callbacks may be, have to deal with repeated callbacks
Common form submission interfaces, network timeouts and other reasons because click submit multiple times, only once successfully
and many more
Second, the common solution
The only index - prevent new dirty data
token mechanisms - to prevent duplicate submission page
Pessimistic locking - time access to data lock (lock or lock table rows)
Optimistic locking - based version version realized, at that moment the update data verification data
Distributed Lock - redis (jedis, redisson) or zookeeper achieve
State Machine - state changes, status update data is determined
Third, the paper implements
In this paper, two kinds of ways, i.e., by an interface mechanism to achieve redis + token verification idempotent.
Fourth, the realization of ideas
Idempotency need to ensure that every request to create a unique token identifier, to obtain the token, and the token is stored redis this, when the request interface, this token into the request header parameter or a request interface, backend interface determines redis whether there is this token:
If so, normal processing business logic, and delete the token from redis, then, if the request is repeated, since the token has been removed, you can not check, do not repeat the operation returns tips
If not, the parameters described are not legal or repeat request, returns prompts.
V. Project
springboot
repeat
+ @ApiIdempotent annotation request interceptor intercepts
@ControllerAdvice global exception handler
Pressure measurement tools: jmeter
Description:
This article focuses on idempotency core implementation on how springboot integration redis, ServerResponse, ResponseCode and other minutiae is not within the scope of this discussion.
Sixth, code implementation
pom
<!-- Redis-Jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <!--lombok 本文用到@Slf4j注解, 也可不引用, 自定义log即可--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.10</version> </dependency>
JedisUtil
@Component @Slf4j public class JedisUtil { @Autowired private JedisPool jedisPool; private Jedis getJedis() { return jedisPool.getResource(); } /** * 设值 * * @param key * @param value * @return */ public String set(String key, String value) { Jedis jedis = null; try { jedis = getJedis(); return jedis.set(key, value); } catch (Exception e) { log.error("set key:{} value:{} error", key, value, e); return null; } finally { close(jedis); } } ..... }
Custom annotation @ApiIdempotent
/ ** * The need to ensure that the interfaces of the Controller idempotent method uses this annotation * / @Target (ElementType.METHOD {}) @Retention (RetentionPolicy.RUNTIME) public @interface ApiIdempotent { }
ApiIdempotentInterceptor interceptor
/ ** * idempotent interceptor interfaces * / public class ApiIdempotentInterceptor HandlerInterceptor from the implements { @Autowired Private TokenService tokenService; @Override public Boolean The preHandle (the HttpServletRequest Request, Response the HttpServletResponse, Object Handler) { IF (! (Handler HandlerMethod the instanceof)) { to true return; } HandlerMethod HandlerMethod = (HandlerMethod) Handler; Method, Method handlerMethod.getMethod = (); ApiIdempotent methodAnnotation = method.getAnnotation (ApiIdempotent.class); IF (! methodAnnotation = null) { Check (Request); // idempotent check, check through the release, check fails an exception is thrown, and the friendly prompt return through a unified exception handling } return to true; } private void check(HttpServletRequest request) { tokenService.checkToken(request); } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
TokenServiceImpl
@Service public class TokenServiceImpl implements TokenService { private static final String TOKEN_NAME = "token"; @Autowired private JedisUtil jedisUtil; @Override public ServerResponse createToken() { String str = RandomUtil.UUID32(); StrBuilder token = new StrBuilder(); token.append(Constant.Redis.TOKEN_PREFIX).append(str); jedisUtil.set(token.toString(), token.toString(), Constant.Redis.EXPIRE_TIME_MINUTE); return ServerResponse.success(token.toString()); } @Override public void checkToken(HttpServletRequest request) { String token = request.getHeader(TOKEN_NAME); if (StringUtils.isBlank(token)) {// header中不存在token token = request.getParameter(TOKEN_NAME); if (StringUtils.isBlank(token)) {// parameter中也不存在token throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg()); } } if (!jedisUtil.exists(token)) { throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg()); } Long del = jedisUtil.del(token); if (del <= 0) { throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg()); } } }
test Application
@SpringBootApplication @MapperScan("com.wangzaiplus.test.mapper") public class TestApplication extends WebMvcConfigurerAdapter { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } /** * 跨域 * @return */ @Bean public CorsFilter corsFilter() { final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource(); final CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowCredentials(true); corsConfiguration.addAllowedOrigin("*"); corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedMethod("*"); urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); return new CorsFilter(urlBasedCorsConfigurationSource); } @Override public void addInterceptors(InterceptorRegistry registry) { // 接口幂等性拦截器 registry.addInterceptor(apiIdempotentInterceptor()); super.addInterceptors(registry); } @Bean public ApiIdempotentInterceptor apiIdempotentInterceptor() { return new ApiIdempotentInterceptor(); } }
OK, 目前为止, 校验代码准备就绪, 接下来测试验证
七、测试验证
获取token的控制器TokenController
@RestController @RequestMapping("/token") public class TokenController { @Autowired private TokenService tokenService; @GetMapping public ServerResponse token() { return tokenService.createToken(); } }
TestController, 注意@ApiIdempotent注解, 在需要幂等性校验的方法上声明此注解即可, 不需要校验的无影响。
@RestController @RequestMapping("/test") @Slf4j public class TestController { @Autowired private TestService testService; @ApiIdempotent @PostMapping("testIdempotence") public ServerResponse testIdempotence() { return testService.testIdempotence(); } }
3. 获取token
查看redis
4. 测试接口安全性: 利用jmeter测试工具模拟50个并发请求, 将上一步获取到的token作为参数。
5. header或参数均不传token, 或者token值为空, 或者token值乱填, 均无法通过校验, 如token值为"abcd"
八、注意点(非常重要)
上图中, 不能单纯的直接删除token而不校验是否删除成功, 会出现并发安全性问题, 因为, 有可能多个线程同时走到第46行, 此时token还未被删除, 所以继续往下执行, 如果不校验jedisUtil.del(token)的删除结果而直接放行, 那么还是会出现重复提交问题, 即使实际上只有一次真正的删除操作, 下面重现一下。
稍微修改一下代码:
再次请求
再看看控制台
虽然只有一个真正删除掉token, 但由于没有对删除结果进行校验, 所以还是有并发问题, 因此, 必须校验。
九、总结
其实思路很简单, 就是每次请求保证唯一性, 从而保证幂等性, 通过拦截器+注解, 就不用每次请求都写重复代码, 其实也可以利用spring aop实现, 无所谓。
如果小伙伴有什么疑问或者建议欢迎提出。