在分布式系统中,接口幂等性是一个非常重要的概念,它保证了在同样的条件下,同一请求的多次执行所产生的效果都是相同的。在实际开发中,为了防止重复提交或者重复操作带来的问题,我们需要考虑如何实现接口幂等性。
下面我将介绍如何在 SpringBoot + MySQL + MyBatisPlus + Druid 的环境下实现接口幂等性。
接口幂等性是指,对于相同的输入,接口的输出结果应该相同。换句话说,如果接口已经处理了一个请求并返回了结果,那么在相同的输入条件下,该接口的后续请求应该返回相同的结果,而不会产生任何新的副作用。
要实现接口幂等性,需要考虑以下几个方面:
在 SpringBoot + MySQL + MybatisPlus + Druid 的环境下,我们可以通过以下方式实现接口幂等性:
下面是实现接口幂等性的示例代码:
在请求参数中添加一个幂等性校验码:
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";
}
}
在 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);
}
}
这里使用了 MybatisPlus 的 LambdaQueryWrapper 进行查询,并使用自动生成的 UUID 作为幂等性校验码。
全局实现幂等性校验可以使用AOP(面向切面编程)来实现,在方法执行前先进行幂等性校验,如果已经执行过该方法,则直接返回结果。可以通过自定义注解来标记需要进行幂等性校验的方法。
以下是一个简单的示例代码:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
long expireSeconds() default 60;
}
@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();
}
}
@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;
}
// 执行业务操作
// ...
}
在幂等性校验码表的 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;
}
幂等性校验码的生成规则:幂等性校验码的生成规则也需要考虑,应该根据业务的特点来确定。可以采用随机数、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;
}
需要注意的是,由于 Redis 中存储的数据可能会被意外删除或过期,因此在使用 Redis 作为幂等性校验码状态存储介质时,需要考虑数据丢失或过期的情况,确保系统的可靠性和正确性。