蚂蚁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机制+数据库唯一约束),确保万无一失。同时,良好的监控和日志记录对于排查幂等问题至关重要。

相关推荐
毕设源码-钟学长2 小时前
【开题答辩全过程】以 高校课程档案管理系统的设计与实现为例,包含答辩的问题和答案
java·开发语言
ps酷教程2 小时前
ChunkedWriteHandler源码浅析
java·netty·分块传输
88号技师2 小时前
2026年1月一区SCI-波动光学优化算法Wave Optics Optimizer-附Matlab免费代码
开发语言·算法·数学建模·matlab·优化算法
扶苏-su2 小时前
Java-文件
java·开发语言
Tomorrow'sThinker2 小时前
篮球裁判犯规识别系统(四) foul_fn函数 上
java·前端·javascript
wregjru2 小时前
【读书笔记】Effective C++ 条款3:尽可能使用const
开发语言·c++
FreeBuf_2 小时前
“前缀替换“攻击引发恐慌:高度仿真的“Jackson“冒牌库入侵Maven中央仓库
java·python·maven
kylezhao20192 小时前
C#手写串口助手
开发语言·c#
Kyln.Wu2 小时前
【python实用小脚本-292】[HR揭秘]手工党点名10分钟的终结者|Python版Zoom自动签到+名单导出加速器(建议收藏)
开发语言·python·swift