[SpringBoot] Spring Boot implements idempotence of interfaces

1. What is idempotence of interfaces?

In HTTP/1.1, idempotence is defined: one and multiple requests for a resource should have the same result for the resource itself (except for problems such as network timeout), that is, the first request has side effects on the resource. , but subsequent requests will no longer have side effects on the resource.

To put it simply: any multiple executions have the same impact on the resource itself as one execution.

2. Why do you need to implement idempotence of interfaces?

Under normal circumstances, when the interface is called, the information can be returned normally and will not be submitted repeatedly. However, problems may occur when encountering the following situations, such as:

  1. Repeated submission of forms on the front end: When filling in some forms, the user completes the submission. In many cases, due to network fluctuations, the user does not respond to the successful submission in time, causing the user to think that the submission was not successful, and then keeps clicking the submit button. This will happen. Submit the form request repeatedly.
  2. Users maliciously commit fraud: For example, when implementing a user voting function, if a user repeatedly submits a vote for a user, this will cause the interface to receive voting information repeatedly submitted by the user, which will cause the voting results to be seriously inconsistent with the facts.
  3. Interface timeout and repeated submission: In many cases, HTTP client tools enable the timeout retry mechanism by default, especially when a third party calls the interface. In order to prevent request failures caused by network fluctuations, timeouts, etc., a retry mechanism will be added, resulting in multiple submissions of one request. Second-rate.
  4. Repeated consumption of messages: When using MQ message middleware, if an error occurs in the message middleware and consumption information is not submitted in time, repeated consumption will occur.

The biggest advantage of using idempotence is that the interface guarantees any idempotent operation and avoids unknown problems caused by retries, etc. in the system.

3. How to achieve idempotence of interfaces

Solution: Anti-duplication Token

Program description:

For situations where the client continuously clicks or the caller times out and retries, such as submitting an order, the Token mechanism can be used to prevent repeated submissions.

To put it simply : when calling the interface, the caller first requests a global ID (Token) from the backend, and carries this global ID along with the request (it is best to put the Token in Headers). The backend needs to use this Token. As Key, the user information is sent to Redis as Value for key value content verification. If the Key exists and the Value matches, the delete command is executed, and then the subsequent business logic is executed normally. If there is no corresponding Key or Value does not match, a repeated error message will be returned to ensure idempotent operations.

Applicable operations:

  • insert operation
  • update operation
  • Delete operation

Main process:

Insert image description here

  • ① The server provides an interface for obtaining Token. The Token can be a serial number, a distributed ID or a UUID string.

  • ② The client calls the interface to obtain the Token. At this time, the server will generate a Token string.

  • ③ Then store the string in the Redis database, using the Token as the Redis key (note the expiration time).

  • ④ Return the Token to the client. After the client gets it, it should be stored in the hidden field of the form.

  • ⑤ When the client executes and submits the form, it stores the Token in the Headers and carries the Headers with it when executing the business request.

  • ⑥ After receiving the request, the server gets the Token from the Headers, and then searches Redis based on the Token to see whether the key exists.

  • ⑦ The server determines whether the key exists in Redis. If it exists, it deletes the key and then executes the business logic normally. If it does not exist, an exception will be thrown and an error message for repeated submissions will be returned.

Example: Anti-duplication Token

Create a SpringBoot project and introduce redis dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Token tool class:

@Slf4j
@Component
public class TokenUtil {
    
    

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 存入 Redis 的 Token 键的前缀
     */
    private static final String REPEAT_TOKEN_PREFIX = "repeat_token:";

    public String createToken(String value) {
    
    
        // 实例化生成 ID 工具对象
        String token = UUID.randomUUID().toString();
        // 设置存入 Redis 的 Key
        String key = REPEAT_TOKEN_PREFIX + token;
        // 存储 Token 到 Redis,且设置过期时间为5分钟
        redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
        // 返回 Token
        return token;
    }

    public boolean validToken(String token, String value) {
    
    
        // 设置 Lua 脚本,其中 KEYS[1] 是 key,KEYS[2] 是 value
        String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        // 根据 Key 前缀拼接 Key
        String key = REPEAT_TOKEN_PREFIX + token;
        // 执行 Lua 脚本
        Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value));
        // 根据返回结果判断是否成功成功匹配并删除 Redis 键值对,如果结果不为空和0,则验证通过
        if (result != null && result != 0L) {
    
    
            log.info("验证 token={},key={},value={} 成功", token, key, value);
            return true;
        }
        log.info("验证 token={},key={},value={} 失败", token, key, value);
        return false;
    }

}

Token controller:

@RestController
public class TokenController {
    
    

    @Autowired
    private TokenUtil tokenUtil;

    @GetMapping("/getToken")
    public String getToken(String userId) {
    
    
        return tokenUtil.createToken(userId);
    }

    @GetMapping("/test")
    public String test(@RequestHeader("token") String token, String userId) {
    
    
        boolean result = tokenUtil.validToken(token, userId);
        if (result) {
    
    
            // TODO 进行业务处理
            return "正常调用";
        }
        return "重复调用";
    }

}

Postman test

Insert image description here
Get token first

Insert image description here
Then make an interface call

4. Optimization

If there are 10 interfaces in a project that need to be verified for practical idempotence, then this code

boolean result = tokenUtil.validToken(token, userId);
 if (result) {
    
    
     // TODO 进行业务处理
     return "正常调用";
 }

Do I have to write it 10 times? ? ? Obviously violates the DRY principle.

Solution: Custom annotations + AOP

Guess you like

Origin blog.csdn.net/sco5282/article/details/127685117#comments_28497859