Sprinig Boot elegantly implements interface idempotency, which was so simple


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.


Keywords: Java Redis Jedis SpringBoot Lombok

Added by reivax_dj on Tue, 03 Sep 2019 21:09:11 +0300