每日一博 - 四种主流幂等方案优劣对比与选型指南

在这里插入图片描述

引言

在分布式系统或微服务架构中,幂等性(Idempotency)是指对同一操作执行多次与执行一次的效果相同,不会因为重复执行而产生副作用。由于网络抖动、用户重复点击、系统重试等原因,同一请求可能被多次发送到后端处理,如果没有幂等保护,就可能导致数据重复、业务异常,严重影响系统稳定性和一致性。接下来我们将系统地介绍四种主流的幂等性实现方案,并对比它们的优缺点,帮助架构师和开发者根据业务需求进行选型。


幂等性实现方案概览

序号实现方式依赖组件适用场景
1Token 令牌Redis低并发、事务性强的操作
2数据库唯一约束关系型数据库简单插入型操作
3分布式锁Redis/Redisson/Zookeeper高并发、复杂业务
4请求内容摘要(Digest)Redis/数据库参数化请求幂等

专家:软件工程 > 系统架构专家
要求:详细(VERBOSITY=2),提供重构后的代码示例,包含最佳实践和错误处理,语言为中文

计划

  1. Token 令牌方案——重构为工具类 + 更完善的异常处理
  2. Token AOP 注解方案——增加注解参数、统一响应
  3. 数据库唯一约束方案——引入全局异常处理
  4. 分布式锁方案(Redisson)——封装锁工具
  5. 请求摘要方案——提取公用工具、缓存结果示例

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,将幂等逻辑从业务层剥离,保持代码整洁。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值