In development, an exposed interface may face a large number of repeated requests in an instant. If you want to filter out repeated requests and cause damage to the business, you need to implement idempotent
Idempotent:
- Any number of executions has the same impact as a single execution. The final meaning is that the impact on the database can only be one-time and cannot be processed repeatedly.
Solution:
- Establishing a unique index of the database can ensure that only one piece of data is finally inserted into the database
- Token mechanism: obtain a token before each interface request, and then add the token in the header body of the next request for background verification. If the verification passes the deletion of the token, the next request judges the token again (used in this case)
- 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 a unique index to prevent locking the whole table)
- 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
1, Build Redis service
package com.ckw.idempotence.service; /** * @author ckw * @version 1.0 * @date 2020/6/11 9:42 * @description: redis Tools */ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.stereotype.Component; import java.io.Serializable; import java.util.concurrent.TimeUnit; /** * redis Tools */ @Component public class RedisService { private RedisTemplate redisTemplate; @Autowired(required = false) public void setRedisTemplate(RedisTemplate redisTemplate) { RedisSerializer stringSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringSerializer); redisTemplate.setValueSerializer(stringSerializer); redisTemplate.setHashKeySerializer(stringSerializer); redisTemplate.setHashValueSerializer(stringSerializer); this.redisTemplate = redisTemplate; } /** * Write cache * * @param key * @param value * @return */ public boolean set(final String key, Object value) { boolean result = false; try { ValueOperations 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; } /** * Determine 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 o = null; ValueOperations valueOperations = redisTemplate.opsForValue(); return valueOperations.get(key); } /** * Delete the corresponding value * @param key */ public Boolean remove(final String key) { if(exists(key)){ return redisTemplate.delete(key); } return false; } }
2, Custom annotation
Function: when interceptor intercepts the request, judge whether the Controller method corresponding to the called address has a user-defined annotation. If so, it indicates that the interface method is idempotent
package com.ckw.idempotence.annotion; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author ckw * @version 1.0 * @date 2020/6/11 9:55 * @description: */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface AutoIdempotent { }
3, Token creation and verification
package com.ckw.idempotence.service; import com.ckw.idempotence.exectionhandler.BaseException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; import java.util.UUID; /** * @author ckw * @version 1.0 * @date 2020/6/11 9:56 * @description: token service */ @Service public class TokenService { @Autowired RedisService redisService; //Create token public String createToken() { //Use UUID for token UUID uuid = UUID.randomUUID(); String token = uuid.toString(); //Deposit in redis boolean b = redisService.setEx(token, token, 10000L); return token; } //Verify whether there is a token in the request header or request parameter public boolean checkToken(HttpServletRequest request) { String token = request.getHeader("token"); //If the header is empty if(StringUtils.isEmpty(token)){ //Take it from the request token = request.getParameter("token"); if(StringUtils.isEmpty(token)){ throw new BaseException(20001, "Missing parameter token"); } } //If the token obtained from the header is incorrect if(!redisService.exists(token)){ throw new BaseException(20001, "Cannot submit repeatedly-------token Incorrect, empty"); } //Token remove token correctly if(!redisService.remove(token)){ throw new BaseException(20001, "token Remove failed"); } return true; } }
Here we use the custom exception and the custom response body as follows
Custom exception:
package com.ckw.idempotence.exectionhandler; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @author ckw * @version 1.0 * @date 2020/5/16 20:58 * @description: Custom exception class */ @Data @AllArgsConstructor @NoArgsConstructor public class BaseException extends RuntimeException { private Integer code; private String msg; }
Set unified exception handling:
package com.ckw.idempotence.exectionhandler; import com.ckw.idempotence.utils.R; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; /** * @author ckw * @version 1.0 * @date 2020/5/16 20:45 * @description: Unified exception handler */ @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) @ResponseBody public R error(Exception e){ e.printStackTrace(); return R.error(); } @ExceptionHandler(BaseException.class) @ResponseBody public R error(BaseException e){ e.printStackTrace(); return R.error().message(e.getMsg()).code(e.getCode()); } }
Custom response body:
package com.ckw.idempotence.utils; import lombok.Data; import java.util.HashMap; import java.util.Map; /** * @author ckw * @version 1.0 * @date 2020/5/16 18:35 * @description: Return results */ @Data public class R { private Boolean success; private Integer code; private String message; private Map<String, Object> data = new HashMap<String, Object>(); private R() { } //Encapsulation returned successfully public static R ok(){ R r = new R(); r.setSuccess(true); r.setCode(ResultCode.SUCCESS); r.setMessage("success"); return r; } //Encapsulation return failed public static R error(){ R r = new R(); r.setSuccess(false); r.setCode(ResultCode.ERROR); r.setMessage("fail"); return r; } public R success(Boolean success){ this.setSuccess(success); return this; } public R message(String message){ this.setMessage(message); return this; } public R code(Integer code){ this.setCode(code); return this; } public R data(String key, Object value){ this.data.put(key, value); return this; } public R data(Map<String, Object> map){ this.setData(map); return this; } }
Custom response code:
package com.ckw.idempotence.utils; import lombok.Data; import java.util.HashMap; import java.util.Map; /** * @author ckw * @version 1.0 * @date 2020/5/16 18:35 * @description: Return results */ @Data public class R { private Boolean success; private Integer code; private String message; private Map<String, Object> data = new HashMap<String, Object>(); private R() { } //Encapsulation returned successfully public static R ok(){ R r = new R(); r.setSuccess(true); r.setCode(ResultCode.SUCCESS); r.setMessage("success"); return r; } //Encapsulation return failed public static R error(){ R r = new R(); r.setSuccess(false); r.setCode(ResultCode.ERROR); r.setMessage("fail"); return r; } public R success(Boolean success){ this.setSuccess(success); return this; } public R message(String message){ this.setMessage(message); return this; } public R code(Integer code){ this.setCode(code); return this; } public R data(String key, Object value){ this.data.put(key, value); return this; } public R data(Map<String, Object> map){ this.setData(map); return this; } }
4, Interceptor configuration
1. Interceptor configuration class
package com.ckw.idempotence.config; import com.ckw.idempotence.interceptor.AutoIdempotentInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author ckw * @version 1.0 * @date 2020/6/11 10:07 * @description: Interceptor configuration class */ @Configuration public class WebConfiguration implements WebMvcConfigurer { @Autowired private AutoIdempotentInterceptor autoIdempotentInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(autoIdempotentInterceptor); } }
2. Interceptor class
package com.ckw.idempotence.interceptor; import com.ckw.idempotence.annotion.AutoIdempotent; import com.ckw.idempotence.service.TokenService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; /** * @author ckw * @version 1.0 * @date 2020/6/11 10:11 * @description: Block duplicate submission data */ @Component public class AutoIdempotentInterceptor implements HandlerInterceptor { @Autowired private TokenService tokenService; @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(); //Get the custom comment on the method AutoIdempotent annotation = method.getAnnotation(AutoIdempotent.class); //If it is not equal to null, the method needs idempotent if(null != annotation){ return tokenService.checkToken(request); } return true; } }
5, Normal Sevice class
package com.ckw.idempotence.service; import org.springframework.stereotype.Service; /** * @author ckw * @version 1.0 * @date 2020/6/11 10:04 * @description: */ @Service public class TestService { public String testMethod(){ return "Normal business logic"; } }
6, Controller class
package com.ckw.idempotence.controller; import com.ckw.idempotence.annotion.AutoIdempotent; import com.ckw.idempotence.service.TestService; import com.ckw.idempotence.service.TokenService; import com.ckw.idempotence.utils.R; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * @author ckw * @version 1.0 * @date 2020/6/11 9:58 * @description: */ @RestController @CrossOrigin @RequestMapping("/Idempotence") public class TestController { @Autowired private TokenService tokenService; @Autowired private TestService testService; @GetMapping("/getToken") public R getToken(){ String token = tokenService.createToken(); return R.ok().data("token",token); } //Equivalent to adding data interface (continuously click the add data button to see whether the result is to add one data or multiple data) @AutoIdempotent @PostMapping("/test/addData") public R addData(){ String s = testService.testMethod(); return R.ok().data("data",s); } }
7, Testing
First click:
Second click: