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:
-
Unique index – prevent new dirty data
-
token mechanism – prevent repeated submission of pages
-
Pessimistic lock – lock when data is obtained (lock table or lock row)
-
Optimistic lock – it is implemented based on the version number version and verifies the data at the moment of updating the data
-
Distributed lock – redis(jedis, redisson) or zookeeper implementation
wait
technological process:
- When the page is loaded, get the token (UUID) through the interface
- 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
- 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
- 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
- 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 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:
- Write custom annotations without parameters
- 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.