introduce
The concept of idempotency is that the impact of any multiple executions is the same as that of one execution. According to this meaning, the final explanation is that the impact on the database can only be one-time and cannot be processed repeatedly. The means are as follows
- Create a unique index of the database
- token mechanism
- Pessimistic lock or optimistic lock
- Query before judgment
I will mainly introduce you to Redis implementation Automatic idempotency . Its principle is shown in the figure below.
Implementation process
Introducing maven dependency
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
spring configuration file write
server.port=8080 core.datasource.druid.enabled=true core.datasource.druid.url=jdbc:mysql://192.168.1.225:3306/?useUnicode=true&characterEncoding=UTF-8 core.datasource.druid.username=root core.datasource.druid.password= core.redis.enabled=true spring.redis.host=192.168.1.225 #redis address of this machine spring.redis.port=16379 spring.redis.database=3 spring.redis.jedis.pool.max-active=10 spring.redis.jedis.pool.max-idle=10 spring.redis.jedis.pool.max-wait=5s spring.redis.jedis.pool.min-idle=10
Introducing Redis
The redis related stators in spring boot are introduced, and the RedisTemplate encapsulated in spring boot needs to be used later
package cn.smallmartial.demo.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; import java.io.Serializable; import java.util.Objects; import java.util.concurrent.TimeUnit; /** * @Author smallmartial * @Date 2020/4/16 * @Email smallmarital@qq.com */ @Component public class RedisUtil { @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 setting 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; } /** * 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 */ public boolean remove(final String key) { if (exists(key)) { Boolean delete = redisTemplate.delete(key); return delete; } return false; } /** * Determine whether the key exists * * @param key * @return */ public boolean exists(final String key) { boolean result = false; ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue(); if (Objects.nonNull(operations.get(key))) { result = true; } return result; } }
Custom annotation
Customize an annotation. The purpose of defining this annotation is to add it to the method that needs to implement idempotent. As long as a method annotates its, it will automatically implement idempotent operation. Its code is as follows
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface AutoIdempotent { }
Creation and implementation of token
Token service interface. We create a new interface to create a token service. There are mainly two methods, one for creating a token and the other for verifying a token
public interface TokenService { /** * Create token * @return */ public String createToken(); /** * Check token * @param request * @return */ public boolean checkToken(HttpServletRequest request) throws Exception; }
The implementation class of the token refers to the implementation class of the service. The token refers to the redis service. The token is created by using the random algorithm tool class to generate a random uuid string, and then put it into redis. If the putting is successful, the token is returned. The verification method is to obtain the value of the token from the header. If it does not exist, run the exception directly, This exception information can be directly intercepted and returned to the front end.
package cn.smallmartial.demo.service.impl; import cn.smallmartial.demo.bean.RedisKeyPrefix; import cn.smallmartial.demo.bean.ResponseCode; import cn.smallmartial.demo.exception.ApiResult; import cn.smallmartial.demo.exception.BusinessException; import cn.smallmartial.demo.service.TokenService; import cn.smallmartial.demo.utils.RedisUtil; import io.netty.util.internal.StringUtil; 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.Random; import java.util.UUID; /** * @Author smallmartial * @Date 2020/4/16 * @Email smallmarital@qq.com */ @Service public class TokenServiceImpl implements TokenService { @Autowired private RedisUtil redisService; /** * Create token * * @return */ @Override public String createToken() { String str = UUID.randomUUID().toString().replace("-", ""); StringBuilder token = new StringBuilder(); try { token.append(RedisKeyPrefix.TOKEN_PREFIX).append(str); redisService.setEx(token.toString(), token.toString(), 10000L); boolean empty = StringUtils.isEmpty(token.toString()); if (!empty) { return token.toString(); } } catch (Exception ex) { ex.printStackTrace(); } return null; } /** * Check token * * @param request * @return */ @Override public boolean checkToken(HttpServletRequest request) throws Exception { String token = request.getHeader(RedisKeyPrefix.TOKEN_NAME); if (StringUtils.isEmpty(token)) {// token does not exist in header token = request.getParameter(RedisKeyPrefix.TOKEN_NAME); if (StringUtils.isEmpty(token)) {// There is no token in the parameter throw new BusinessException(ApiResult.BADARGUMENT); } } if (!redisService.exists(token)) { throw new BusinessException(ApiResult.REPETITIVE_OPERATION); } boolean remove = redisService.remove(token); if (!remove) { throw new BusinessException(ApiResult.REPETITIVE_OPERATION); } return true; } }
Interceptor configuration
It is used to intercept the token of the front end and judge whether the token of the front end is valid
@Configuration public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Bean public AuthInterceptor authInterceptor() { return new AuthInterceptor(); } /** * Interceptor configuration * * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor()); // .addPathPatterns("/ksb/**") // .excludePathPatterns("/ksb/auth/**", "/api/common/**", "/error", "/api/*"); super.addInterceptors(registry); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**").addResourceLocations( "classpath:/static/"); registry.addResourceHandler("swagger-ui.html").addResourceLocations( "classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations( "classpath:/META-INF/resources/webjars/"); super.addResourceHandlers(registry); } }
Interceptor processor: mainly used to intercept scan to Autoldempotent to annotation method, and then call tokenService's checkToken method to check whether token is correct. If we catch the exception, render the abnormal information into json and return it to the front end. This part of the code is mainly linked to the user-defined annotation part. The main codes are as follows:
@Slf4j public class AuthInterceptor extends HandlerInterceptorAdapter { @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(); //Scan marked by apiidempotent AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class); if (methodAnnotation != null) { try { return tokenService.checkToken(request);// Idempotency verification. If the verification passes, it will be released. If the verification fails, an exception will be thrown, and a friendly prompt will be returned through unified exception handling } catch (Exception ex) { throw new BusinessException(ApiResult.REPETITIVE_OPERATION); } } return true; } }
test case
The related test cases are used to simulate the business request class, and the related token is obtained through the related paths. Then the testidempotence method is called. This method annotates @Autoldempotent, and the interceptor will intercept all requests. When it is judged that there is a note on the processing method, the checkToken() method in TokenService will be called. If there is an exception, it will run out, and the code is as follows
/** * @Author smallmartial * @Date 2020/4/16 * @Email smallmarital@qq.com */ @RestController public class BusinessController { @Autowired private TokenService tokenService; @GetMapping("/get/token") public Object getToken(){ String token = tokenService.createToken(); return ResponseUtil.ok(token) ; } @AutoIdempotent @GetMapping("/test/Idempotence") public Object testIdempotence() { String token = "Interface idempotency test"; return ResponseUtil.ok(token) ; } }
Access with browser
First access with the obtained token
Access again with the obtained token
You can see that the second access failed, that is, the idempotency verification passed.