-
In a real development project, an exposed interface is often faced with a large number of repeated requests submitted in an instant. If you want to filter out the repeated requests and cause damage to the business, you need to implement idempotent!
-
Let's explain the concept of idempotent:
Any number of executions has the same impact as a single 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.
- How to ensure its idempotence, there are usually the following means:
1. Establishing a unique index of the database can ensure that only one piece of data is finally inserted into the database
2. Token mechanism: obtain a token before each interface request, and then add the token in the header body of the request at the next request for background verification. If the verification passes deleting the token, the next request will judge the token again
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 condition of select must be unique index to prevent locking the whole table)
4. First, query and then judge. First, query whether there is data in the database. If the existence certificate has been requested, directly reject the request. If it does not exist, it is the first time to enter and directly release.
Implementation steps
1, Set up the service Api of redis
1. First, build a redis server.
2. It is also possible to introduce the redis state or Spring packaged jedis from springboot. The main api used later is its set method and exists method. Here we use the Spring boot packaged redisTemplate
/** * redis Tool class */ @Component publicclass RedisService { @Autowired private RedisTemplate redisTemplate; /** * Write cache * @param key * @param value * @return */ publicbooleanset ( 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 */ publicboolean 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; } /** * Determine whether there is a corresponding value in the cache * @param key * @return */ publicboolean 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 */ publicboolean remove( final String key) { if (exists(key)) { Boolean delete = redisTemplate. delete (key); returndelete ; } returnfalse ; } }
2, Custom annotation AutoIdempotent
Customize an annotation. The main purpose of defining this annotation is to add it to the method that needs to implement idempotent. If a method annotates it, it will implement automatic idempotent. If the annotation is scanned by reflection in the background, the method will be processed to realize automatic idempotence. The meta annotation ElementType.METHOD indicates that it can only be placed on the method, and etentionPolicy.RUNTIME indicates that it is running.
@Target ({ ElementType .METHOD}) @Retention ( RetentionPolicy .RUNTIME) public @interface AutoIdempotent { }
3, token creation and verification
1. token service interface
We create a new interface to create a token service. There are two main methods, one is used to create a token and the other is used to verify the token. Creating a token mainly produces a string. When checking a token, it mainly conveys the request object. Why do you want to pass the request object? The main function is to obtain the token in the header, and then check it. The specific error reporting information will be obtained and returned to the front end through the Exception thrown.
publicinterface TokenService { /** * Create token * @return */ public String createToken(); /** * Test token * @param request * @return */ publicboolean checkToken( HttpServletRequest request) throws Exception ; }
2. Service implementation class of token
The token refers to the redis service, creates a token, generates a random uuid string using a random algorithm tool class, and then puts it into redis (in order to prevent redundant retention of data, the expiration time is set to 10000 seconds here, depending on the business). If the token is put in successfully, the value of the token will be returned at last. The checkToken method is to get the token from the header to the value (if the header can't get it, get it from paramter). If it doesn't exist, throw an exception 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 * * @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 ; } /** * Test token * * @param request * @return */ @Override publicboolean 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)) { // token does not exist in 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 ; } }
4, Interceptor configuration
1. The web Configuration class implements the WebMvcConfigurerAdapter. Its main function is to add the autoidemponentinterceptor to the Configuration class, so that we can enter the interceptor. Pay attention to the @ Configuration annotation, so that it can be added into the context when the container is started.
@Configuration publicclass WebConfiguration extends WebMvcConfigurerAdapter { @Resource private AutoIdempotentInterceptor autoIdempotentInterceptor; /** * Add interceptor * @param registry */ @Override publicvoid addInterceptors( InterceptorRegistry registry) { registry.addInterceptor(autoIdempotentInterceptor); super .addInterceptors(registry); } }
2, 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.
/** * Interceptor */ @Component publicclass AutoIdempotentInterceptor implements HandlerInterceptor { @Autowired private TokenService tokenService; /** * Preprocessing * * @param request * @param response * @param handler * @return * @throws Exception */ @Override publicboolean 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 ApiIdempotment AutoIdempotent methodAnnotation = method.getAnnotation( AutoIdempotent . class ); if (methodAnnotation != null ) { try { return tokenService.checkToken(request); // Idempotent check: if the check is passed, it will be released; if the check 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 blocked returntrue ; } @Override publicvoid postHandle( HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override publicvoid afterCompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } /** * json value returned * @param response * @param json * @throws Exception */ privatevoid 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(); } } }
5, Test case
1. Simulate business request class
First of all, we need to get the specific token through the / get/token path through the getToken() method, and then we call the testIdempotence method, which annotates @ AutoIdempotent. The interceptor will intercept all requests. When it is judged that there is such annotation on the processed method, it will call the checkToken() method in the TokenService. If it catches an exception, it will be different The caller is often thrown. Let's simulate the request as follows:
@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; } }
2. Using postman requests
First, access the get/token path to get the specific token: Get the token and put it in the specific request to the header. You can see that the first request is successful. Then we request the second time: The second request is a repetitive operation. It can be seen that the repeatability verification is passed. When the second request is repeated, we will only let it succeed for the first time. The second request is a failure:
Six, summary
This blog introduces how to use springboot, interceptor and redis to implement interface idempotent gracefully, which is very important in the actual development process, because an interface may be called by countless clients, how to ensure that it does not affect the background business processing, how to ensure that it only affects the data once is very important, it can prevent the generation of dirty data or Random data 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 automation can also improve program scalability.