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)其它相关文章!
精彩评论