在分布式系统中,接口幂等性是一个非常重要的概念,它保证了在同样的条件下,同一请求的多次执行所产生的效果都是相同的。在实际开发中,为了防止重复提交或者重复操作带来的问题,我们需要考虑如何实现接口幂等性。
下面我将介绍如何在 SpringBoot + MySQL + MybatisPlus + Druid 的环境下实现接口幂等性。
什么是接口幂等性? 接口幂等性是指,对于相同的输入,接口的输出结果应该相同。换句话说,如果接口已经处理了一个请求并返回了结果,那么在相同的输入条件下,该接口的后续请求应该返回相同的结果,而不会产生任何新的副作用。
如何实现接口幂等性? 要实现接口幂等性,需要考虑以下几个方面:
请求唯一标识:每个请求都应该有一个唯一的标识,可以是请求参数的组合或者是一个单独的参数。 幂等性校验:每次请求到达服务器时,服务器需要判断该请求是否已经被处理过,如果已经被处理过,则直接返回处理结果,否则执行请求操作,并记录请求的唯一标识,以便后续的幂等性校验。 在 SpringBoot + MySQL + MybatisPlus + Druid 的环境下,我们可以通过以下方式实现接口幂等性:
在请求参数中添加一个幂等性校验码(比如 UUID),用于唯一标识每个请求。 在请求处理前,先查询幂等性校验码是否已经存在于数据库中,如果存在则说明该请求已经被处理过,直接返回结果。 如果幂等性校验码不存在于数据库中,则执行请求操作,并将幂等性校验码插入到数据库中。 下面是实现接口幂等性的示例代码:
在请求参数中添加一个幂等性校验码:
复制 public class RequestDTO {
private String idempotenceKey ;
// other request fields and methods
}
在 MybatisPlus 中创建对应的实体类:
复制 @Data
@TableName ("idempotence_key" )
public class IdempotenceKey {
@TableId (type = IdType .ASSIGN_UUID )
private String id ;
private String key ;
private Date createTime ;
}
在 Controller 中实现幂等性校验:
复制 @RestController
public class UserController {
@Autowired
private UserService userService ;
@PostMapping ("/user" )
public String createUser (@RequestBody RequestDTO request ) {
// 幂等性校验
if (checkIdempotence (request .getIdempotenceKey ())) {
return "success" ;
}
// 执行请求操作
userService .createUser (request );
// 插入幂等性校验码
saveIdempotence (request .getIdempotenceKey ());
return "success" ;
}
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 在 Service 中实现幂等性校验和插入幂等性校验码:
复制 @Service
public class UserService {
@Autowired
private IdempotenceKeyMapper idempotenceKeyMapper ;
public void createUser (RequestDTO request ) {
// 创建用户
// ...
}
private boolean checkIdempotence (String key ) {
IdempotenceKey idempotenceKey = idempotenceKeyMapper .selectOne (new LambdaQueryWrapper < IdempotenceKey > ().eq (IdempotenceKey ::getKey , key ));
return idempotenceKey != null ;
}
private void saveIdempotence (String key ) {
IdempotenceKey idempotenceKey = new IdempotenceKey ();
idempotenceKey .setKey (key );
idempotenceKey .setCreateTime (new Date ());
idempotenceKeyMapper .insert (idempotenceKey );
}
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 这里使用了 MybatisPlus 的 LambdaQueryWrapper 进行查询,并使用自动生成的 UUID 作为幂等性校验码。
全局实现幂等性校验可以使用AOP(面向切面编程)来实现,在方法执行前先进行幂等性校验,如果已经执行过该方法,则直接返回结果。可以通过自定义注解来标记需要进行幂等性校验的方法。
以下是一个简单的示例代码:
自定义注解 Idempotent: 复制 @Target (ElementType .METHOD )
@Retention (RetentionPolicy .RUNTIME )
public @interface Idempotent {
long expireSeconds () default 60 ;
}
编写 AOP 切面,用于拦截带有 @Idempotent 注解的方法: 复制 @Aspect
@Component
public class IdempotentAspect {
@Autowired
private IdempotenceKeyMapper idempotenceKeyMapper ;
@Pointcut ("@annotation(com.example.demo.annotation.Idempotent)" )
public void idempotentPointcut () {}
@Around ("idempotentPointcut()" )
public Object idempotentAround (ProceedingJoinPoint point ) throws Throwable {
MethodSignature signature = (MethodSignature ) point .getSignature ();
Method method = signature .getMethod ();
Idempotent idempotent = method .getAnnotation (Idempotent .class );
String key = getKey (point );
if (StringUtils .isBlank (key )) {
throw new RuntimeException ("幂等性校验码不能为空" );
}
if (checkIdempotence (key )) {
throw new RuntimeException ("请勿重复操作" );
}
saveIdempotence (key , idempotent .expireSeconds ());
return point .proceed ();
}
private boolean checkIdempotence (String key ) {
IdempotenceKey idempotenceKey = idempotenceKeyMapper .selectOne (new LambdaQueryWrapper < IdempotenceKey > ().eq (IdempotenceKey ::getKey , key ));
return idempotenceKey != null ;
}
private void saveIdempotence (String key , long expireSeconds ) {
IdempotenceKey idempotenceKey = new IdempotenceKey ();
idempotenceKey .setKey (key );
idempotenceKey .setCreateTime (new Date ());
idempotenceKey .setExpireTime (new Date (System .currentTimeMillis () + expireSeconds * 1000 ));
idempotenceKeyMapper .insert (idempotenceKey );
}
private String getKey (ProceedingJoinPoint point ) {
Object [] args = point .getArgs ();
if (args .length == 0 ) {
return null ;
}
return args [0 ].toString ();
}
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 在需要进行幂等性校验的方法上添加 @Idempotent 注解: 复制 @Service
public class UserService {
@Autowired
private IdempotenceKeyMapper idempotenceKeyMapper ;
@Idempotent (expireSeconds = 60 )
public void createUser (String username ) {
// 创建用户
// ...
}
}
通过以上方式,在方法执行前会先进行幂等性校验,如果已经执行过该方法,则直接返回结果,不会再次执行。
在实际应用中,需要考虑一些特殊情况的处理,以提高幂等性校验的准确性和可靠性。下面列举一些可能遇到的情况:
请求超时处理:由于幂等性校验码是有过期时间的,如果客户端发起的请求在幂等性校验码过期后才到达服务器,那么该请求就不应该再被视为重复请求。为了解决这个问题,可以在幂等性校验码表中记录请求的时间戳,并在校验幂等性校验码时进行时间戳比较,以判断请求是否超时。 在幂等性校验码表中添加一个请求时间戳的字段,将请求时间戳一并存储,以便在校验幂等性校验码时进行时间戳比较。
复制 CREATE TABLE `idempotent_key ` (
`id ` bigint (20 ) NOT NULL AUTO_INCREMENT COMMENT '主键ID' ,
`key ` varchar (128 ) NOT NULL COMMENT '幂等性校验码' ,
`create_time ` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ,
`expire_time ` datetime NOT NULL COMMENT '过期时间' ,
`request_time ` datetime NOT NULL COMMENT '请求时间' ,
PRIMARY KEY (`id ` ),
UNIQUE KEY `uk_key ` (`key ` )
) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT = '幂等性校验码表' ;
在进行幂等性校验时,需要先判断幂等性校验码是否过期,如果过期则不再进行校验。
复制 public void processRequest () {
String key = generateIdempotentKey ();
LocalDateTime now = LocalDateTime .now ();
LocalDateTime expireTime = now .plusMinutes (5 );
LocalDateTime requestTime = now ;
// 将幂等性校验码和请求时间戳存入数据库中
idempotentKeyDao .insert (key , expireTime , requestTime );
// 判断请求是否过期
LocalDateTime threshold = now .minusMinutes (5 );
if (requestTime .isBefore (threshold )) {
// 请求已经过期,不再进行幂等性校验
return ;
}
// 进行幂等性校验
boolean success = idempotentKeyDao .checkAndUpdate (key );
if (! success ) {
// 幂等性校验失败
return ;
}
// 执行业务操作
// ...
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 高并发下的幂等性校验:在高并发场景下,多个请求可能同时到达服务器进行幂等性校验,这时需要保证校验的准确性和唯一性。可以通过对幂等性校验码进行唯一索引的方式来保证每个幂等性校验码只会出现一次,避免多个请求同时通过校验。 在幂等性校验码表的 key 字段上添加唯一索引,以保证每个幂等性校验码只会出现一次。
复制 ALTER TABLE `idempotent_key` ADD UNIQUE INDEX `uk_key` (`key`);
在进行幂等性校验时,需要使用数据库的唯一索引进行校验。
复制 public boolean checkAndUpdate (String key ) {
// 利用数据库的唯一索引保证幂等性校验码的唯一性
int affectedRows = jdbcTemplate .update (
"UPDATE idempotent_key SET request_count = request_count + 1 WHERE key = ?" ,
key );
return affectedRows == 1 ;
}
幂等性校验码的重复利用:在一些场景下,比如一个请求执行失败需要重试,或者用户进行了一些撤销操作后需要再次执行该操作等,幂等性校验码可能会被多次使用。为了避免重复利用同一个幂等性校验码导致的校验失效,可以对幂等性校验码进行标记,标记该校验码已被使用过,避免再次使用。 在幂等性校验码表中添加一个 used 字段,标记该幂等性校验码是否已被使用过。
在进行幂等性校验时,需要判断该幂等性校验码是否已经被使用过,如果已经被使用过,则不再进行校验。
复制 public boolean checkAndUpdate (String key ) {
// 判断幂等性校验码是否已经被使用过
boolean used = jdbcTemplate .queryForObject (
"SELECT used FROM idempotent_key WHERE key = ?" ,
Boolean .class ,
key );
if (used ) {
// 幂等性校验码已经被使用过,不再进行校验
return true ;
}
// 将幂等性校验码标记为已使用
int affectedRows = jdbcTemplate .update (
"UPDATE idempotent_key SET used = true WHERE key = ?" ,
key );
return affectedRows == 1 ;
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 幂等性校验码的生成规则:幂等性校验码的生成规则也需要考虑,应该根据业务的特点来确定。可以采用随机数、UUID、请求参数哈希等方式生成幂等性校验码。需要保证幂等性校验码在相同的请求条件下生成的结果一致。 在分布式环境下,需要保证不同实例之间共享幂等性校验码的状态。可以使用 Redis 等分布式缓存来存储幂等性校验码状态。
复制 public boolean checkAndUpdate (String key ) {
// 从 Redis 中获取幂等性校验码的状态
boolean used = redisTemplate .opsForValue ().get (key );
if (used ) {
// 幂等性校验码已经被使用过,不再进行校验
return true ;
}
// 将幂等性校验码标记为已使用
redisTemplate .opsForValue ().set (key , true );
// 执行业务操作
// ...
return true ;
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 需要注意的是,由于 Redis 中存储的数据可能会被意外删除或过期,因此在使用 Redis 作为幂等性校验码状态存储介质时,需要考虑数据丢失或过期的情况,确保系统的可靠性和正确性。