目录
- 1. 基于唯一标识的去重机制
- 实现方式:
- 2. 令牌(Token)机制
- 实现流程:
- 3. 乐观锁机制
- 4. 状态机控制
- 5. 分布式锁(高并发场景)
- 选择建议
在 Java 中保证接口幂等性(即多次调用同一接口产生与单次调用相同的结果,不会引发副作用),需要结合业务场景选择合适的方案。以下是常见的实现方式及技术细节:
1. 基于唯一标识的去重机制
核心思想:为每次请求生成唯一标识(如订单号、请求 ID),服务端通过记录该标识是否已处理,避免重复执行。
实现方式:
数据库唯一约束:将唯一标识作为数据库表的唯一索引,重复请求会触发主键冲突异常,直接返回成功(或错误提示)。
// 示例:订单表唯一索引(order_no) @Entity @Table(uniqueConstraints = {@UniqueConstraint(columnNames = "orderNo")}) public class Order { @Id private Long id; private String orderNo; // 唯一订单号(作为幂等标识) // 其他字段... } // 服务层处理 @Transactional public Result createOrder(OrderDTO dto) { try { // 尝试插入订单(依赖数据库唯一约束) Order order = new Order(); order.setOrderNo(dto.getOrderNo()); orderRepository.save(order); // 执行后续业务(如扣减库存) 编程 return Result.success(); } catch (DataIntegrityViolationException e) { // 唯一约束冲突,说明已处理过 log.warn("订单已存在: {}", dto.getOrderNo()); return Result.success(); // 或返回已有结果 } }
缓存记录(Redis) :利用 Redis 的SETNX
(不存在则设置)特性,判断请求是否已处理。
@Autowired private StringRedisTemplate redisTemplate; public Result processRequest(String requestId) { // 尝试设置唯一标识,过期时间防止内存溢出 Boolean isFirst = redisTemplate.opsForValue().setIfAbsent( "idempotent:" + requestId, "processed", 1, TimeUnit.HOURS ); if (Boolean.TRUE.equals(isFirst)) { // 首次请求,执行业务逻辑 doBusiness(); return Result.success(); } else { // 重复请求,返回已有结果 return Result.success("已处理"); } }
2. 令牌(Token)机制
核心思想:客户端先向服务端申请令牌,请求接口时携带令牌,服务端验证令牌有效性后处理业务,并标记令牌为已使用。
实现流程:
- 客户端请求获取令牌(服务端生成令牌并存储到 Redis)。
- 客户端携带令牌调用业务接口。
- 服务端校验令牌:存在则处理业务并删除令牌;不存在则拒绝。
// 生成令牌 public String generateToken() { String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set("token:" + token, "valid", 30, TimeUnit.MINUTES); return token; } // 校验令牌并处理业务 public Result doBusiness(String token, BusinessDTO dto) { // 删除令牌(原子操作,确保唯一处理) Boolean isValid = redisTemplate.delete("token:" + token); if (Boolean.TRUE.equals(isValid)) { // 令牌有效,执行业务 process(dto); return Result.success(); } else { // 令牌无效(已使用或过期) return Result.fail("重复请求"); } }
3. 乐观锁机制
核心思想php:适用于更新操作,通过版本号控制,确保只有版本匹配时才执行更新,避免重复更新。
@Entity public class Product { @Id private Long id; private Integer stock; // 库存 private Integer version; // 版本号 } @Transactional public Result reduceStock(Long productId, Integer quantity) { // 查询商品及当前版本 Product product = productRepository.findById(productId) .orElseThrow(() -> new RuntimeException("商品不存在")); // 检查库存 if (product.getStock() < quantity) { return Result.fail("库存不足"); } // 乐观锁更新(where条件包含版本号) int rows = productRepository.reduceStock( productId, quantity, product.getVersion() // 当前版本 ); if (rows > 0) { return Result.success(); } else { // 版本不匹配,说明已被其他请求处理 return Result.fail("操作冲突,请重试"); } } // Repository层SQL(JPA示例) @Modifying @Query("UPDATE Product p SET p.stock = p.stock - :quantity, p.version = p.version + 1 " + "WHERE p.id = :id AND p.version = :version") int reduceStock(@Param("id") Long id, @Param("quantity") Integer quantity, @Param("version") Integer version);
4. 状态编程客栈机控制
核心思想:通过状态流转约束,确保接口只能在特定状态下执行,避免重复操作(如订单状态从 “待支付” 到 “已支付” 的单向流转)。
public enum OrderStatus { CREATED(1, "待支付"), PAID(2, "已支付"), CANCELLED(3, "已取消"); private int code; private String desc; // 构造器、getter... } @Transactional public Result payOrder(Long orderId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new RuntimeException("订单不存在")); // 仅允许“待支付”状态执行支付 if (order.getStatus() != OrderStatus.CREATED) { log.warn("订单状态异常: {}", orderId); return R编程客栈esult.success("订单已处理"); // 重复支付请求直接返回成功 } // 执行支付逻辑(如调用支付网关) boolean paySuccess = paymentGateway.pay(order); if (paySuccess) { order.setStatus(OrderStatus.PAID); orderRepository.save(order); return Result.success(); } else { return Result.fail("支付失败"); } }
5. 分布式锁(高并发场景)
核心思想:在分布式系统中,通过分布式锁(如 Redjsis、ZooKeeper)确保同一时间只有一个请求处理业务。
// 基于Redis的分布式锁(使用Redisson) @Autowired private RedissonClient redissonClient; public Result processDistributed(String key) { RLock lock = redissonClient.getLock("lock:" + key); try { // 尝试获取锁,最多等待10秒,持有锁1分钟 boolean locked = lock.tryLock(10, 60, TimeUnit.SECONDS); if (locked) { // 检查是否已处理(双重校验) if (isProcessed(key)) { return Result.success("已处理"); } // 执行业务 doBusiness(); markASProcessed(key); // 标记为已处理 return Result.success(); } else { // 获取锁失败,可能是重复请求 return Result.fail("操作繁忙,请重试"); } } finally { // 释放锁(仅释放自己持有的锁) if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
选择建议
- 查询接口:天然幂等,无需额外处理。
- 新增操作:优先使用 “唯一标识 + 数据库 / Redis 去重”。
- 更新操作:优先使用 “乐观锁” 或 “状态机”。
- 分布式系统:结合 “分布式锁” 与 “唯一标识” 确保一致性。
- 高并发场景:优先使用 Redis(性能优于数据库)。
需注意:幂等性设计需结合业务场景,避免过度设计;同时要处理异常情况(如网络超时),确保客户端重试时的正确性。
到此这篇关于java中接口幂等性的五种实现方法的文章就介绍到这了,更多相关java 接口幂等性内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论