Sprinig Boot idempotency elegant implementation of the interface, so simple


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

Sprinig Boot优雅实现接口幂等性,原来这么简单


查看redis

Sprinig Boot优雅实现接口幂等性,原来这么简单


4. 测试接口安全性: 利用jmeter测试工具模拟50个并发请求, 将上一步获取到的token作为参数。

Sprinig Boot优雅实现接口幂等性,原来这么简单


5. header或参数均不传token, 或者token值为空, 或者token值乱填, 均无法通过校验, 如token值为"abcd"

Sprinig Boot优雅实现接口幂等性,原来这么简单


八、注意点(非常重要)

Sprinig Boot优雅实现接口幂等性,原来这么简单


上图中, 不能单纯的直接删除token而不校验是否删除成功, 会出现并发安全性问题, 因为, 有可能多个线程同时走到第46行, 此时token还未被删除, 所以继续往下执行, 如果不校验jedisUtil.del(token)的删除结果而直接放行, 那么还是会出现重复提交问题, 即使实际上只有一次真正的删除操作, 下面重现一下。

稍微修改一下代码:

Sprinig Boot优雅实现接口幂等性,原来这么简单


再次请求

Sprinig Boot优雅实现接口幂等性,原来这么简单


再看看控制台

Sprinig Boot优雅实现接口幂等性,原来这么简单


虽然只有一个真正删除掉token, 但由于没有对删除结果进行校验, 所以还是有并发问题, 因此, 必须校验。

九、总结

其实思路很简单, 就是每次请求保证唯一性, 从而保证幂等性, 通过拦截器+注解, 就不用每次请求都写重复代码, 其实也可以利用spring aop实现, 无所谓。

如果小伙伴有什么疑问或者建议欢迎提出。


Guess you like

Origin blog.51cto.com/14528283/2435274