SpringBoot Redis solves the problem of duplicate submission

Posted by SalientAnimal on Tue, 21 Dec 2021 14:09:37 +0100

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.

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.

Recommend a basic Spring Boot tutorial and practical example:

/**java Project fhadmin cn
 * redis Tool class
 */
@Component
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * Write cache
     * @param key
     * @param value
     * @return
     */
    public boolean set(finalString 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(finalString 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(finalString key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * Read cache
     * @param key
     * @return
     */
    public Objectget(finalString 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(finalString key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        returnfalse;

    }

}

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, this method will be processed to realize automatic idempotence and use the meta annotation ElementType Method means that it can only be placed on methods, etentionpolicy Runtime indicates that it is running.

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

publicinterface TokenService {

    /**java Project fhadmin cn
     * 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
publicclass TokenServiceImpl implements TokenService {

    @Autowired
    private RedisService redisService;

    /**
     * Create token
     * java fhadmin.cn
     * @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();
        }
        returnnull;
    }

    /**
     * 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
                thrownew ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
            }
        }

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

        boolean remove = redisService.remove(token);
        if (!remove) {
            thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
        }
        returntrue;
    }
}

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

/** fhadmin.cn
 * Interceptor
 */
@Component
publicclass 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)) {
            returntrue;
        }
        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 verification. If the verification passes, it will be released. If the verification fails, an exception will be thrown, and a friendly prompt will be returned 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
        returntrue;
    }

    @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
publicclass 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:

Topics: Java