Wechat search “Java fish boy”, every day a knowledge not bad

One point a day

What is interface idempotence and how to implement it?

(1) Idempotence

Idempotent is originally a mathematical concept. When applied to an interface, it can be understood as: if the same interface sends the same request several times, the operation must be executed only once. An exception to the call interface and repeated attempts will always cause a loss that the system cannot afford, so it must be prevented. For example, if the interface idempotent is not implemented, it can have serious consequences: the payment interface, repeated payment will lead to multiple deduction of the order interface, the same order may be created multiple times.

(2) Idempotent solutions

Unique index The unique index can avoid the addition of dirty data. When repeated data is inserted, the database throws an exception, ensuring the uniqueness of data.

Optimistic lock The optimistic lock refers to the implementation of the principle of optimistic lock, add a version field to the data field, when the data needs to be updated, first go to the database to obtain the version number

select version from tablename where xxx
Copy the code

When updating the data, first compare it with the version number. If it is not equal, it indicates that there have been other requests to update the data, indicating that the update failed.

update tablename set count=count+1,version=version+1 where version=#{version}
Copy the code

Pessimistic lock What can be achieved with optimistic lock can also be achieved with pessimistic lock. When data is obtained, the lock is implemented. When there are multiple repeated requests at the same time, other requests cannot be operated

The idempotent nature of distributed lock is the problem of distributed lock, which can be realized by Redis or ZooKeeper. In a distributed environment, locking globally unique resources makes requests serialized, which actually acts as a mutex to prevent duplication and solve idempotentalities.

Token Mechanism The core idea of the token mechanism is to generate a unique credential, also known as a token, for each operation. A token has only one execution right at each stage of the operation. Once the execution is successful, the execution result is saved. Return the same result for repeated requests. The token mechanism is widely used.

(3) Implementation of token mechanism

Here is an example of how to implement interface idempotentiality through token mechanism: github first introduces the required dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
Copy the code

3.1. Configure the method body and enumeration class of the request

First configure the generic request return body

public class Response {
    private int status;
    private String msg;
    private Object data;
    // Omit get, set, toString, no-parameter and parameter constructors
}
Copy the code

And return code

public enum ResponseCode {
    // Generic module 1xxxx
    ILLEGAL_ARGUMENT(10000."Parameter is not valid"),
    REPETITIVE_OPERATION(10001."Do not repeat operation"),; ResponseCode(Integer code, String msg) {this.code = code;
        this.msg = msg;
    }
    private Integer code;
    private String msg;
    public Integer getCode(a) {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMsg(a) {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg; }}Copy the code

3.2 Customizing Exceptions and Configuring global Exception classes

public class ServiceException extends RuntimeException{
    private String code;
    private String msg;
    // Omit get, set, toString, and constructor
}
Copy the code

Configure the global exception catcher

@ControllerAdvice
public class MyControllerAdvice {
    @ResponseBody
    @ExceptionHandler(ServiceException.class)
    public Response serviceExceptionHandler(ServiceException exception){
        Response response=new Response(Integer.valueOf(exception.getCode()),exception.getMsg(),null);
        returnresponse; }}Copy the code

3.3 Write interfaces and implementation classes for creating and verifying tokens

@Service
public interface TokenService {
    public Response createToken(a);
    public Response checkToken(HttpServletRequest request);
}
Copy the code

The actual implementation class, the core business logic is written in the comments

@Service
public class TokenServiceImpl implements TokenService {

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public Response createToken(a) {
        // Generate a UUID as a token
        String token = UUID.randomUUID().toString().replaceAll("-"."");
        // Store the generated token in redis
        redisTemplate.opsForValue().set(token,token);
        // Return the correct result information
        Response response=new Response(0,token.toString(),null);
        return response;
    }

    @Override
    public Response checkToken(HttpServletRequest request) {
        // Obtain the token from the request header
        String token=request.getHeader("token");
        if (StringUtils.isBlank(token)){
            // If the request header token is null, get it from the parameter
            token=request.getParameter("token");
            // An error that throws an argument exception if both are null
            if (StringUtils.isBlank(token)){
                throw newServiceException(ResponseCode.ILLEGAL_ARGUMENT.getCode().toString(),ResponseCode.ILLEGAL_ARGUMENT.getMsg()); }}// If redis does not contain the token, the token has been removed, and the request is repeated
        if(! redisTemplate.hasKey(token)){throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg());
        }
        / / delete the token
        Boolean del=redisTemplate.delete(token);
        // If the deletion is unsuccessful (it has already been deleted by another request), throw a request duplicate exception
        if(! del){throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getCode().toString(),ResponseCode.REPETITIVE_OPERATION.getMsg());
        }
        return new Response(0."Verification successful".null); }}Copy the code

3.4 Configuring User-defined Annotations

This is an important step to implement token validation by adding a custom annotation to the methods that need to implement interface idempotentiality

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
Copy the code

Interface interceptor

public class ApiIdempotentInterceptor implements HandlerInterceptor {
    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(! (handlerinstanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod= (HandlerMethod) handler;
        Method method=handlerMethod.getMethod();
        ApiIdempotent methodAnnotation=method.getAnnotation(ApiIdempotent.class);
        if(methodAnnotation ! =null) {// If the verification passes the permit, if the verification does not pass the global exception, the output returns the result
            tokenService.checkToken(request);
        }
        return true;
    }

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

3.5 Configuring Interceptors and Redis

Configure webConfig and add interceptors

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiIdempotentInterceptor());
    }

    @Bean
    public ApiIdempotentInterceptor apiIdempotentInterceptor(a) {
        return newApiIdempotentInterceptor(); }}Copy the code

Configure Redis so that Chinese can be transmitted normally

@Configuration
public class RedisConfig {
    // A custom redistemplate
    @Bean(name = "redisTemplate")
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
        // Create a RedisTemplate Object to return key string and value Object for convenience
        RedisTemplate<String,Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        // Set the json serialization configuration
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new
                Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper=new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance);
        // Serialization of string
        StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
        // The key is serialized as a string
        template.setKeySerializer(stringRedisSerializer);
        // Value is serialized in Jackson mode
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // The hashkey is serialized as a string
        template.setHashKeySerializer(stringRedisSerializer);
        // HashValue uses Jackson's serialization mode
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        returntemplate; }}Copy the code

Finally, the controller

@RestController
@RequestMapping("/token")
public class TokenController {
    @Autowired
    private TokenService tokenService;

    @GetMapping
    public Response token(a){
        return tokenService.createToken();
    }

    @PostMapping("checktoken")
    public Response checktoken(HttpServletRequest request){
        returntokenService.checkToken(request); }}Copy the code

The rest of the code is available on github at the end of the article

(4) Verification of results

Firstly, create a token through the token interface. At this time, the token also exists in Redis

With 50 requests running simultaneously in JMeter, we can observe that only the first request is validated, and subsequent requests are prompted not to repeat operations.

Jmeter test file (Token Plan.jmx) and code from Github