SpringBoot + Redis solves the problem of massive repeated submission

Author Murong Qianyu

Source|   https://www.jianshu.com/p/c806003a8530

preface

In actual development projects, an exposed interface often faces many requests. Let's explain the concept of idempotence: the impact of any multiple execution is the same as that of one execution. According to this meaning, the final meaning is that the impact on the database can only be one-time and cannot be processed repeatedly. The following methods are usually used to ensure its idempotency:

1. Establishing a unique index of the database can ensure that only one piece of data is finally inserted into the database.

2. The token mechanism obtains a token before each interface request, and then adds the token to the request header body in the next request for background verification. If the verification passes, delete the token, and judge the token again in the next request.

3. Pessimistic lock or optimistic lock. Pessimistic lock can ensure that other sql cannot update data every time for update (when the database engine is innodb, the select condition must be a unique index to prevent locking all tables)

4. Query first and then judge. First, query whether there is data in the database. If there is evidence that the request has been made, the request is directly rejected. If there is no evidence, it is the first time to enter and release directly.

If you are learning Spring Boot, we recommend a free tutorial that has been continuously updated for many years: http://blog.didispace.com/spring-boot-learning-2x/

Schematic diagram of redis realizing automatic idempotent:

Build Redis service API

1. The first is to build a redis server.

2. The redis stators in springboot or the Spring encapsulated jedis can also be introduced. The APIs used later are its set method and exists method. Here we use the Spring boot encapsulated redisTemplate.

/** * redis Tool class  */
@Component
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    /** * Write cache  * @ param   key  * @ param   value  * @ return  */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }


    /** * Write cache set aging time  * @ param   key  * @ param   value  * @ return  */
    public boolean setEx(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }


    /** * Judge whether there is a corresponding value in the cache  * @ param   key  * @ return  */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    /** * Read cache  * @ param   key  * @ return  */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }

    /** * Delete the corresponding value  * @ param   key  */
    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;

    }

}

Custom annotation AutoIdempotent

Customize an annotation. The main purpose of defining this annotation is to add it to the method that needs to implement idempotence. If a method annotates it, it will implement automatic idempotence.

If the annotation is scanned by reflection in the background, the method will be processed to achieve automatic idempotence. The meta annotation ElementType.METHOD indicates that it can only be placed on the method, and etentionPolicy.RUNTIME indicates that it is in runtime.

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
  
}

token creation and verification

Token service interface: we create a new interface to create a token service. There are mainly two methods, one for creating a token and the other for verifying a token. Creating a token mainly produces a string. When checking a token, it mainly conveys the request object. Why should the request object be passed? The main function is to obtain the token in the header, then verify it, obtain the specific error information through the thrown Exception, and return it to the front end.

public interface TokenService {

    /** * Create token  * @ return  */
    public String createToken();

    /** * Check token  * @ param   request  * @ return  */
    public boolean checkToken(HttpServletRequest request) throws Exception;

}

Service implementation class of Token: the token refers to the redis service, creates a token, uses the random algorithm tool class to generate a random uuid string, and then puts it into redis (in order to prevent redundant retention of data, the expiration time is set to 10000 seconds, depending on the business). If the putting is successful, the token value is returned finally. The checkToken method is to obtain the token value from the header (if it cannot be obtained from the header, it can be obtained from the paramter). If it does not exist, an exception will be thrown directly. This exception information can be captured by the interceptor and then returned to the front end.

@Service
public class TokenServiceImpl implements TokenService {

    @Autowired
    private RedisService redisService;


    /** * Create token  * * @ return  */
    @Override
    public String createToken() {
        String str = RandomUtil.randomUUID();
        StrBuilder token = new StrBuilder();
        try {
            token.append(Constant.Redis.TOKEN_PREFIX).append(str);
            redisService.setEx(token.toString(), token.toString(),10000L);
            boolean notEmpty = StrUtil.isNotEmpty(token.toString());
            if (notEmpty) {
                return token.toString();
            }
        }catch (Exception ex){
            ex.printStackTrace();
        }
        return null;
    }


    /** * Check token  * * @ param   request  * @ return  */
    @Override
    public boolean checkToken(HttpServletRequest request) throws Exception {

        String token = request.getHeader(Constant.TOKEN_NAME);
        if (StrUtil.isBlank(token)) {//  token does not exist in header
            token = request.getParameter(Constant.TOKEN_NAME);
            if (StrUtil.isBlank(token)) {//  There is no token in the parameter
                throw new ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
            }
        }

        if (!redisService.exists(token)) {
            throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
        }

        boolean remove = redisService.remove(token);
        if (!remove) {
            throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
        }
        return true;
    }
}

Interceptor configuration

The web Configuration class implements WebMvcConfigurerAdapter. Its main function is to add autoidempoteninterceptor to the Configuration class, so that the interceptor can take effect. Pay attention to the @ Configuration annotation, so that it can be added to the context when the container is started.

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Resource
   private AutoIdempotentInterceptor autoIdempotentInterceptor;

    /** * Add interceptor  * @ param   registry  */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry);
    }
}

Interceptor processor: the main function is to intercept scan to AutoIdempotent to annotation to the method, then call the checkToken() method of tokenService to check if token is correct. If the exception is caught, render the abnormal information to json and return it to the front end.

If you are learning Spring Boot, we recommend a free tutorial that has been continuously updated for many years: http://blog.didispace.com/spring-boot-learning-2x/

/** * Interceptor  */
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    /** * Pretreatment  * * @ param   request  * @ param   response  * @ param   handler  * @ return  * @ throws   Exception  */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //Scan marked by apiidempotent
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {
            try {
                return tokenService.checkToken(request);//  Idempotency check,   If it passes the verification, it will be released,   If the verification fails, an exception is thrown,   And return friendly tips through unified exception handling
            }catch (Exception ex){
                ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
                writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
                throw ex;
            }
        }
        //Must return true, otherwise all requests will be intercepted
        return true;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }

    /** * Returned JSON value  * @ param   response  * @ param   json  * @ throws   Exception  */
    private void writeReturnJson(HttpServletResponse response, String json) throws Exception{
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try {
            writer = response.getWriter();
            writer.print(json);

        } catch (IOException e) {
        } finally {
            if (writer != null)
                writer.close();
        }
    }

}

test case

Simulate the business request class. First, we need to get the specific token through the / get/token path and the getToken() method, and then we call the testidempotent method. This method is annotated with @ AutoIdempotent. The interceptor will intercept all requests. When it is judged that there is such an annotation on the method handled everywhere, it will call the checkToken() method in TokenService, If an exception is caught, the exception will be thrown to the caller. Let's simulate the request:

@RestController
public class BusinessController {


    @Resource
    private TokenService tokenService;

    @Resource
    private TestService testService;


    @PostMapping("/get/token")
    public String  getToken(){
        String token = tokenService.createToken();
        if (StrUtil.isNotEmpty(token)) {
            ResultVo resultVo = new ResultVo();
            resultVo.setCode(Constant.code_success);
            resultVo.setMessage(Constant.SUCCESS);
            resultVo.setData(token);
            return JSONUtil.toJsonStr(resultVo);
        }
        return StrUtil.EMPTY;
    }


    @AutoIdempotent
    @PostMapping("/test/Idempotence")
    public String testIdempotence() {
        String businessResult = testService.testIdempotence();
        if (StrUtil.isNotEmpty(businessResult)) {
            ResultVo successResult = ResultVo.getSuccessResult(businessResult);
            return JSONUtil.toJsonStr(successResult);
        }
        return StrUtil.EMPTY;
    }
}

Using the postman request, first access the get/token path to get the specific token

By obtaining the token and putting it into the specific request header, we can see that the first request is successful, and then we request the second time:

For the second request, the return is a repetitive operation. It can be seen that the repeatability verification has passed. For more requests, we will only make them succeed for the first time and fail for the second time:

summary

This blog introduces how to use springboot, interceptor and redis to gracefully implement interface idempotence. It is very important for idempotence in the actual development process, because an interface may be called by countless clients. How to ensure that it does not affect the business processing of the background and how to ensure that it only affects the data once is very important, It can prevent the generation of dirty data or disordered data, and can also reduce the amount of concurrency, which is a very useful thing. The traditional method is to judge the data every time. This method is not intelligent and automatic, which is troublesome. Today's automated processing can also improve the scalability of programs

Click on the bottom card / WeChat search to pay attention to the official account of "ID:gh_cc865e4c536b" (Tian Yuwen).

Tianyuwen creative music school

Personal subscription number mainly provides a platform for sharing the latest information, IT tutorials and free novels

26 original content

official account

Keywords: Java Redis Spring Boot

Added by social_experiment on Fri, 22 Oct 2021 04:23:00 +0300