(Transfer + share) SpringBoot + Redis + Annotation + Interceptor to achieve interface idempotence check / Wangzaiplus Code Road

1. Concept

Idempotence, in layman's terms, is an interface that initiates the same request multiple times, and it must be ensured that the operation can only be performed once, for
example:

  • Order interface, no order can be created multiple times

  • Payment interface, the same order can only be deducted once for repeated payments

  • Alipay callback interface, there may be multiple callbacks, and repeated callbacks must be handled

  • Ordinary form submission interface, if you click to submit multiple times due to network timeout and other reasons, you can only succeed once,
    etc.

Two, common solutions

  1. Unique index - prevent new dirty data

  2. Token mechanism-prevent repeated page submission

  3. Pessimistic lock-lock when acquiring data (lock table or lock row)

  4. Optimistic lock-based on the version number version to verify the data at the moment the data is updated

  5. Distributed lock-redis (jedis, redisson) or zookeeper implementation

  6. State machine - state changes, judging the state when updating data

Third, the realization of this article

     This article adopts the second method to achieve the interface idempotence check through the redis + token mechanism.

Four, realization of ideas

        Create a unique token for each request that needs to ensure idempotence, get the token first, and store this token in redis, when requesting the interface, put this token in the header or request the interface as a request parameter, and the back-end interface judges redis Does this token exist in:

  • If it exists, the business logic is processed normally, and the token is deleted from redis. Then, if it is a repeated request, because the token has been deleted, it cannot pass the verification, and return to the Do not repeat operation prompt

  • If it does not exist, it means that the parameter is illegal or the request is repeated, just return to the prompt

5. Project introduction

  • springboot

  • repeat

  • @ApiIdempotent annotation + interceptor intercepts the request

  • @ControllerAdvice global exception handling

  • Pressure measurement tool: jmeter

Description:

  • This article focuses on the core implementation of idempotence. The details on how springboot integrates redis, ServerResponse, ResponseCode, etc. are beyond the scope of this article. Interested friends can check my Github project: https://github.com/wangzaiplus/springboot /tree/wxw

Six, code implementation

1.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>

2.JedisUtil

 

package com.wangzaiplus.test.util;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;@Component@Slf4jpublic 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);        }    }    /**     * 设值     *     * @param key     * @param value     * @param expireTime 过期时间, 单位: s     * @return     */    public String set(String key, String value, int expireTime) {
   
           Jedis jedis = null;        try {
   
               jedis = getJedis();            return jedis.setex(key, expireTime, value);        } catch (Exception e) {
   
               log.error("set key:{} value:{} expireTime:{} error", key, value, expireTime, e);            return null;        } finally {
   
               close(jedis);        }    }    /**     * 取值     *     * @param key     * @return     */    public String get(String key) {
   
           Jedis jedis = null;        try {
   
               jedis = getJedis();            return jedis.get(key);        } catch (Exception e) {
   
               log.error("get key:{} error", key, e);            return null;        } finally {
   
               close(jedis);        }    }    /**     * 删除key     *     * @param key     * @return     */    public Long del(String key) {
   
           Jedis jedis = null;        try {
   
               jedis = getJedis();            return jedis.del(key.getBytes());        } catch (Exception e) {
   
               log.error("del key:{} error", key, e);            return null;        } finally {
   
               close(jedis);        }    }    /**     * 判断key是否存在     *     * @param key     * @return     */    public Boolean exists(String key) {
   
           Jedis jedis = null;        try {
   
               jedis = getJedis();            return jedis.exists(key.getBytes());        } catch (Exception e) {
   
               log.error("exists key:{} error", key, e);            return null;        } finally {
   
               close(jedis);        }    }    /**     * 设值key过期时间     *     * @param key     * @param expireTime 过期时间, 单位: s     * @return     */    public Long expire(String key, int expireTime) {
   
           Jedis jedis = null;        try {
   
               jedis = getJedis();            return jedis.expire(key.getBytes(), expireTime);        } catch (Exception e) {
   
               log.error("expire key:{} error", key, e);            return null;        } finally {
   
               close(jedis);        }    }    /**     * 获取剩余时间     *     * @param key     * @return     */    public Long ttl(String key) {
   
           Jedis jedis = null;        try {
   
               jedis = getJedis();            return jedis.ttl(key);        } catch (Exception e) {
   
               log.error("ttl key:{} error", key, e);            return null;        } finally {
   
               close(jedis);        }    }    private void close(Jedis jedis) {
   
           if (null != jedis) {
   
               jedis.close();        }    }}

3. Custom annotation @ApiIdempotent

package com.wangzaiplus.test.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * 在需要保证 接口幂等性 的Controller的方法上使用此注解 */@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public @interface ApiIdempotent {
   
   }

4.ApiIdempotentInterceptor interceptor

package com.wangzaiplus.test.interceptor;import com.wangzaiplus.test.annotation.ApiIdempotent;import com.wangzaiplus.test.service.TokenService;import org.springframework.beans.factory.annotation.Autowired;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 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);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示        }        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 {
   
       }}

5.TokenServiceImpl

package com.wangzaiplus.test.service.impl;import com.wangzaiplus.test.common.Constant;import com.wangzaiplus.test.common.ResponseCode;import com.wangzaiplus.test.common.ServerResponse;import com.wangzaiplus.test.exception.ServiceException;import com.wangzaiplus.test.service.TokenService;import com.wangzaiplus.test.util.JedisUtil;import com.wangzaiplus.test.util.RandomUtil;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.apache.commons.lang3.text.StrBuilder;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import javax.servlet.http.HttpServletRequest;@Servicepublic 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());        }    }}

6.TestApplication

package com.wangzaiplus.test;import com.wangzaiplus.test.interceptor.ApiIdempotentInterceptor;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.Bean;import org.springframework.web.cors.CorsConfiguration;import org.springframework.web.cors.UrlBasedCorsConfigurationSource;import org.springframework.web.filter.CorsFilter;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;@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, so far, the verification code is ready, then test and verify.

Seven, test verification

1. Get the token controller TokenController

package com.wangzaiplus.test.controller;import com.wangzaiplus.test.common.ServerResponse;import com.wangzaiplus.test.service.TokenService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/token")public class TokenController {
   
       @Autowired    private TokenService tokenService;    @GetMapping    public ServerResponse token() {
   
           return tokenService.createToken();    }}

2.TestController, pay attention to the @ApiIdempotent annotation, just declare this annotation on the method that requires idempotency verification, no need for verification

package com.wangzaiplus.test.controller;import com.wangzaiplus.test.annotation.ApiIdempotent;import com.wangzaiplus.test.common.ServerResponse;import com.wangzaiplus.test.service.TestService;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/test")@Slf4jpublic class TestController {
   
       @Autowired    private TestService testService;    @ApiIdempotent    @PostMapping("testIdempotence")    public ServerResponse testIdempotence() {
   
           return testService.testIdempotence();    }}

3. Get the token

 

View redis

 

4. Test interface security: Use the jmeter test tool to simulate 50 concurrent requests, and use the token obtained in the previous step as a parameter

 

 

5. Header or parameters do not pass the token, or the token value is empty, or the token value is filled in randomly, and the verification fails, such as the token value "abcd"

 

8. Points to note (very important)

 

In the above figure, you cannot simply delete the token directly without verifying whether the deletion is successful, there will be a concurrency safety problem, because it is possible that multiple threads go to line 46 at the same time, and the token has not been deleted at this time, so continue to If you do not verify the delete result of jedisUtil.del(token) and let it go directly, then there will still be a repeated submission problem, even if there is actually only one real delete operation, let's reproduce it below

Modify the code slightly:

 

Request again

Look at the console again

Although there is only one token that is actually deleted, since the deletion result is not verified, there is still a concurrency problem, so it must be verified

Nine, summary

In fact, the idea is very simple, that is to ensure uniqueness for each request, thereby ensuring idempotence. Through interceptor + annotation, you do not need to write repeated code for each request. In fact, you can also use spring aop to achieve it, it doesn't matter.

 

                                     ------------------------------------------- < END >---------------------------------------

The road of  wangzaiplus  code farmer code May 10

Source: jianshu.com/p/6189275403ed

 

 

 

Guess you like

Origin blog.csdn.net/qq_31653405/article/details/107204508