preface

In a real development project, where an exposed interface often faces many requests, let’s explain the concept of idempotence: Any number of executions has the same impact as one execution. By this definition, the ultimate implication is that the impact on the database must be one-time and not repeated. How to ensure its idempotent, usually have the following means:

1, the database to establish a unique index, can ensure that the final insert database only one data.

2. Token mechanism: The interface obtains a token before each request, and then adds the token to the header body of the request in the next request for background verification. If the verification succeeds, the token is determined again in the next request.

3, pessimistic lock or optimistic lock, pessimistic lock can ensure that every time for update, other SQL cannot update data (innoDB engine,select condition must be unique index, prevent full table lock)

4, the first query after judgment, first by querying the database whether there is data, if there is proof has requested, directly reject the request, if there is no proof, it is the first time to come in, direct release.

Redis implements automatic idempotent:

Build Redis service API

1. First, set up redis server.

2, the introduction of Springboot to redis stater, or Spring packaged Jedis can also be used, the main API used in the following is its set method and exists method, here we use springboot packaged redisTemplate.

Spring Boot tutorial:

/** Java project fhadmin.cn redis utility class */ @Component public class RedisService {@autoWired private RedisTemplate redisTemplate; /** * 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; } /** * 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; } public Boolean exists(finalString key) {return;} public Boolean exists(finalString key) {return redisTemplate.hasKey(key); } /** * public Objectget(finalString key) {Object result = null; ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); result = operations.get(key); return result; } /** * delete value * @param key */ public Boolean remove(finalString key) {if (exists(key)) {Boolean delete = redisTemplate.delete(key); return delete; } returnfalse; }}Copy the code

Custom annotation AutoIdempotent

Define a custom annotation. The main purpose of defining this annotation is to add it to methods that need to implement idempotent. Any method that annotates it will implement automatic idempotent. If the annotation is scanned by reflection in the background, the METHOD is automatically idempotent, using the meta annotation elementType. METHOD to indicate that it can only be placed on the METHOD, and etentionPolicy.runtime to indicate that it is at RUNTIME.

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

}
Copy the code

Token creation and verification

Token service interface: We create a new interface to create the token service. There are mainly two methods, one for creating the token and the other for verifying the token. Creating a token is a string. Checking a token is a request object. Why pass a Request object? The main function is to get the token in the header, and then check, through the Exception thrown to get the specific error message back to the front end.

Public String createToken(); public String createToken(); public String createToken(); public String createToken(); /** * checkToken * @param request * @return */ public Boolean checkToken(HttpServletRequest request) throws Exception; }Copy the code

Token service implementation class: Token refers to the Redis service. The random algorithm tool class is used to generate a random UUID string to create the token, and then the token is put into redis (to prevent redundant data retention, the expiration time is set to 10000 seconds, depending on the service). If the token is successfully put into REDis, the token value is returned. The checkToken method simply fetches the token from the header to the value (if not from the header, then from the paramter) and throws an exception if it does not exist. This exception message can be caught by the interceptor and returned to the front end.

@Service publicclass TokenServiceImpl implements TokenService { @Autowired private RedisService redisService; @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; } /** * checkToken ** @param request * @return */ @override public Boolean checkToken(HttpServletRequest request) throws Exception { String token = request.getHeader(Constant.TOKEN_NAME); If (strutil. isBlank(token)) {// If (strutil. isBlank(token)) {// Token = request. If (StrUtil isBlank (token)) {/ / parameter does not exist in the token 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; }}Copy the code

Configuration of interceptors

Web Configuration class, realize WebMvcConfigurerAdapter, main effect is to add autoIdempotentInterceptor to the Configuration class, so the interceptor can we effect, pay attention to using the @ Configuration annotations, So you can add it to the context when the container starts.

@Configuration publicclass WebConfiguration extends WebMvcConfigurerAdapter { @Resource private AutoIdempotentInterceptor autoIdempotentInterceptor; Public void addInterceptors(InterceptorRegistry) { registry.addInterceptor(autoIdempotentInterceptor); super.addInterceptors(registry); }}Copy the code

Interception handler: the main function is to intercept the scanned AutoIdempotent annotations to the method, then call the checkToken() method of tokenService to verify whether the token is correct, if the exception is captured, render the exception information into JSON and return it to the front end.

/ * * fhadmin. Cn * interceptor * / @ Component publicclass AutoIdempotentInterceptor implements HandlerInterceptor {@autowired private TokenService tokenService; /** * Preprocessing ** @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(); AutoIdempotent Method.getannotation (AutoIdempotent. Class); // Scan AutoIdempotent method.getannotation (AutoIdempotent. if (methodAnnotation ! = null) { try { return tokenService.checkToken(request); // Check idempotent, pass the check, fail to throw an exception, } Catch (Exception ex){ResultVo failedResult = resultVo.getFailedResult (101, ex. GetMessage ());} Catch (Exception ex){ResultVo failedResult = ResultVo. writeReturnJson(response, JSONUtil.toJsonStr(failedResult)); throw ex; } // returntrue, otherwise all requests will be blocked; } @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 {} /** * The 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(); }}}Copy the code

The test case

To simulate the business request class, first we need to get the token through the /get/ Token path through the getToken() method to obtain the specific token, then we call the testIdempotence method annotated @Autoidempotent, the interceptor will intercept all the request. The checkToken() method in TokenService is called when the annotation is detected. If an exception is caught, it will throw the exception to the caller.

@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; }}Copy the code

With postman request, first access the get/token path to obtain the token specific:

The first request is successful, and then the second request is made:

The second request is returned as a repeat operation, so we can make the first request succeed. The second request will fail: