蚂蚁Java面试被问:接口幂等性的保证方案

一、什么是接口幂等性?

幂等性 是指对同一个接口的多次相同请求一次请求产生相同的效果。在分布式系统中,保证接口幂等性是确保数据一致性的重要手段。

二、常见需要幂等性的场景

1. 网络问题导致的重复请求

  • 前端重复提交表单

  • 网络超时重试机制

  • 消息队列重复消费

2. 业务场景需求

  • 支付接口(防止重复扣款)

  • 订单创建(防止重复下单)

  • 库存扣减(防止超卖)

  • 用户注册(防止重复注册)

三、幂等性解决方案

方案1:Token机制(最常用)

实现流程:

java

复制

下载

复制代码
// 1. 客户端获取token
@GetMapping("/token")
public String getToken() {
    String token = UUID.randomUUID().toString();
    // 存储token到Redis,设置过期时间
    redisTemplate.opsForValue().set("token:" + token, "1", 5, TimeUnit.MINUTES);
    return token;
}

// 2. 提交请求携带token
@PostMapping("/submit")
public Response submitOrder(@RequestHeader("X-Token") String token, OrderDTO order) {
    // 验证并删除token
    Boolean deleted = redisTemplate.delete("token:" + token);
    if (!deleted) {
        return Response.fail("重复提交或token已过期");
    }
    // 处理业务逻辑
    return processOrder(order);
}
前端配合:

javascript

复制

下载

复制代码
// 提交前获取token
async function submitForm() {
    const token = await getToken();
    axios.post('/api/submit', data, {
        headers: { 'X-Token': token }
    });
}

方案2:唯一索引约束

数据库层面保证:

sql

复制

下载

复制代码
-- 创建唯一索引
ALTER TABLE orders ADD UNIQUE INDEX idx_unique_order (order_no, user_id);

-- 或者创建防重表
CREATE TABLE duplicate_prevention (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    biz_type VARCHAR(50) NOT NULL,
    biz_id VARCHAR(100) NOT NULL,
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_biz (biz_type, biz_id)
);
业务代码实现:

java

复制

下载

复制代码
@Service
public class OrderService {
    
    @Transactional
    public Order createOrder(OrderDTO dto) {
        // 1. 先插入防重记录
        try {
            duplicatePreventionDao.insert("ORDER_CREATE", dto.getOrderNo());
        } catch (DuplicateKeyException e) {
            throw new BusinessException("订单已存在");
        }
        
        // 2. 创建订单
        return orderDao.create(dto);
    }
}

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】​​​

方案3:乐观锁机制

基于版本号:

java

复制

下载

复制代码
// 实体类
@Data
public class Product {
    private Long id;
    private String name;
    private Integer stock;
    private Integer version;  // 版本号字段
}

// 更新操作
@Update("UPDATE product SET stock = stock - #{quantity}, version = version + 1 
         WHERE id = #{id} AND version = #{version}")
int deductStockWithVersion(@Param("id") Long id, 
                          @Param("quantity") Integer quantity,
                          @Param("version") Integer version);

// 业务代码
public boolean deductStock(Long productId, Integer quantity) {
    Product product = productDao.selectById(productId);
    int rows = productDao.deductStockWithVersion(productId, quantity, product.getVersion());
    return rows > 0;  // 返回是否更新成功
}
基于状态机:

java

复制

下载

复制代码
// 订单状态流转
public enum OrderStatus {
    INIT(0), PAYING(1), PAID(2), CANCELLED(-1);
    
    // 状态转换验证
    public static boolean canChange(OrderStatus from, OrderStatus to) {
        // 定义允许的状态转换规则
    }
}

// 更新时检查状态
@Update("UPDATE orders SET status = #{newStatus} 
         WHERE id = #{id} AND status = #{oldStatus}")
int updateStatus(@Param("id") Long id, 
                @Param("oldStatus") Integer oldStatus,
                @Param("newStatus") Integer newStatus);

方案4:分布式锁

基于Redis实现:

java

复制

下载

复制代码
@Component
public class IdempotentService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    public <T> T executeWithIdempotent(String key, long expireTime, 
                                      Supplier<T> businessLogic) {
        RLock lock = redissonClient.getLock("idempotent:" + key);
        
        try {
            // 尝试加锁
            if (lock.tryLock(0, expireTime, TimeUnit.SECONDS)) {
                // 检查是否已处理(可选)
                if (isProcessed(key)) {
                    return getCachedResult(key);
                }
                
                // 执行业务逻辑
                T result = businessLogic.get();
                
                // 标记已处理
                markAsProcessed(key, result, expireTime);
                
                return result;
            } else {
                throw new BusinessException("请求正在处理中");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BusinessException("系统繁忙");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

方案5:全局唯一ID + 幂等表

实现架构:

java

复制

下载

复制代码
// 1. 生成全局唯一请求ID
public class RequestIdGenerator {
    public static String generate() {
        // 雪花算法、UUID等
        return SnowflakeIdGenerator.nextId();
    }
}

// 2. 幂等表设计
@Entity
@Table(name = "idempotent_record")
public class IdempotentRecord {
    @Id
    private String requestId;      // 请求ID
    private String bizType;        // 业务类型
    private String bizKey;         // 业务唯一标识
    private Integer status;        // 处理状态
    private String result;         // 处理结果(JSON格式)
    private LocalDateTime createTime;
}

// 3. 处理流程
@Component
public class IdempotentProcessor {
    
    @Transactional
    public Object process(String requestId, String bizType, Supplier<Object> logic) {
        // 先查询是否已处理
        IdempotentRecord record = recordDao.findByRequestId(requestId);
        if (record != null) {
            return JSON.parse(record.getResult());
        }
        
        // 插入记录(状态为处理中)
        record = new IdempotentRecord(requestId, bizType, "PROCESSING");
        recordDao.save(record);
        
        try {
            // 执行业务逻辑
            Object result = logic.get();
            
            // 更新记录状态和结果
            record.setStatus("SUCCESS");
            record.setResult(JSON.toJSONString(result));
            recordDao.update(record);
            
            return result;
        } catch (Exception e) {
            // 更新为失败状态
            record.setStatus("FAILED");
            recordDao.update(record);
            throw e;
        }
    }
}

四、方案选择策略

方案 适用场景 优点 缺点
Token机制 前端表单提交、用户交互 实现简单,用户体验好 需要前后端配合
唯一索引 数据创建类操作 绝对可靠,数据库保障 不能用于更新操作
乐观锁 库存扣减、余额变更 并发性能好 需要设计版本字段
分布式锁 高并发扣减、抢购 强一致性保证 性能开销较大
全局ID 分布式系统、异步消息 通用性强,可追溯 实现相对复杂

五、最佳实践建议

1. 分层防御

java

复制

下载

复制代码
// 多层幂等性保障
public class MultiLayerIdempotent {
    // 第一层:Token验证(防重复提交)
    // 第二层:分布式锁(防并发重复)
    // 第三层:数据库唯一约束(最终保障)
}

2. 合理设计幂等键

  • 支付业务:订单号 + 支付方式

  • 库存扣减:商品ID + 订单号

  • 消息消费:消息ID + 消费者组

3. 状态机设计

java

复制

下载

复制代码
// 清晰的状态流转
public enum OrderStatus {
    // 明确的状态定义和转换规则
    INIT {
        @Override
        public boolean canChangeTo(OrderStatus status) {
            return status == PAYING || status == CANCELLED;
        }
    },
    PAYING {
        @Override
        public boolean canChangeTo(OrderStatus status) {
            return status == PAID || status == CANCELLED;
        }
    };
}

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案
【点击此处即可/免费获取】​​​

4. 超时与重试机制

java

复制

下载

复制代码
// 结合重试机制的幂等
@Retryable(value = Exception.class, maxAttempts = 3)
@Idempotent(key = "#order.orderNo")
public Order createOrder(Order order) {
    // 业务逻辑
}

5. 监控与告警

  • 记录幂等拦截日志

  • 监控重复请求比例

  • 设置异常告警阈值

六、常见陷阱与注意事项

  1. DELETE操作的幂等性:删除不存在的资源应该返回成功

  2. 部分成功的处理:确保操作原子性,避免部分成功

  3. 缓存一致性:使用缓存时要考虑缓存与数据库的一致性

  4. 分布式事务:跨服务调用时需要分布式事务支持

  5. 时钟同步:基于时间戳的方案需要确保时钟同步

七、Spring Boot实现示例

java

复制

下载

复制代码
@RestControllerAdvice
public class IdempotentAspect {
    
    @Around("@annotation(idempotent)")
    public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) {
        String key = generateKey(joinPoint, idempotent);
        
        // 尝试获取锁
        if (!redisLock.tryLock(key, idempotent.expireTime())) {
            throw new IdempotentException("重复请求");
        }
        
        try {
            // 检查是否已处理
            if (redisTemplate.hasKey(key)) {
                return redisTemplate.get(key);
            }
            
            // 执行业务
            Object result = joinPoint.proceed();
            
            // 缓存结果
            redisTemplate.set(key, result, idempotent.expireTime(), TimeUnit.SECONDS);
            
            return result;
        } finally {
            redisLock.unlock(key);
        }
    }
}

// 使用注解
@PostMapping("/order")
@Idempotent(key = "#order.orderNo", expireTime = 30)
public Order createOrder(@RequestBody OrderDTO order) {
    return orderService.create(order);
}

总结

幂等性设计需要根据具体的业务场景选择合适的方案,通常建议采用多层次防御策略。对于重要的金融、交易类接口,建议至少采用两种方案结合使用(如Token机制+数据库唯一约束),确保万无一失。同时,良好的监控和日志记录对于排查幂等问题至关重要。

相关推荐
阿杰 AJie4 分钟前
Lambda 表达式大全
开发语言·windows·python
格鸰爱童话5 分钟前
python基础总结
开发语言·python
输出输入10 分钟前
JAVA能进行鸿蒙系统应用的开发吗
java
a努力。10 分钟前
宇树Java面试被问:数据库死锁检测和自动回滚机制
java·数据库·elasticsearch·面试·职场和发展·rpc·jenkins
叁散15 分钟前
实验项目4 光电式传感器原理与应用(基于Matlab)
开发语言·matlab
先做个垃圾出来………15 分钟前
Python try-except-else 语句详解
开发语言·python
PwnGuo16 分钟前
Android逆向:在 Unidbg 中解决 native 函数内调用 Java 方法的报错
android·java·python
进击的小头18 分钟前
为什么C语言也需要设计模式
c语言·开发语言·设计模式
输出输入24 分钟前
IJ IDEA 目录结构
java
Kratzdisteln29 分钟前
【1902】预先生成完整的树状PPT结构
java·前端·powerpoint