@RestController public class IdempotentController { @Autowired private RedisTemplate<String, String> redisTemplate; /** * 堆代码 duidaima.com * 提交接口,需要携带有效的token参数 */ @PostMapping("/submit") public String submit(@RequestParam("token") String token) { // 检查Token是否有效 if (!isValidToken(token)) { return "Invalid token"; } // 具体的接口处理逻辑,在这里实现你的业务逻辑 // 使用完毕后删除Token deleteToken(token); return "Success"; } /** * 检查Token是否有效 */ private boolean isValidToken(String token) { // 检查Token是否存在于Redis中 return redisTemplate.hasKey(token); } /** * 删除Token */ private void deleteToken(String token) { // 从Redis中删除Token redisTemplate.delete(token); } /** * 生成Token接口,用于获取一个唯一的Token */ @GetMapping("/generateToken") public String generateToken() { // 生成唯一的Token String token = UUID.randomUUID().toString(); // 将Token保存到Redis中,并设置过期时间(例如10分钟) redisTemplate.opsForValue().set(token, "true", Duration.ofMinutes(10)); return token; } }上述代码和前面描述的原理一致,但实际上存在问题,那就是在高并发场景下依然会有幂等性问题,这是因为没有充分利用redis的原子性。
@RestController public class IdempotentController { @Autowired private RedisTemplate<String, String> redisTemplate; /** * 提交接口,需要携带有效的token参数 */ @PostMapping("/submit") public String submit(@RequestParam("token") String token) { // 使用SETNX命令尝试将Token保存到Redis中,如果返回1表示设置成功,说明是第一次提交;否则返回0,表示重复提交 Boolean success = redisTemplate.opsForValue().setIfAbsent(token, "true", Duration.ofMinutes(10)); if (success == null || !success) { return "Duplicate submission"; } try { // 具体的接口处理逻辑,在这里实现你的业务逻辑 return "Success"; } finally { // 使用DEL命令删除Token redisTemplate.delete(token); } } }可以看到,我们使用了setIfAbsent方法来尝试将Token保存到Redis中,并设置过期时间(例如10分钟)。如果设置成功,则执行具体的接口处理逻辑,处理完成后会自动删除Token。如果设置失败,说明该Token已存在,即重复提交,直接返回错误信息。
通过使用Redis的原子性操作,我们可以更可靠地实现接口的幂等性,并在高并发情况下提供更好的性能和准确性。但是,在高并发场景下,这样其实依然有问题,依然有概率出现幂等性问题。这是因为,高并发场景下,可能会出现同时两个请求都从redis中获取到token,在服务端都能校验成功,最终破坏幂等性。
@RestController public class IdempotentController { @Autowired private RedisTemplate<String, String> redisTemplate; /** * 提交接口,需要携带有效的token参数 */ @PostMapping("/submit") public String submit(@RequestHeader("token") String token) { if (StringUtils.isBlank(token)) { return "Missing token"; } DefaultRedisScript<Boolean> script = new DefaultRedisScript<>(LUA_SCRIPT, Boolean.class); // 使用Lua脚本执行原子性操作 Boolean success = redisTemplate.execute(script, Collections.singletonList(token), "true", "600"); if (success == null || !success) { return "Duplicate submission"; } try { // 具体的接口处理逻辑,在这里实现你的业务逻辑 return "Success"; } finally { // 使用DEL命令删除Token redisTemplate.delete(token); } } /** * 生成Token接口,用于获取一个唯一的Token */ @GetMapping("/generateToken") public String generateToken() { // 生成唯一的Token String token = UUID.randomUUID().toString(); // 将Token保存到Redis中,并设置过期时间(例如10分钟) redisTemplate.opsForValue().set(token, "true", Duration.ofMinutes(10)); return token; } // Lua脚本 private final String LUA_SCRIPT = "if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then\n" + " redis.call('EXPIRE', KEYS[1], ARGV[2])\n" + " return true\n" + "else\n" + " return false\n" + "end"; }其中,这段Lua脚本的含义如下: