一、什么是接口幂等性?
幂等性 是指对同一个接口的多次相同请求 与一次请求产生相同的效果。在分布式系统中,保证接口幂等性是确保数据一致性的重要手段。
二、常见需要幂等性的场景
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. 监控与告警
-
记录幂等拦截日志
-
监控重复请求比例
-
设置异常告警阈值
六、常见陷阱与注意事项
-
DELETE操作的幂等性:删除不存在的资源应该返回成功
-
部分成功的处理:确保操作原子性,避免部分成功
-
缓存一致性:使用缓存时要考虑缓存与数据库的一致性
-
分布式事务:跨服务调用时需要分布式事务支持
-
时钟同步:基于时间戳的方案需要确保时钟同步
七、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机制+数据库唯一约束),确保万无一失。同时,良好的监控和日志记录对于排查幂等问题至关重要。