Interface idempotency verification (use interceptor + custom annotation + redis to solve the problem)

Concept:

Idempotency, in popular terms, is an interface that initiates the same request multiple times. It must be ensured that the operation can only be executed once

For example:

  • Order interface, cannot create order more than once
  • Payment interface. You can only deduct money once for repeated payment of the same order
  • The callback interface of Alipay may be callback many times, and it must handle repeated callbacks.
  • The common form submission interface can only succeed once if you click to submit multiple times due to network timeout and other reasons

Wait

Common solutions are:

  1. Unique index – prevent new dirty data

  2. token mechanism – prevent repeated submission of pages

  3. Pessimistic lock – lock when data is obtained (lock table or lock row)

  4. Optimistic lock – it is implemented based on the version number version and verifies the data at the moment of updating the data

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

    wait

technological process:

  1. When the page is loaded, get the token (UUID) through the interface
  2. When accessing an interface, it will pass through the interceptor. If it is found that the interface has a custom idempotent check annotation, it indicates that the idempotency of the interface needs to be verified
  3. Check whether there is a value of key=token in the request header. If yes, and the deletion is successful, the interface will be accessed successfully. Otherwise, it is a duplicate submission
  4. If it is found that the interface does not have a custom idempotent check annotation, it will be released directly

code

Custom annotation MidengCheck

  • Customize an annotation. The main purpose of defining this annotation is to add it to the method that needs to realize idempotence. If a method annotates it, it will realize automatic idempotence.
  • If the annotation is scanned by reflection in the background, this method will be processed to realize automatic idempotence
  • Use meta annotation ElementType Method means that it can only be placed on methods, retentionpolicy Runtime indicates that it is running.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * User defined annotation as idempotent verification annotation of interface
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MidengCheck {
}

RedisService tool class of service layer redis service

/**
 * Redis Tool service 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
     * @param expireTime
     * @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 according to the key
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * Read cache according to key
     * @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 according to the key
     * @param key
     * @return
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;
    }
}

token creation and verification of TokenService

  1. 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.
  2. The checkToken method is to obtain the token value from the header. 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). If it does exist, Redis will be queried to see whether it exists. If it does not exist, an exception will be thrown. If Redis exists, the token verification passes.
/**
 * Get and verify Token
 */
@Component
public class TokenService {

    @Autowired
    private RedisService redisService;

    //Get token
    public String getToken() {
        String uuid = UUID.randomUUID().toString();
        //Add a uniform prefix string to the key stored in Redis
        String token = "mideng_check_prefix:" + uuid;
        //Deposit in Redis
        boolean result = redisService.setEx(token, uuid, 10000L);
        if(result){
            return token;
        }
        return null;
    }

    public boolean checkToken(HttpServletRequest request) throws Exception {
        //Get the value of token from the request header
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            //If there is no token in the request header, it is an illegal request and an exception is thrown directly
            throw new Exception("Illegal request");
        }
        if (!redisService.exists(token)) {
            //If a token exists in the request header but does not exist in Redis, an exception will also be thrown
            throw new Exception("token error");
        }
        //When the code is executed here, it indicates that the token verification is successful, so you need to delete the value in Redis
        boolean remove = redisService.remove(token);
        if (!remove) {
            //Deletion failed
            throw new Exception("token delete error");
        }
        return true;
    }
}

Interceptor configuration

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

import org.colin.interceptor.CheckIdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import javax.annotation.Resource;

/**
 * @ClassName: WebConfiguration
 * @description: Unified interceptor configuration class
 * @Version 1.1.0.1
 */
@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {

    @Resource
    private CheckIdempotentInterceptor checkIdempotentInterceptor;

    //Add interceptor
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //The checkidempoteninterceptor interceptor intercepts only / saveUser requests
        registry.addInterceptor(checkIdempotentInterceptor).addPathPatterns("/saveUser");
        super.addInterceptors(registry);
    }
}

Now we begin to write an intercept processor. The main function is to intercept the method that is annotated by CheckIdempotent. Then we call tokenService's checkToken() method to check if token is correct. If we catch the exception, we will return the abnormal information to the front end.

/**
 * Interceptor of idempotent check
 */
@Component
public class MidengCheckInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    /**
     * Preprocessing
     * This method will be called before the request is processed
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        //handler instanceof HandlerMethod: used to judge whether the request is a requested method (some requests are static resources of the request)
        if (!(handler instanceof HandlerMethod)) {
            //Direct release: a request to release a non method
            return true;
        }
        //When the code runs here, it indicates that the requested resource is the method
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        //handlerMethod.getMethod() gets the requested method object
        Method method = handlerMethod.getMethod();

        //Get the MidengCheck annotation above the request method
        MidengCheck methodAnnotation = method.getAnnotation(MidengCheck.class);
        if (methodAnnotation != null) {
            //Entering here indicates that the requested method is annotated with MidengCheck
            //At this point, I need to check the idempotency of the interface
            try {
                return tokenService.checkToken(request);
            }catch (Exception ex){
                writeJson(response, ex.getMessage());
                return false;
            }
        }
        //You must return true, otherwise all requests will be intercepted and the contents in the controller method will not be executed
        return true;
    }

    /**
     * Return prompt information to the front end
     */
    private void writeJson(HttpServletResponse response, String message){
        response.reset();
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=utf-8");
        response.setStatus(404);
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.print(message);
            outputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Startup class

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class);
    }
}

yml profile

server:
  port: 1010
spring:
  #redis configuration
  redis:
    #Which database to use (0-15)
    database: 0
    host: 127.0.0.1
    port: 6379
    #password: 123456 #password
    timeout: 5000

Class test

@RestController
@Slf4j
public class TestController {

    @Autowired
    private TokenService tokenService;

    //Get token value
    @GetMapping("/getToken")
    public String getToken(){
        return tokenService.getToken();
    }

    //Save user information
    @PostMapping("/saveUser")
    @MidengCheck
    public String saveUser(){
        //What we want is to print once
        log.info("----------------------------User information saved successfully----------------------------");
        //Business logic code for saving user information - omit
        return "add user success";
    }

}

test

1. Browser input http://localhost:1100/getToken Get token: mideng_check_prefix:7c29f136-9b39-4786-a15d-509428681364
2. Use Apache JMeter pressure measurement tool to request 100 times
Discovery will only succeed once

Summary:

  1. Write custom annotations without parameters
  2. Write interceptor, function: check the idempotency of the interface
  • It is not the request of the request method, and all are released
  • In these requests for request methods, you need to filter out the methods containing custom annotations, and release all the other methods
  • Get the token value in the request header, and then judge whether it is not empty and whether redis exists. Delete the redis value according to the token: if the deletion is successful, release it. If the deletion fails, throw an exception and do not release it.

Keywords: Java Redis

Added by Devious Designs on Sun, 13 Feb 2022 12:52:29 +0200