幂等有很多方案,但是个人觉得这个最好用,来一个示例方案先
我们使用Redis
保存Token令牌,引入SpringBoot
,Redis
,ULID
相关的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId>
<version>5.2.0</version>
</dependency>
Redis相关配置
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0
spring.redis.timeout=60
server.port=8080
server.servlet.context-path=/coderacademy
Token令牌的生成
使用ULID生成随机串,存Redis.这里以idempotent_token+账户+请求操作类型+token作为key。
private StringRedisTemplate stringRedisTemplate;
/**
* 存入Redis的Token键的前缀
*/
private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:%s:$s:%s";
/**
* 生成token令牌
*
* @param accountSecret 账户令牌
* @param operatorType 接口请求类型,可以是接口url或者其他可以区分接口服务类型的值
* @return token令牌
*/
@Override
public String generateToken(String accountSecret, String operatorType) {
// 创建或获取ULID生成器实例
long timestampInMillis = LocalDateTime.now().atZone(ZoneOffset.systemDefault()).toInstant().toEpochMilli();
Ulid ulid = UlidCreator.getUlid(timestampInMillis);
String token = ulid.toString();
// 设置存入 Redis 的 Key
String key = String.format(IDEMPOTENT_TOKEN_PREFIX, accountSecret, operatorType, token);
// 存储 Token 到 Redis,且设置过期时间为5分钟
stringRedisTemplate.opsForValue().set(key, accountSecret, 5, TimeUnit.MINUTES);
// 返回 Token
return token;
}
校验Token
这里我们使用Redis执行Lua
命令去查找以及删除key,Lua 表达式能保证命令执行的原子性。
/**
* 验证 Token 正确性
*
* @param token token 字符串
* @param operatorType 接口请求类型,可以是接口url或者其他可以区分接口服务类型的值
* @return 验证结果
*/
private boolean validToken(String token, String accountSecret, String operatorType) {
// 设置 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 = String.format(IDEMPOTENT_TOKEN_PREFIX, accountSecret, operatorType, token);
// 执行 Lua 脚本
Long result = stringRedisTemplate.execute(redisScript, Arrays.asList(key, operatorType));
// 根据返回结果判断是否成功成功匹配并删除 Redis 键值对,若果结果不为空和0,则验证通过
if (result != null && result != 0L) {
System.out.println(String.format("验证 token=%s,key=%s,value=%s 成功", token, key, operatorType));
return true;
}
System.err.println(String.format("验证 token=%s,key=%s,value=%s 失败", token, key, operatorType));
return false;
}
业务代码以及接口
我们在实现模拟创建订单的服务,在创建订单之前,首先校验token令牌
/**
* 创建订单接口
*
* @param requestVO 创建订单参数
* @param accountSecret 账户令牌
* @param token token令牌
* @return 生成的订单号
*/
@Override
public String createOrder(OrderCreateRequestVO requestVO, String accountSecret, String token) {
// 根据 Token 和与用户相关的信息到 Redis 验证是否存在对应的信息
boolean result = validToken(token, accountSecret, "createOrder");
if (!result){
// 这里需要自定义异常,统一处理异常,再统一响应返回
throw new RuntimeException("重复的请求");
}
// 根据验证结果响应不同信息
return "Success";
}
校验如果不存在token,则说明请求时重复请求,直接抛出异常,由统一异常管理,直接返回客户端请求失败的错误信息。关于SpringBoot中统一异常处理,统一结果响应,请查看:
我们在定义获取Token令牌的接口,以及创建订单的接口
@RestController
@RequestMapping("order")
public class OrderController {
private IOrderService orderService;
/**
* 获取token接口
* @param secret 账户令牌
* @return
*/
@GetMapping("getToken")
public String getToken(@RequestHeader("secret") String secret){
return orderService.generateToken(secret, "createOrder");
}
/**
* 创建订单接口
* @param requestVO 参数
* @param token token令牌
* @param secret 账户令牌
* @return 响应信息
*/
@PostMapping("create")
public OrderCreateResponseVO createOrder(@RequestBody OrderCreateRequestVO requestVO,
@RequestHeader("token") String token,
@RequestHeader("secret") String secret){
OrderCreateResponseVO responseVO = new OrderCreateResponseVO();
String result = orderService.createOrder(requestVO, secret, token);
responseVO.setSuccess(Boolean.TRUE);
responseVO.setMsg(result);
return responseVO;
}
@Autowired
public void setOrderService(IOrderService orderService) {
this.orderService = orderService;
}
}
已有 0 条评论