文章目录
引言
在分布式系统或微服务架构中,幂等性(Idempotency)是指对同一操作执行多次与执行一次的效果相同,不会因为重复执行而产生副作用。由于网络抖动、用户重复点击、系统重试等原因,同一请求可能被多次发送到后端处理,如果没有幂等保护,就可能导致数据重复、业务异常,严重影响系统稳定性和一致性。接下来我们将系统地介绍四种主流的幂等性实现方案,并对比它们的优缺点,帮助架构师和开发者根据业务需求进行选型。
幂等性实现方案概览
序号 | 实现方式 | 依赖组件 | 适用场景 |
---|---|---|---|
1 | Token 令牌 | Redis | 低并发、事务性强的操作 |
2 | 数据库唯一约束 | 关系型数据库 | 简单插入型操作 |
3 | 分布式锁 | Redis/Redisson/Zookeeper | 高并发、复杂业务 |
4 | 请求内容摘要(Digest) | Redis/数据库 | 参数化请求幂等 |
专家:软件工程 > 系统架构专家
要求:详细(VERBOSITY=2),提供重构后的代码示例,包含最佳实践和错误处理,语言为中文
计划
- Token 令牌方案——重构为工具类 + 更完善的异常处理
- Token AOP 注解方案——增加注解参数、统一响应
- 数据库唯一约束方案——引入全局异常处理
- 分布式锁方案(Redisson)——封装锁工具
- 请求摘要方案——提取公用工具、缓存结果示例
1. 基于 Token 令牌的幂等性实现
IdempotentTokenService(工具类)
@Service
public class IdempotentTokenService {
private static final String PREFIX = "idemp:token:";
private final StringRedisTemplate redis;
@Autowired
public IdempotentTokenService(StringRedisTemplate redisTemplate) {
this.redis = redisTemplate;
}
public String generateToken(Duration expire) {
String token = UUID.randomUUID().toString();
redis.opsForValue()
.set(PREFIX + token, "1", expire.toMillis(), TimeUnit.MILLISECONDS);
return token;
}
public void consumeToken(String token) {
String key = PREFIX + token;
Boolean exists = redis.hasKey(key);
if (!Boolean.TRUE.equals(exists)) {
throw new IdempotentException("令牌不存在或已过期");
}
if (!Boolean.TRUE.equals(redis.delete(key))) {
throw new IdempotentException("令牌已被使用");
}
}
}
Controller
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class OrderController {
private final IdempotentTokenService tokenService;
private final OrderService orderService;
@GetMapping("/token")
public Result<String> getToken() {
String token = tokenService.generateToken(Duration.ofMinutes(10));
return Result.success(token);
}
@PostMapping("/order")
public Result<Order> createOrder(
@RequestHeader("Idempotent-Token") String token,
@RequestBody @Valid OrderRequest req) {
tokenService.consumeToken(token);
Order order = orderService.createOrder(req);
return Result.success(order);
}
}
2. 基于 AOP 注解简化 Token 方案
自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IdempotentToken {
/**
* 过期时间,单位秒
*/
long expireSeconds() default 600;
}
TokenAspect
@Aspect @Component
@RequiredArgsConstructor
public class IdempotentTokenAspect {
private final IdempotentTokenService tokenService;
@Around("@annotation(tokenAnno)")
public Object around(ProceedingJoinPoint pjp, IdempotentToken tokenAnno) throws Throwable {
HttpServletRequest req = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String token = req.getHeader("Idempotent-Token");
if (StringUtils.isBlank(token)) {
throw new IdempotentException("幂等性 Token 不能为空");
}
tokenService.consumeToken(token);
return pjp.proceed();
}
}
Controller 使用
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping("/order")
@IdempotentToken(expireSeconds = 1800)
public Result<Order> createOrder(@RequestBody @Valid OrderRequest req) {
Order order = orderService.createOrder(req);
return Result.success(order);
}
}
3. 基于数据库唯一约束的幂等性实现
全局异常处理
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<Result<?>> handleConstraint(DataIntegrityViolationException ex) {
Throwable cause = ex.getMostSpecificCause();
if (cause instanceof ConstraintViolationException) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(Result.fail("操作已存在,无需重复提交"));
}
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(Result.fail("数据完整性错误"));
}
}
PaymentServiceImpl
@Service
@RequiredArgsConstructor
public class PaymentServiceImpl implements PaymentService {
private final PaymentRepository repo;
@Transactional
@Override
public PaymentResponse processPayment(PaymentRequest req) {
Payment pay = Payment.builder()
.orderNo(req.getOrderNo())
.transactionId(req.getTransactionId())
.amount(req.getAmount())
.status(PaymentStatus.PROCESSING)
.createTime(LocalDateTime.now())
.build();
repo.save(pay);
// 调用支付网关...
pay.setStatus(PaymentStatus.SUCCESS);
repo.save(pay);
return new PaymentResponse(true, "支付成功", pay.getId());
}
}
4. 基于分布式锁的幂等性实现(Redisson 简化)
分布式锁工具
@Component
@RequiredArgsConstructor
public class LockExecutor {
private final RedissonClient redisson;
public <T> T executeWithLock(String key, Duration wait, Duration lease, Supplier<T> supplier) {
RLock lock = redisson.getLock(key);
boolean acquired;
try {
acquired = lock.tryLock(wait.toMillis(), lease.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("获取分布式锁被中断");
}
if (!acquired) {
throw new BusinessException("请求正在处理中,请勿重复提交");
}
try {
return supplier.get();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
InventoryServiceImpl
@Service
@RequiredArgsConstructor
public class InventoryServiceImpl implements InventoryService {
private final LockExecutor lockExec;
private final InventoryRepository repo;
@Override
public DeductResponse deductInventory(DeductRequest req) {
String lockKey = "inv:lock:" + req.getRequestId();
return lockExec.executeWithLock(
lockKey,
Duration.ofSeconds(5),
Duration.ofSeconds(10),
() -> {
// 检查并执行库存扣减逻辑...
return new DeductResponse(true, "库存扣减成功", /* id */ 123L);
});
}
}
5. 基于请求内容摘要的幂等性实现
DigestUtils(工具类)
@Component
public class IdempotentDigestService {
private static final String PREFIX = "idemp:digest:";
private final StringRedisTemplate redis;
@Autowired
public IdempotentDigestService(StringRedisTemplate redisTemplate) {
this.redis = redisTemplate;
}
public String generateKey(Object req) {
String json = new ObjectMapper().writeValueAsString(req);
byte[] md5 = MessageDigest.getInstance("MD5")
.digest(json.getBytes(StandardCharsets.UTF_8));
return PREFIX + HexFormat.of().formatHex(md5);
}
public boolean tryMark(String key, long expireSeconds) {
return Boolean.TRUE.equals(redis.opsForValue()
.setIfAbsent(key, "processing", expireSeconds, TimeUnit.SECONDS));
}
public void remove(String key) {
redis.delete(key);
}
}
Controller 示例
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class TransferController {
private final IdempotentDigestService digestService;
private final TransferService transferService;
@PostMapping("/transfer")
public Result<TransferResult> transfer(@RequestBody TransferRequest req) throws JsonProcessingException {
String key = digestService.generateKey(req);
if (!digestService.tryMark(key, 3600)) {
return Result.fail("请求正在处理中,请勿重复提交");
}
try {
TransferResult res = transferService.executeTransfer(req, key);
return Result.success(res);
} catch (Exception e) {
digestService.remove(key);
return Result.fail("处理失败: " + e.getMessage());
}
}
}
优缺点对比
方案 | 优点 | 缺点 |
---|---|---|
Token | 简单易懂;AOP 侵入小;可预生成减少延迟 | 需额外两次请求;客户端复杂度提升;依赖 Redis |
数据库 | 无需额外组件;强一致性 | 异常开销大;高并发时性能瓶颈 |
分布式锁 | 适合高并发;可结合其他策略 | 实现复杂;依赖外部存储;锁释放需谨慎 |
摘要 | 通用性强;无客户端改动 | 摘要计算成本;参数顺序敏感 |
实践建议
- 低并发、事务性强:优先考虑数据库唯一约束或 Token 方案。
- 高并发、大流量:推荐分布式锁与摘要方案结合使用,以减少异常和锁竞争。
- 统一治理:通过自定义注解和 AOP,将幂等逻辑从业务层剥离,保持代码整洁。