SpringBoot environment uses Redis + AOP + custom annotations to achieve interface idempotence

I. Introduction

      Interface idempotence means that under the same conditions, multiple calls to an interface have the same effect as a single call. In short, no matter how many times an interface is called, the state of the system should remain consistent and should not produce different results due to multiple calls.
      In web development, especially in RESTful API design, idempotence is an important concept. Idempotent interfaces are easier to deal with when facing network instability, repeated message sending, or other abnormal situations, because they can ensure that multiple identical requests will not cause unexpected side effects.

2. Introduction to mainstream implementation solutions

2.1. Front-end button loading status restriction (required)

      For the front-end, the loading status must be added when processing the order submission button. If there is no response when calling the interface, clicks are not allowed again. If the server does not make an idempotent judgment, the user may quickly click the submit button multiple times. There are many orders that are the same, and even if the server makes an idempotent judgment so that the operation of multiple ordering interfaces can be quickly clicked and called, there is still a problem.

2.2. The client uses unique identifiers

      Each time a request is sent, the client generates a unique requestRequestId and places thisRequestId in the header or parameter of the request. When the server receives the request, it first verifies whether RequestId has been used. If it has already been used, it means that the request is repeated and the result will be returned directly. Otherwise, process the request and mark theRequestId as used.

2.3. The server performs idempotent verification by detecting request parameters (used in this article)

      This method does not require front-end cooperation. The specific implementation method is to obtain the request parameters of the interface, hash or md5 the request parameters, and then store the request parameters after the hash as a key in Redis and set an expiration time for each request. It will first determine whether the key exists in the cache. If it exists, it means repeated submission. This method is also the most commonly used. It will be implemented in combination with AOP + custom annotations. It is very flexible to use paging.

3. Code implementation

3.1

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--springboot中的redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- lettuce pool 缓存连接池-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <optional>true</optional>
        </dependency>
    </dependencies>

3.2、application.yml

server:
  port: 8000

spring:
  #redis配置信息
  redis:
    ## Redis数据库索引(默认为0
    database: 0
    ## Redis服务器地址
    host: 127.0.0.1
    ## Redis服务器连接端口
    port: 6379
    ## Redis服务器连接密码(默认为空)
    password: '123456'
    ## 连接超时时间(毫秒)
    timeout: 5000
    lettuce:
      pool:
        ## 连接池最大连接数(使用负值表示没有限制)
        max-active: 10
        ## 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
        ## 连接池中的最大空闲连接
        max-idle: 10
        ## 连接池中的最小空闲连接
        min-idle: 1

3.3. Redis configuration class

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig{
    
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    
    
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSeial.setObjectMapper(om);
        // 值采用json序列化
        template.setValueSerializer(jacksonSeial);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jacksonSeial);
        template.afterPropertiesSet();
        return template;
    }
}

3.4. Custom annotations

import java.lang.annotation.*;

/**
 * 自定义注解防止表单重复提交
 */
@Target(ElementType.METHOD) // 注解只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
@Documented
public @interface RepeatSubmitCheck {
    
    

    /**
     * 业务标识,不传默认ALL,便于区分业务
     */
    String key() default "ALL";

    /**
     * 防重复提交保持时间,默认1s
     */
    int keepSeconds() default 1;
}

3.5. AOP aspect implementation

import com.redisscene.annotation.RepeatSubmitCheck;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;

@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {
    
    

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private HttpServletRequest request;

    // 重复提交锁key
    private String RP_LOCK_RESTS = "RP_LOCK_RESTS:";

    @Pointcut("@annotation(com.redisscene.annotation.RepeatSubmitCheck)")
    public void requestPointcut() {
    
    
    }

    @Around("requestPointcut() && @annotation(repeatSubmitCheck)")
    public Object interceptor(ProceedingJoinPoint pjp, RepeatSubmitCheck repeatSubmitCheck) throws Throwable {
    
    
        final String lockKey = RP_LOCK_RESTS + repeatSubmitCheck.key() + ":" + generateKey(pjp);
        // 上锁 类似setnx,并且是原子性的设置过期时间
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "0", repeatSubmitCheck.keepSeconds(), TimeUnit.SECONDS);
        if (!lock) {
    
    
            // 这里也可以改为自己项目自定义的异常抛出 也可以直接return
//            throw new RuntimeException("重复提交");
            return "time="+ LocalDateTime.now() + " 重复提交";
        }
        return pjp.proceed();
    }

    private String generateKey(ProceedingJoinPoint pjp) {
    
    
        StringBuilder sb = new StringBuilder();
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        sb.append(pjp.getTarget().getClass().getName())//类名
                .append(method.getName());//方法名
        for (Object o : pjp.getArgs()) {
    
    
            if (o != null) {
    
    
                sb.append(o.toString());//参数
            }
        }
        String token = request.getHeader("token") == null ? "" : request.getHeader("token");
        sb.append(token);//token
        log.info("RP_LOCK generateKey() called with parameters => 【sb = {}】", sb);
        return DigestUtils.md5DigestAsHex(sb.toString().getBytes(Charset.defaultCharset()));
    }
}

3.6. Interface test idempotence effect

import com.redisscene.annotation.RepeatSubmitCheck;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
public class RepeatSubmitTestControlller {
    
    

    // curl -X GET -H "token: A001" "http://127.0.0.1:8000/t1?param1=nice&param2=hello"
    @RepeatSubmitCheck
    @GetMapping("/t1")
    public String t1(String param1,String param2){
    
    
        log.info("t1 param1={} param2={}",param1,param2);

        return "time="+LocalDateTime.now() + " t1";
    }
    // curl -X POST -H "token: A001" -H "Content-Type: application/json" -d "{'name':'kerwin'}" "http://127.0.0.1:8000/t2"
    @RepeatSubmitCheck(key = "T2",keepSeconds = 5)
    @PostMapping("/t2")
    public String t2(@RequestBody String body){
    
    
        log.info("t2 body={}",body);
        return "time="+LocalDateTime.now() + " t2";
    }
}

Two test interfaces are provided here. I will use curl to test here. You can execute it directly on the cmd command line, or you can use postman and other tools to test.

  • t1 method test

    curl -X GET -H "token: A001" "http://127.0.0.1:8000/t1?param1=nice&param2=hello"
    

    Insert image description here
    Here you can see that when the t1 interface is called twice, repeated submission will occur if it is called again within 1s. After 1s, the call can be successful again.

  • t2 method test

    curl -X POST -H "token: A001" -H "Content-Type: application/json" -d "{'name':'kerwin'}" "http://127.0.0.1:8000/t2"
    

    Insert image description here
    Here you can see that when the t2 interface is called twice, repeated submission will occur if it is called again within 5s. After 5s, the call can be successful again.

4. Summary

      Implementing interface idempotence through Redis + AOP + custom annotations is very flexible. Only add annotations to the interfaces that require idempotent judgment. This article only implements the core logic. For use in actual projects, only simple modifications are needed. That’s it.

Guess you like

Origin blog.csdn.net/weixin_44606481/article/details/134468637