Java项目实现幂等性方案总结
幂等性(Idempotence)是分布式系统和API设计中一个重要的概念,指的是对同一个操作执行一次或多次,其产生的结果是相同的。在Java项目中实现幂等性可以避免重复操作带来的问题,如重复支付、重复下单等。
一、幂等性基础概念
1. 幂等性场景
- 用户重复点击提交按钮
- 消息队列重复消费
- 接口超时重试
- 分布式服务调用失败重试
2. 需要幂等的操作
- 创建操作(需防止重复创建)
- 更新操作(需防止重复更新)
- 删除操作(需防止重复删除)
- 支付/交易类操作
二、常见幂等性实现方案
1. 唯一索引/主键约束
适用场景:防止重复插入数据
// 数据库表添加唯一约束
ALTER TABLE orders ADD UNIQUE KEY (order_no);
// Java代码
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
try {
orderDao.insert(order);
} catch (DuplicateKeyException e) {
// 捕获唯一键冲突异常
log.warn("重复订单: {}", order.getOrderNo());
throw new BusinessException("订单已存在");
}
}
}
2. 乐观锁
适用场景:更新操作幂等
// 数据库表添加version字段
ALTER TABLE products ADD COLUMN version INT DEFAULT 0;
// Java代码
@Service
public class ProductService {
@Transactional
public void updateStock(Long productId, int quantity) {
Product product = productDao.selectById(productId);
int affected = productDao.updateStock(
productId, quantity, product.getVersion());
if (affected == 0) {
throw new OptimisticLockException("更新失败,请重试");
}
}
}
// Mapper XML
<update id="updateStock">
UPDATE products
SET stock = stock - #{quantity},
version = version + 1
WHERE id = #{productId}
AND version = #{version}
</update>
3. 分布式锁
适用场景:分布式环境下的幂等控制
@Service
public class PaymentService {
@Autowired
private RedissonClient redissonClient;
public void makePayment(String orderNo, BigDecimal amount) {
String lockKey = "payment:" + orderNo;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,等待5秒,锁有效期30秒
boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后再试");
}
// 检查是否已处理过
if (paymentDao.existsByOrderNo(orderNo)) {
return; // 已处理,直接返回
}
// 执行业务逻辑
processPayment(orderNo, amount);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("支付处理被中断");
} finally {
lock.unlock();
}
}
}
4. Token机制
适用场景:防止表单重复提交
@RestController
public class OrderController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/order/token")
public String generateToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("order:token:" + token, "1", 5, TimeUnit.MINUTES);
return token;
}
@PostMapping("/order/create")
public ResponseEntity<?> createOrder(@RequestParam String token, Order order) {
String key = "order:token:" + token;
Boolean deleted = redisTemplate.delete(key);
if (Boolean.FALSE.equals(deleted)) {
return ResponseEntity.badRequest().body("无效或已使用的token");
}
orderService.createOrder(order);
return ResponseEntity.ok().build();
}
}
5. 状态机
适用场景:有状态流转的业务
@Service
public class OrderService {
@Transactional
public void cancelOrder(Long orderId) {
Order order = orderDao.findById(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
if (order.getStatus() == OrderStatus.CANCELLED) {
return; // 已经是取消状态,直接返回
}
if (order.getStatus() != OrderStatus.PAID) {
throw new BusinessException("当前状态不能取消订单");
}
order.setStatus(OrderStatus.CANCELLED);
orderDao.update(order);
}
}
6. 消息队列幂等
适用场景:消息队列消费幂等
@Component
@RabbitListener(queues = "order.queue")
public class OrderMessageListener {
@Autowired
private OrderDao orderDao;
@RabbitHandler
public void process(@Payload OrderMessage message,
@Headers Map<String, Object> headers) {
String messageId = (String) headers.get("message_id");
// 检查是否已处理过该消息
if (orderDao.existsByMessageId(messageId)) {
return;
}
// 处理订单
Order order = convertToOrder(message);
order.setMessageId(messageId);
orderDao.insert(order);
}
}
三、分布式系统幂等方案
1. 全局唯一ID + 去重表
@Service
public class PaymentService {
@Autowired
private DistributedIdGenerator idGenerator;
@Transactional
public void processPayment(PaymentRequest request) {
// 生成唯一业务ID
String businessId = "pay_" + idGenerator.nextId();
// 检查是否已处理
if (paymentDao.existsByBusinessId(businessId)) {
return;
}
// 记录处理标记
paymentDao.insertProcessingRecord(businessId);
try {
// 执行业务逻辑
doPayment(request);
// 更新状态为成功
paymentDao.updateStatus(businessId, "SUCCESS");
} catch (Exception e) {
// 更新状态为失败
paymentDao.updateStatus(businessId, "FAILED");
throw e;
}
}
}
2. 基于Redis的幂等控制
@Service
public class IdempotentService {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean checkAndSet(String idempotentKey, long expireSeconds) {
// setIfAbsent = SETNX + EXPIRE
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(idempotentKey, "1", expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
public void release(String idempotentKey) {
redisTemplate.delete(idempotentKey);
}
}
// 使用示例
@RestController
public class ApiController {
@Autowired
private IdempotentService idempotentService;
@PostMapping("/api/do-something")
public ResponseEntity<?> doSomething(@RequestHeader("X-Request-Id") String requestId) {
if (!idempotentService.checkAndSet("idempotent:" + requestId, 3600)) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body("请求正在处理或已处理完成");
}
try {
// 执行业务逻辑
return ResponseEntity.ok().build();
} finally {
idempotentService.release("idempotent:" + requestId);
}
}
}
四、最佳实践建议
- 合理选择方案:根据业务场景选择最适合的幂等方案
- 客户端配合:前端应防止重复提交(如按钮禁用)
- 日志记录:关键操作记录详细日志以便排查问题
- 过期机制:设置合理的过期时间,避免存储无限增长
- 性能考虑:幂等控制不应成为系统瓶颈
- 异常处理:设计良好的异常处理机制
- 测试验证:充分测试幂等逻辑,特别是并发场景
五、常见问题解决方案
1. 网络超时问题
- 客户端超时后应查询结果而不是直接重试
- 服务端应提供查询接口
2. 并发问题
- 使用分布式锁控制并发
- 数据库使用乐观锁/悲观锁
3. 分布式环境一致性问题
- 考虑使用分布式事务或最终一致性方案
- 引入消息队列实现异步处理
通过合理选择和组合上述方案,可以在Java项目中有效实现各种场景下的幂等性需求,提高系统的健壮性和可靠性。