java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot实现接口幂等

SpringBoot项目中实现接口幂等的代码详解

作者:IT橘子皮

在 Spring Boot 项目中实现接口幂等性是确保系统数据一致性和可靠性的关键手段,尤其在支付、订单等核心业务场景中,下面我将为你介绍几种常见的实现方案、选择建议以及一些注意事项,需要的朋友可以参考下

前言

在 Spring Boot 项目中实现接口幂等性是确保系统数据一致性和可靠性的关键手段,尤其在支付、订单等核心业务场景中。下面我将为你介绍几种常见的实现方案、选择建议以及一些注意事项。

方案核心机制实现复杂度性能影响外部依赖典型适用场景
Token 令牌预生成一次性令牌,使用后即焚Redis用户下单、支付
数据库唯一索引利用数据库唯一约束防止重复数据插入数据库数据插入操作,如支付记录创建
数据库乐观锁通过版本号控制更新,避免重复更新数据库更新操作,如库存扣减
分布式锁加锁确保同一业务标识的请求串行处理Redis/ZooKeeper高并发场景,如秒杀、抢券
请求摘要计算请求参数哈希值作为幂等键Redis参数固定的重复请求

1. Token 令牌机制

这种方式要求客户端在发起业务请求前,先获取一个服务器颁发的唯一令牌(Token),并在后续请求中携带该令牌。

实现步骤:

服务端提供获取 Token 的接口,生成一个唯一 Token(如 UUID)并存入 Redis,设置合理的过期时间。

客户端调用业务接口时,在 HTTP Header(如 Idempotent-Token)中携带此 Token。

服务端拦截请求,检查 Redis 中是否存在该 Token:

优点​:对业务代码侵入性相对较小,安全性较高,能有效防止重复提交。

缺点​:需额外一次获取 Token 的请求,依赖 Redis 等外部存储。

代码示意 (使用 AOP 简化)​​:

// 1. 自定义幂等注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    long timeout() default 10; // 过期时间,单位分钟
}

// 2. AOP 实现
@Aspect
@Component
public class IdempotentAspect {
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Around("@annotation(idempotent)")
    public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        String token = request.getHeader("Idempotent-Token");
        // ... 校验 Token 是否为空...
        String key = "idempotent:token:" + token;
        // 原子性验证并删除 Token
        Boolean isDeleted = redisTemplate.delete(key);
        if (Boolean.FALSE.equals(isDeleted)) {
            throw new BusinessException("请求已处理,请勿重复提交");
        }
        return joinPoint.proceed();
    }
}

// 3. 在控制器方法上使用注解
@PostMapping("/order")
@Idempotent(timeout = 10)
public Result createOrder(@RequestBody OrderRequest request) {
    // 业务逻辑
}

使用 AOP 可以使业务代码更简洁。

2. 数据库唯一约束

利用数据库本身的唯一性索引来保证重复请求不会插入多条记录。

实现步骤:

  1. 在数据库表中为能唯一标识业务的字段(如订单号 order_no、支付流水号 transaction_id)创建唯一索引。
  2. 执行业务逻辑插入数据时,如果重复插入,数据库会抛出 DataIntegrityViolationException等异常。
  3. 捕获该异常,并根据业务需求处理(如查询已存在的记录并返回)。

优点​:实现简单,可靠性高,利用数据库特性,强一致性。

缺点​:依赖于数据库,高并发时可能产生较多异常,需合理处理。

代码示意​:

@Service
public class PaymentService {
    @Autowired
    private PaymentRepository paymentRepository;

    @Transactional
    public PaymentResponse processPayment(PaymentRequest request) {
        try {
            Payment payment = new Payment();
            payment.setOrderNo(request.getOrderNo());
            payment.setTransactionId(request.getTransactionId()); // 此字段有唯一索引
            payment.setAmount(request.getAmount());
            paymentRepository.save(payment);
            // ...其他支付逻辑...
            return new PaymentResponse(true, "支付成功");
        } catch (DataIntegrityViolationException e) {
            // 捕获唯一约束违反异常
            Payment existingPayment = paymentRepository.findByTransactionId(request.getTransactionId());
            return new PaymentResponse(true, "支付已处理", existingPayment.getId());
        }
    }
}

通常需要结合 @Transactional注解保证事务性。

3. 数据库乐观锁

通过数据库的版本号字段实现,适用于更新操作。

实现步骤:

  1. 在数据库表中增加一个 version字段。
  2. 更新数据时,在 SQL 语句中同时更新版本号并校验旧版本号:update table set column = new_value, version = version + 1 where id = #{id} and version = #{oldVersion}
  3. 根据更新返回的影响行数判断是否更新成功。如果影响行数为 0,说明版本号不符或记录已被更新,可能是重复请求或数据冲突。

优点​:避免使用悲观锁的性能开销,适合读多写少的场景。

缺点​:如果并发冲突多,失败次数会增加。

代码示意 (MyBatis-Plus 示例)​​:

// 实体类
@Data
public class InventoryItemDO {
    private Long id;
    private Integer sellableQuantity;
    @Version // 乐观锁版本号字段注解
    private Integer version;
}

// Mapper 接口中使用
public interface InventoryItemMapper extends BaseMapper<InventoryItemDO> {
    // MyBatis-Plus 会自动在更新时带上版本条件
}

// Service 中调用 updateById 方法时,MyBatis-Plus 会自动处理版本号

4. 分布式锁

在分布式环境下,使用分布式锁(如基于 Redis)确保对同一业务键的操作是串行的。

实现步骤:

  1. 获取锁:在执行业务逻辑前,尝试获取一个分布式锁,锁的 Key 通常由业务标识生成(如 lock:order:1001)。
  2. 处理业务:获取锁成功后,先检查是否已处理过(可选,二次校验),再执行业务逻辑。
  3. 释放锁:最后释放分布式锁。

优点​:适用于分布式和高并发场景,能有效保证强一致性。

缺点​:实现相对复杂,依赖外部分布式锁服务(如 Redis),获取释放锁会增加响应时间。

代码示意 (使用 Redisson)​​:

@Service
public class SeckillService {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private OrderRepository orderRepository;

    public SeckillResponse seckill(SeckillRequest request) {
        String lockKey = "lock:seckill:" + request.getGoodsId();
        RLock lock = redissonClient.getLock(lockKey);
        try {
            if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { // 尝试获取锁,等待3秒,锁持有10秒
                // 二次校验:查询是否已下单,防止重复处理
                if (orderRepository.existsByUserIdAndGoodsId(request.getUserId(), request.getGoodsId())) {
                    return SeckillResponse.alreadyOrdered();
                }
                // 执行业务逻辑...
                return SeckillResponse.success();
            } else {
                throw new BusinessException("系统繁忙,请稍后再试");
            }
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

分布式锁常与唯一索引等其他幂等方案结合使用,形成双重保障。

5. 请求摘要(内容指纹)

将请求参数(如请求体)通过哈希算法(如 MD5、SHA-256)生成一个唯一摘要,以此作为幂等键。

实现步骤:

  1. 接收到请求后,计算请求参数的摘要(如对 JSON 请求体计算 MD5)。
  2. 以该摘要为 Key,尝试在 Redis 中执行 setIfAbsent(SETNX)操作。
  3. 如果设置成功,说明是首次请求,执行业务逻辑。
  4. 如果设置失败(Key 已存在),说明是重复请求,直接返回之前的处理结果或错误信息。

优点​:客户端无需额外操作,对用户透明。

缺点​:计算摘要有性能损耗,需确保计算摘要的参数能唯一标识业务操作,否则可能误判。

代码示意​:

@PostMapping("/transfer")
public Result transfer(@RequestBody TransferRequest request) {
    String idempotentKey = generateIdempotentKey(request); // 根据请求参数生成摘要
    String redisKey = "idempotent:digest:" + idempotentKey;
    // 原子性地设置键值,仅当键不存在时
    Boolean isFirstRequest = redisTemplate.opsForValue().setIfAbsent(redisKey, "processing", 24, TimeUnit.HOURS);
    if (Boolean.FALSE.equals(isFirstRequest)) {
        // 重复请求,可根据业务查询之前的结果或直接返回错误
        return Result.fail("请勿重复提交");
    }
    try {
        // 执行业务逻辑...
        return Result.success();
    } finally {
        // 可选:业务成功完成后,可以更新Redis中的状态;若失败,可删除键允许重试
        // redisTemplate.delete(redisKey);
    }
}

此方法的关键在于生成幂等键的算法要能准确识别重复请求。

如何选择幂等方案?

选择哪种方案取决于你的具体业务场景、性能要求和系统架构:

注意事项

  1. 幂等键的生成与传递​:幂等键(Token、业务ID等)需要保证全局唯一性。常见的生成方式有 UUID、雪花算法(Snowflake)等。同时,需要和调用方约定好传递方式,如放在 HTTP Header(如 Idempotency-Key)中。
  2. 异常处理与重试​:设计幂等时要考虑业务失败的情况。例如,在 Token 机制中,如果业务执行失败,可能需要归还 Token​ 允许客户端重试。在请求摘要模式中,也需考虑失败后是否删除 Redis 中的键。
  3. 幂等与并发的区别​:幂等主要解决重复请求问题(可能非并发),而并发控制解决同时操作的问题。两者常结合使用,例如用分布式锁处理并发,用唯一索引保证最终幂等。
  4. HTTP 方法的幂等性​:了解 RESTful API 中不同 HTTP 方法的幂等性语义对你设计接口有帮助(例如,GET、PUT、DELETE 通常是幂等的,而 POST 不是)。

总结

实现接口幂等性是构建健壮分布式系统的关键。你可以根据实际业务场景选择最合适的方案,很多时候这些方案会组合使用以达到最佳效果。

以上就是SpringBoot项目中实现接口幂等的代码详解的详细内容,更多关于SpringBoot实现接口幂等的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文