前言
针对客户端连续点击或者调用方的超时重试等情况,例如提交订单,此种操作就可以用 Token
的机制实现防止重复提交。
简单的说就是调用方在调用接口的时候先向后端请求一个全局 ID(Token),请求的时候携带这个全局 ID 一起请求(Token 最好将其放到 Headers 中),后端需要对这个 Token 作为 Key,用户信息作为 Value 到 Redis 中进行键值内容校验,如果 Key 存在且 Value 匹配就执行删除命令,然后正常执行后面的业务逻辑。如果不存在对应的 Key 或 Value 不匹配就返回重复执行的错误信息,这样来保证幂等操作。
实现
pom.xml
在 pom.xml 中引入 Redis 依赖:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-pool2</artifactId>
- </dependency>
application.yaml
在 application 配置文件中配置连接 Redis 的参数:
- spring:
- application:
- name: 郎家岭伯爵
- redis:
- ssl: false
- host: 127.0.0.1
- port: 6379
- database: 0
- timeout: 1000
- password:
- lettuce:
- pool:
- max-active: 100
- max-wait: -1
- min-idle: 0
- max-idle: 20
创建与验证Token工具类
创建用于操作 Token 相关的 Service 类,里面存在 Token 创建与验证方法,其中:
-
Token 创建方法: 使用 UUID 工具创建 Token 串,设置以
"idempotent_token:" + "Token串"
作为Key
,以用户信息当成Value
,将信息存入 Redis 中。 -
Token 验证方法: 接收 Token 串参数,加上 Key 前缀形成 Key,再传入 Value 值,执行
Lua
表达式(Lua 表达式能保证命令执行的原子性)进行查找对应 Key 与删除操作。执行完成后验证命令的返回结果,如果结果不为空且非0,则验证成功,否则失败。
- package com.langjialing.helloworld.service;
- import java.util.Arrays;
- import java.util.UUID;
- import java.util.concurrent.TimeUnit;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.core.script.DefaultRedisScript;
- import org.springframework.data.redis.core.script.RedisScript;
- import org.springframework.stereotype.Service;
- /**
- * @author 郎家岭伯爵
- * @time 2023/3/22 14:15
- */
- @Slf4j
- @Service
- public class TokenUtilService {
- @Autowired
- private StringRedisTemplate redisTemplate;
- /**
- * 存入 Redis 的 Token 键的前缀
- */
- private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";
- /**
- * 创建 Token 存入 Redis,并返回该 Token
- *
- * @param value 用于辅助验证的 value 值
- * @return 生成的 Token 串
- */
- public String generateToken(String value) {
- // 实例化生成 ID 工具对象
- String token = UUID.randomUUID().toString();
- // 设置存入 Redis 的 Key
- String key = IDEMPOTENT_TOKEN_PREFIX + token;
- // 存储 Token 到 Redis,且设置过期时间为5分钟
- redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
- // 返回 Token
- return token;
- }
- /**
- * 验证 Token 正确性
- *
- * @param token token 字符串
- * @param value value 存储在Redis中的辅助验证信息
- * @return 验证结果
- */
- 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 = IDEMPOTENT_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;
- }
- }
创建测试的Controller类
创建用于测试的 Controller 类,里面有获取 Token 与测试接口幂等性的接口,内容如下:
- package com.langjialing.helloworld.controller;
- import com.langjialing.helloworld.service.TokenUtilService;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.*;
- /**
- * @author 郎家岭伯爵
- * @time 2023/3/22 14:17
- */
- @Slf4j
- @RestController
- public class TokenController {
- @Autowired
- private TokenUtilService tokenService;
- /**
- * 获取 Token 接口
- *
- * @return Token 串
- */
- @GetMapping("/token")
- public String getToken(@RequestParam String userId) {
- // 获取 Token 字符串,并返回
- return tokenService.generateToken(userId);
- }
- /**
- * 接口幂等性测试接口
- *
- * @param token 幂等 Token 串
- * @return 执行结果
- */
- @PostMapping("/test")
- public String test(@RequestHeader(value = "token") String token, @RequestParam String userId) {
- // 根据 Token 和与用户相关的信息到 Redis 验证是否存在对应的信息
- boolean result = tokenService.validToken(token, userId);
- // 根据验证结果响应不同信息
- return result ? "正常调用" : "重复调用或无效的userId";
- }
- }
POSTMAN测试
调用 /token"
接口,创建指定用户的 Token 值:
调用 /test"
接口,通过验证 Headers 里的 Token 值是否仍存在 Redis 中(如存在则为第一次调用接口,业务代码正常执行;如不存在则为重复调用,拦截执行):
第一次调用:
重复调用:
总结
本文通过 Token 令牌的方案来解决接口幂等性问题。