开发者

SpringBoot中4种接口幂等性的实现策略

目录
  • 1. 基于Token令牌的幂等性实现
    • 实现步骤
    • 代码实现
    • 通过AOP简化实现
    • 优缺点分析
  • 2. 基于数据库唯一约束的幂等性实现
    • 实现方式
    • 代码实现
    • 优缺点分析
  • 3. 基于分布式锁的幂等性实现
    • 实现方式
    • 基于Redis的分布式锁实现
    • 使用Redisson简化实现
    • 优缺点分析
  • 4. 基于请求内容摘要的幂等性实现
    • 实现方式
    • 代码实现
    • 使用自定义注解简化实现
    • 优缺点分析
  • 总结

    幂等性是指对同一操作执行多次与执行一次的效果相同,不会因为重复执行而产生副作用。在实际应用中,由于网络延迟、用户重复点击提交、系统自动重试等原因,可能导致同一请求被多次发送到服务端处理,如果没有实现幂等性,就可能导致数据重复、业务异常等问题。

    1. 基于Token令牌的幂等性实现

    Token令牌策略是最常见的幂等性实现方式之一,其核心思想是在执行业务操作前先获取一个唯一token,然后在调用接口时将其随请求一起提交,服务端校验并销毁token,确保其只被使用一次。

    实现步骤

    • 客户端先调用获取token接口
    • 服务端生成唯一token并存入Redis,设置过期时间
    • 客户端调用业务接口时附带token参数
    • 服务端验证token存在性并删除,防止重复使用

    代码实现

    @RestController
    @RequestMapping("/api")
    public class OrderController {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
        
        @Autowired
        private OrderService orderService;
        
        // 获取token接口
        @GetMapping("/token")
        public Result<String> getToken() {
            // 生成唯一token
            String token = UUID.randomUUID().toString();
            // 存入Redis并设置过期时间
            redisTemplate.opsForValue().set("idempotent:token:" + token, "1", 10, TimeUnit.MINUTES);
       http://www.devze.com     return Result.success(token);
        }
        
        // 创建订单接口
        @PostMapping("/order")
        public Result<Order> createOrder(@RequestHeader("Idempotent-Token") String token, @RequestBody OrderRequest request) {
            // 检查token是否存在
            String key = "idempotent:token:" + token;
            Boolean exist = redisTemplate.hasKey(key);
            if (exist == null || !exist) {
                return Result.fail("令牌不存在或已过期");
            }
            
            // 删除token,保证幂等性
            if (Boolean.FALSE.equals(redisTemplate.delete(key))) {
                return Result.fail("令牌已被使用");
            }
            
            // 执行业务逻辑
            Order order = orderService.createOrder(request);
            return Result.success(order);
        }
    }
    

    通过AOP简化实现

    可以通过自定义注解和AOP进一步简化幂等性实现:

    // 自定义幂等性注解
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Idempotent {
        long timeout() default 10; // 过期时间,单位分钟
    }
    
    // AOP实现
    @ASPect
    @Component
    public class IdempotentAspect {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
        
        @Around("@annotation(idempotent)")
        public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
            // 获取请求头中的token
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String token = request.getHeader("Idempotent-Token");
            
            if (StringUtils.isEmpty(token)) {
                throw new BusinessException("幂等性Token不能为空");
            }
            
            String key = "idempotent:token:" + token;
            Boolean exist = redisTemplate.hasKey(key);
            
            if (exist == null || !exist) {
                throw new BusinessException("令牌不存在或已过期");
            }
            
            // 删除token,保证幂等性
            if (Boolean.FALSE.equals(redisTemplate.delete(key))) {
                throw new BusinessException("令牌已被使用");
            }
            
            // 执行目标方法
            return joinPoint.proceed();
        }
    }
    
    // 控制器使用注解
    @RestController
    @RequestMapping("/api")
    public class OrderController {
    
        @Autowired
        private OrderService orderService;
        
        @PostMapping("/order")
        @Idempotent(timeout = 30)
        public Result<Order> createOrder(@RequestBody OrderRequest request) {
            Order order = orderService.createOrder(request);
            return Result.success(order);
        }
    }
    

    优缺点分析

    优点

    • 实现简单,易于理解
    • 对业务代码侵入小,可通过AOP实现
    • 可以预先生成token,减少请求处理时的延迟

    缺点

    • 需要两次请求才能完成一次业务操作
    • 增加了客户端的复杂度
    • 依赖Redis等外部存储

    2. 基于数据库唯一约束的幂等性实现

    利用数据库的唯一约束特性可以简单有效地实现幂等性。当尝试插入重复数据时,数据库会抛出唯一约束异常,我们可以捕获这个异常并进行合适的处理。

    实现方式

    • 在关键业务表上添加唯一索引
    • 在插入数据时捕获唯一约束异常
    • 根据业务需求决定是返回错误还是返回已存在的数据

    代码实现

    @Service
    public class PaymentServiceImpl implements PaymentService {
    
        @Autowired
        private PaymentRepository paymentRepository;
        
        @Transactional
        @Override
        public PaymentResponse processPayment(PaymentRequest request) {
            try {
                // 创建支付记录,包含唯一业务标识
                Payment payment = new Payment();
                payment.setOrderNo(request.getOrderNo());
                payment.setTransactionId(request.getTransactionId()); // 唯一交易ID
                payment.setAmount(request.getAmount());
                payment.setStatus(PaymentStatus.PROCESSING);
                payment.setCreateTime(new Date());
                
                // 保存支付记录
                paymentRepository.save(payment);
                
                // 调用支付网关API
                // ...支付处理逻辑...
                
                // 更新支付状态
                payment.setStatus(PaymentStatus.SUCCESS);
                paymentRepository.save(payment);
                
                return new PaymentResponse(true, "支付成功", payment.getId());
            } catch (DataIntegrityViolationException e) {
                // 捕获唯一约束异常
                if (e.getCause() instanceof ConstraintViolationException) {
                    // 幂等性处理 - 查询已存在的支付记录
                    Payment existingPayment = paymentRepository
                            .findByTransactionId(request.getTransactionId())
                            .orElse(null);
                    
                    if (existingPayment != null) {
                        if (PaymentStatus.SUCCESS.equals(existingPayment.getStatus())) {
                            // 支付已成功处理,返回成功结果
                            return new PaymentResponse(true, "支付已处理", existingPayment.getId());
                        } else {
                            // 支付正在处理中,返回适当提示
                            return new PaymentResponse(false, "支付处理中", existingPayment.getId());
                        }
                    }
                }
                
                // 其他数据完整性问题
                log.error("支付失败", e);
                return new PaymentResponse(false, "支付失败", null);
            }
        }
    }
    
    // 支付实体类
    @Entity
    @Table(name = "payments")
    public class Payment {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        
        private String orderNo;
        
        @Column(unique = true) // 唯一约束
        private String transactionId;
        
        private BigDecimal amount;
        
        @Enumerated(EnumType.STRING)
        private PaymentStatus status;
        
        private Date createTime;
        
        // Getters and setters...
    }
    

    优缺点分析

    优点

    • 实现简单,利用数据库已有特性
    • 无需额外的存储组件
    • 强一致性保证

    缺点

    • 依赖数据库的唯一约束特性
    • 可能导致频繁的异常处理
    • 在高并发情况下可能成为性能瓶颈

    3. 基于分布式锁的幂等性实现

    分布式锁是实现幂等性的另一种有效方式,特别适合于高并发场景。通过对业务唯一标识加锁,可以确保同一时间只有一个请求能够执行业务逻辑。

    实现方式

    • 使用Redis、Zookeeper等实现分布式锁
    • 以请求的唯一标识作为锁的key
    • 在业务处理前获取锁,处理完成后释放锁

    基于Redis的分布式锁实现

    @Service
    public class InventoryServiceImpl implements InventoryService {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
        
        @Autowired
        private InventoryRepository inventoryRepository;
        
        private static final String LOCK_PREFIX = "inventory:lock:";
        private static final long LOCK_EXPIRE = 10000; // 10秒
        
        @Override
        public DeductResponse deductInventory(DeductRequest request) {
            String lockKey = LOCK_PREFIX + request.getRequestId();
            String requestId = UUID.randomUUID().toString();
            
            try {
                // 尝试获取分布式锁
                Boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, LOCK_EXPIRE, TimeUnit.MILLISECONDS);
                
                if (Boolean.FALSE.equals(acquired)) {
                    // 获取锁失败,说明可能是重复请求
                    return new DeductResponse(false, "请求正在处理中,请勿重复提交");
                }
                
                // 查询是否已处理过该请求
                Optional<InventoryRecord> existingRecord = inventoryRepository.findByRequestId(request.getRequestId());
                if (existingRecord.isPresent()) {
                    // 幂等性控制 - 请求已处理过
                    return new DeductResponse(true, "库存已扣减", existingRecord.get().getId());
                }
                
                // 执行库存扣减逻辑
                Inventory inventory = inventoryRepository.findByProductId(request.getProductId())
                        .orElseThrow(() -> new BusinessException("商品不存在"));
                        
                if (inventory.getStock() < request.getQuantity()) {
                    throw new BusinessException("库存不足");
                }
                
                // 扣减库存
                inventory.setStock(inventory.getStock() - request.getQuantity());
                inventoryRepository.save(inventory);
                
                // 记录库存操作
                InventoryRecord record = new InventoryRecord();
                record.setRequestId(request.getRequestId());
                record.setProductId(request.getProductId());
                record.setQuantity(request.getQuantity());
                record.setCreateTime(new Date());
                inventoryRepository.save(record);
                
                return new DeductResponse(true, "库存扣减成功", record.getId());
            } catch (BusinessException e) {
                return new DeductResponse(false, e.getMessage(), null);
            } catch (Exception e) {
                log.error("库存扣减失败", e);
                return new DeductResponse(false, "库存扣减失败", null);
            } finally {
                // 释放锁,注意只释放自己的锁
                if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
                    redisTemplate.delete(lockKey);
                }
            }
        }
    }
    

    使用Redisson简化实现

    @Service
    public class InventoryServiceImpl implements InventoryService {
    
        @Autowired
        private RedissonClient redissonClient;
        
        @Autowired
        private InventoryRepository inventoryRepository;
        
        private static final String LOCK_PREFIX = "inventory:lock:";
        
        @Override
        public DeductResponse deductInventory(DeductRequest request) {
            String lockKey = LOCK_PREFIX + request.getRequestId();
            RLock lock = redissonClient.getLock(lockKey);
            
            try {
                // 尝试获取锁,等待5秒,锁过期时间10秒
                boolean acquired = lock.tryLock(5, 10, TimeUnit.SECONDS);
                
                if (!acquired) {
                    return new DeductResponse(false, "请求正在处理中,请勿重复提交");
                }
                
                // 查询是否已处理过该请求
                // ...后续业务逻辑与前面例子相同...
                
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return new DeductResponse(false, "请求被中断", null);
            } catch (Exception e) {
                log.error("库存扣减失败", e);
                return new DeductResponse(false, "库存扣减失败", null);
            } finallyhttp://www.devze.com {
                // 释放锁
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        }
    }
    

    优缺点分析

    优点

    • 适用于高并发场景
    • 可以与其他幂等性策略结合使用
    • 提供较好的实时性控制

    缺点

    • 实现复杂度较高
    • 依赖外部存储服务

    4. 基于请求内容摘要的幂等性实现

    这种方案通过计算请求内容的哈希值或摘要,生成唯一标识作为幂等键,确保相同内容的请求只处理一次。

    实现方式

    • 计算请求参数的摘要值(如MD5, SHA-256等)
    • 将摘要值作为幂等键存储在Redis或数据库中
    • 请求处理前先检查该摘要值是否已存在
    • 存在则表示重复请求,不执行业务逻辑

    代码实现

    @RestController
    @RequestMapping("/api")
    public class TransferController {
    
        @Autowired
        private TransferService transferService;
        
        @Autowired
        private StringRedisTemplate redisTemplate;
        
        @PostMapping("/transfer")
        public Result<TransferResult&gpythont; transfer(@RequestBody TransferRequest request) {
            // 生成请求摘要作为幂等键
            String idempotentKey = generateIdempotentKey(request);
            String redisKey = "idempotent:digest:" + idempotentKey;
            
            // 尝试在Redis中设置幂等键,使用SetNX操作确保原子性
            Boolean isFirstRequest = redisTemplate.opsForValue()
                    .setIfAbsent(redisKey, "processed", 24, TimeUnit.HOURS);
            
            // 如果键已存在,说明是重复请求
            if (Boolean.FALSE.equals(isFirstRequest)) {
                // 查询处理结果(也可以直接存储处理结果)
                TransferRecord record = transferService.findByIdempotentKey(idempotentKey);
                
                if (record != null) {
                    // 返回之前的处理结果
                    return Result.success(new TransferResult(
                            record.getTransactionId(), 
                            "交易已处理", 
                   编程客栈         record.getAmount(),
                            record.getStatus()));
                } else {
                    // 幂等键存在但找不到记录,可能正在处理
                    return Result.fail("请求正在处理中,请勿重复提交");
                }
            }
            
            try {
                // 执行转账业务逻辑
                TransferResult result = transferService.executeTransfer(request, idempotentKey);
                return Result.success(result);
            } catch (Exception e) {
                // 处理失败时,删除幂等键,允许客户端重试
                // 或者可以保留键但记录失败状态,取决于业务需求
                redisTemplate.delete(redisKey);
                return Result.fail("转账处理失败: " + e.getMessage());
            }
        }
        
        /**
         * 生成请求内容摘要作为幂等键
         */
        private String generateIdempotentKey(TransferRequest request) {
            // 组合关键字段,确保能唯一标识业务操作
            String content = request.getFroMACcount() 
                    + "|" + request.getToAccount() 
                    + "|" + request.getAmount().toString()
                    + "|" + request.getRequestTime();
            
            // 计算MD5摘要
            try {
                MessageDigest md 编程= MessageDigest.getInstance("MD5");
                byte[] digest = md.digest(content.getBytes(StandardCharsets.UTF_8));
                return HexFormat.of().formatHex(digest);
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException("生成幂等键失败", e);
            }
        }
    }
    
    @Service
    public class TransferServiceImpl implements TransferService {
    
        @Autowired
        private TransferRecordRepository transferRecordRepository;
        
        @Autowired
        private AccountRepository accountRepository;
        
        @Override
        @Transactional
        public TransferResult executeTransfer(TransferRequest request, String idempotentKey) {
            // 执行转账业务逻辑
            // 1. 检查账户余额
            // 2. 扣减来源账户
            // 3. 增加目标账户
            
            // 生成交易ID
            String transactionId = UUID.randomUUID().toString();
            
            // 保存交易记录,包含幂等键
            TransferRecord record = new TransferRecord();
            record.setTransactionId(transactionId);
            record.setFromAccount(request.getFromAccount());
            record.setToAccount(request.getToAccount());
            record.setAmount(request.getAmount());
            record.setIdempotentKey(idempotentKey);
            record.setStatus(TransferStatus.SUCCESS);
            record.setCreateTime(new Date());
            
            transferRecordRepository.save(record);
            
            return new TransferResult(
                    transactionId,
                    "转账成功",
                    request.getAmount(),
                    TransferStatus.SUCCESS);
        }
        
        @Override
        public TransferRecord findByIdempotentKey(String idempotentKey) {
            return transferRecordRepository.findByIdempotentKey(idempotentKey).orElse(null);
        }
    }
    
    // 转账记录实体
    @Entity
    @Table(name = "transfer_records", indexes = {
        @Index(name = "idx_idempotent_key", columnList = "idempotent_key", unique = true)
    })
    public class TransferRecord {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        
        private String transactionId;
        
        private String fromAccount;
        
        private String toAccount;
        
        private BigDecimal amount;
        
        @Column(name = "idempotent_key")
        private String idempotentKey;
        
        @Enumerated(EnumType.STRING)
        private TransferStatus status;
        
        private Date createTime;
        
        // Getters and setters...
    }
    

    使用自定义注解简化实现

    // 自定义幂等性注解
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Idempotent {
        /**
         * 过期时间(秒)
         */
        int expireSeconds() default 86400; // 默认24小时
        
        /**
         * 幂等键来源,可从请求体、请求参数等提取
         */
        KeySource source() default KeySource.REQUEST_BODY;
        
        /**
         * 提取参数的表达式(如SpEL表达式)
         */
        String[] expression() default {};
        
        enum KeySource {
            REQUEST_BODY,  // 请求体
            PATH_VARIABLE, // 路径变量
            REQUEST_PARAM, // 请求参数
            CUSTOM        // 自定义
        }
    }
    
    // AOP实现
    @Aspect
    @Component
    public class IdempotentAspect {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
        
        @Around("@annotation(idempotent)")
        public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
            // 获取请求参数
            Object[] args = joinPoint.getArgs();
            
            // 根据注解配置生成幂等键
            String idempotentKey = generateKey(joinPoint, idempotent);
            String redisKey = "idempotent:digest:" + idempotentKey;
            
            // 检查是否重复请求
            Boolean setSuccess = redisTemplate.opsForValue()
                    .setIfAbsent(redisKey, "processing", idempotent.expireSeconds(), TimeUnit.SECONDS);
            
            if (Boolean.FALSE.equals(setSuccess)) {
                // 获取存储的处理结果
                String value = redisTemplate.opsForValue().get(redisKey);
                
                if ("processing".equals(value)) {
                    throw new BusinessException("请求正在处理中,请勿重复提交");
                } else if (value != null) {
                    // 已处理,返回缓存的结果
                    return jsON.parseobject(value, Object.class);
                }
            }
            
            try {
                // 执行实际方法
                Object result = joinPoint.proceed();
                
                // 存储处理结果
                redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(result), 
                        idempotent.expireSeconds(), TimeUnit.SECONDS);
                
                return result;
            } catch (Exception e) {
                // 处理失败,删除键允许重试
                redisTemplate.delete(redisKey);
                throw e;
            }
        }
        
        /**
         * 根据注解配置生成幂等键
         */
        private String generateKey(ProceedingJoinPoint joinPoint, Idempotent idempotent) {
            // 提取请求参数,根据KeySource和expression生成摘要
            // 实际实现会更复杂,这里简化
            String content = "";
            
            // 计算MD5摘要
            try {
                MessageDigest md = MessageDigest.getInstance("MD5");
                byte[] digest = md.digest(content.getBytes(StandardCharsets.UTF_8));
                return HexFormat.of().formatHex(digest);
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException("生成幂等键失败", e);
            }
        }
    }
    
    // 控制器使用注解
    @RestController
    @RequestMapping("/api")
    public class TransferController {
    
        @Autowired
        private TransferService transferService;
        
        @PostMapping("/transfer")
        @Idempotent(expireSeconds = 3600, source = KeySource.REQUEST_BODY, 
                    expression = {"fromAccount", "toAccount", "amount", "requestTime"})
        public Result<TransferResult> transfer(@RequestBody TransferRequest request) {
            // 执行转账业务逻辑
            TransferResult result = transferService.executeTransfer(request);
            return Result.success(result);
        }
    }
    

    优缺点分析

    优点

    • 方案更通用
    • 实现相对简单,易于集成
    • 对客户端友好,不需要额外的token请求

    缺点

    • 哈希计算有一定性能开销
    • 表单数据顺序变化可能导致不同的摘要值

    总结

    幂等性设计是系统稳定性和可靠性的重要保障,通过合理选择和实现幂等性策略,可以有效防止因重复请求导致的数据不一致问题。在实际项目中,应根据具体的业务需求和系统架构,选择最适合的幂等性实现方案。

    以上就是SpringBoot中4种接口幂等性的实现策略的详细内容,更多关于SpringBoot接口幂等性的资料请关注编程客栈(www.devze.com)其它相关文章!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜