1. Concepts
Idempotency, commonly referred to as an interface, makes the same request multiple times and must ensure that the operation can only be performed once
For example:
Order interface, order cannot be created more than once
Payment interface, double payment of the same order can only be deducted once
Alipay callback interface, may have multiple callbacks, must handle duplicate callbacks
Common form submission interface, because of network timeout and other reasons, multiple clicks submit, can only succeed once
Wait
2. Common Solutions
Unique Index--Prevent new dirty data
token mechanism--prevent page duplicate submission
Pessimistic Lock -- Lock when data is acquired (lock table or row)
Optimistic Lock--Validate data at the moment it is updated based on version number implementation
Distributed Lock -- redis(jedis, redisson) or zookeeper implementation
State Machine - State change, state judgement when updating data
3. Realization of this Article
In this paper, the second way is to implement the interface idempotency check by using the redis + token mechanism.
4. Ideas for implementation
To create a unique token for each request that needs to be idempotent, first get the token and save it in redis. When requesting an interface, place the token in the header or as a request parameter to the request interface. The back-end interface determines if this token exists in redis:
If it exists, the business logic is processed properly, and the token is removed from redis, then if it is a duplicate request, because the token has been deleted, it cannot pass the check and return the Do Not Repeat prompt
If it does not exist, the parameter is illegal or the request is repeated, then the prompt can be returned.
5. Project introduction
springboot
redis
@ApiIdempotent comment + interceptor intercepts requests
@ControllerAdvice Global Exception Handling
Pressure tool: jmeter
Explain:
This article focuses on idempotent core implementations, and the details of how springboot integrates redis, ServerResponse, ResponseCode, and so on are beyond the scope of this article.
6. Code implementation
pom
<!-- Redis-Jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <!--lombok Used in this article@Slf4j annotation, Or not to reference, custom log that will do--> <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(); } /** * Set Value * * @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 Comment@ApiIdempotent
/** * Use this annotation on Controller methods that need to guarantee interface idempotency */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ApiIdempotent { }
ApiIdempotentInterceptor Interceptor
/** * Interface idempotent interceptor */ public class ApiIdempotentInterceptor implements HandlerInterceptor { @Autowired private TokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class); if (methodAnnotation != null) { check(request);//Identity checks, pass checks, fail checks, throw exceptions, and return friendly hints through uniform exception handling } return 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)) {//No token exists in header token = request.getParameter(TOKEN_NAME); if (StringUtils.isBlank(token)) {//Nor does token exist in parameter 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()); } } }
TestApplication
@SpringBootApplication @MapperScan("com.wangzaiplus.test.mapper") public class TestApplication extends WebMvcConfigurerAdapter { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } /** * Cross-domain * @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) { //Interface idempotent interceptor registry.addInterceptor(apiIdempotentInterceptor()); super.addInterceptors(registry); } @Bean public ApiIdempotentInterceptor apiIdempotentInterceptor() { return new ApiIdempotentInterceptor(); } }
OK, so far, the check code is ready, then test the validation
7. Test Verification
Get token's controller TokenController
@RestController @RequestMapping("/token") public class TokenController { @Autowired private TokenService tokenService; @GetMapping public ServerResponse token() { return tokenService.createToken(); } }
TestController, note the @ApiIdempotent annotation, which can be declared on methods that require idempotency checks, without the need for checks to have no effect.
@RestController @RequestMapping("/test") @Slf4j public class TestController { @Autowired private TestService testService; @ApiIdempotent @PostMapping("testIdempotence") public ServerResponse testIdempotence() { return testService.testIdempotence(); } }
3. Get token
View redis
4. Testing interface security: Simulate 50 concurrent requests using the jmeter test tool, taking the token obtained in the previous step as a parameter.
5. Neither the header nor the parameter passes a token, or the token value is empty, or the token value is filled in randomly and cannot pass the check, such as a token value of "abcd"
8. Points of attention (very important)
In the figure above, you cannot simply delete a token without checking whether it was deleted successfully. Concurrency security issues arise because it is possible that multiple threads may go to line 46 at the same time and the token has not yet been deleted, so proceed. If you do not check the deletion result of jedisUtil.del(token), it will still go out.Repeat submitting the question now, even if there is actually only one real deletion, repeat below.
Modify the code a little:
Request Again
Look at the console again
Although only one token was actually deleted, there was a concurrency problem because the deletion results were not checked, so they had to be checked.
9. Summary
In fact, the idea is simple, that is, each request guarantees uniqueness, thus ensuring idempotency. With interceptor + annotation, you do not need to write duplicate code for each request. In fact, you can also use spring aop to achieve, it does not matter.
If your partner has any questions or suggestions, you are welcome to make them.