摘要介绍了接口幂等性的概念与重要性,分析了网络波动、客户端重试等引发重复请求的场景,并指出支付、订单创建等接口对幂等性的高要求。重点阐述了四种实现方案:基于数据库唯一索引、Token令牌、分布式锁及Redis原子操作,分别说明其原理、优劣势与适用场景。
幂等性(Idempotence)这个术语听起来有些学术,但通俗来讲就是:无论对同一个操作执行多少次,结果始终保持一致。在API设计中,这一特性如同安全锁,确保重复请求不会引发意外的副作用,例如同一个订单被重复扣款,或同一个用户被重复创建。
在实际系统中,触发重复请求的场景十分常见,主要可归纳为以下几种:
长期稳定更新的攒劲资源: >>>点此立即查看<<<
几乎每个核心接口都需要考虑幂等性,以下场景尤其敏感:
实现原理
该方案思路最为直接:在数据库表中添加唯一索引,利用数据库自身的约束机制拦截重复插入。如果两个请求的订单号相同,第二次插入时会触发唯一索引冲突,捕获异常后直接返回已存在的数据即可。
核心源码
数据库表设计:
CREATE TABLE `order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `amount` decimal(10,2) NOT NULL COMMENT '金额', `status` int(11) NOT NULL COMMENT '状态', `create_time` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) COMMENT '订单号唯一索引' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
Service层实现:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public Order createOrder(OrderCreateRequest request) {
// 生成唯一订单号
String orderNo = generateOrderNo();
Order order = new Order();
order.setOrderNo(orderNo);
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
order.setStatus(0);
order.setCreateTime(new Date());
try {
orderMapper.insert(order);
return order;
} catch (DuplicateKeyException e) {
// 唯一索引冲突,说明订单已存在
return orderMapper.selectByOrderNo(orderNo);
}
}
private String generateOrderNo() {
// 生成唯一订单号的逻辑
return "ORD" + System.currentTimeMillis() + RandomUtil.randomString(6);
}
}
优劣势分析
优点:
缺点:
适用场景
实现原理
该方案较为经典,流程清晰:
核心源码
Token生成与验证:
@Component
public class IdempotentTokenService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 生成幂等性令牌
*/
public String generateToken() {
String token = UUID.randomUUID().toString();
// 存储令牌,设置过期时间为5分钟
redisTemplate.opsForValue().set("idempotent_token:" + token, token, 5, TimeUnit.MINUTES);
return token;
}
/**
* 验证令牌
*/
public boolean validateToken(String token) {
if (StringUtils.isEmpty(token)) {
return false;
}
String key = "idempotent_token:" + token;
// 原子操作,获取并删除令牌
return redisTemplate.delete(key);
}
}
Controller层实现:
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private IdempotentTokenService idempotentTokenService;
@Autowired
private OrderService orderService;
/**
* 获取幂等性令牌
*/
@GetMapping("/token")
public ResponseEntity getToken() {
String token = idempotentTokenService.generateToken();
return ResponseEntity.ok(token);
}
/**
* 创建订单
*/
@PostMapping
public ResponseEntity createOrder(@RequestHeader("X-Idempotent-Token") String token,
@RequestBody OrderCreateRequest request) {
// 验证令牌
if (!idempotentTokenService.validateToken(token)) {
return ResponseEntity.badRequest().build();
}
Order order = orderService.createOrder(request);
return ResponseEntity.ok(order);
}
}
优劣势分析
优点:
缺点:
适用场景
实现原理
分布式锁的思路是:同一时间只允许一个请求执行关键操作,其他请求要么等待,要么直接返回。通过锁的互斥性,天然避免重复处理。
核心源码
分布式锁实现:
@Component
public class DistributedLockService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 获取分布式锁
*/
public boolean tryLock(String key, long expireTime) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "locked", expireTime, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 释放分布式锁
*/
public void releaseLock(String key) {
redisTemplate.delete(key);
}
}
Service层实现:
@Service
public class PaymentService {
@Autowired
private DistributedLockService distributedLockService;
@Autowired
private PaymentMapper paymentMapper;
public Payment processPayment(PaymentRequest request) {
// 生成锁键
String lockKey = "payment_lock:" + request.getOrderNo();
try {
// 尝试获取锁,设置过期时间为30秒
if (!distributedLockService.tryLock(lockKey, 30000)) {
throw new BusinessException("支付处理中,请稍后再试");
}
// 检查是否已支付
Payment existingPayment = paymentMapper.selectByOrderNo(request.getOrderNo());
if (existingPayment != null && existingPayment.getStatus() == 1) {
return existingPayment;
}
// 处理支付逻辑
Payment payment = new Payment();
payment.setOrderNo(request.getOrderNo());
payment.setAmount(request.getAmount());
payment.setStatus(1);
payment.setCreateTime(new Date());
paymentMapper.insert(payment);
return payment;
} finally {
// 释放锁
distributedLockService.releaseLock(lockKey);
}
}
}
优劣势分析
优点:
缺点:
适用场景
实现原理
利用Redis的原子操作(如SETNX)创建一个"去重标记"。如果标记已存在,说明请求已被处理,直接拒绝。该方案与Token方案类似,但更加轻量,无需提前获取令牌。
核心源码
Redis幂等性服务:
@Component
public class RedisIdempotentService {
@Autowired
private RedisTemplate redisTemplate;
/**
* 检查并设置幂等性键
*/
public boolean checkAndSet(String key, long expireTime) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "processed", expireTime, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(result);
}
/**
* 生成幂等性键
*/
public String generateKey(String prefix, String... params) {
StringBuilder sb = new StringBuilder(prefix);
for (String param : params) {
sb.append(":").append(param);
}
return sb.toString();
}
}
Service层实现:
@Service
public class OrderService {
@Autowired
private RedisIdempotentService redisIdempotentService;
@Autowired
private OrderMapper orderMapper;
public Order createOrder(OrderCreateRequest request) {
// 生成幂等性键
String key = redisIdempotentService.generateKey("order_create",
request.getUserId(),
request.getProductId());
// 检查是否已处理
if (!redisIdempotentService.checkAndSet(key, 30000)) {
throw new BusinessException("订单已处理,请不要重复提交");
}
// 创建订单逻辑
Order order = new Order();
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setAmount(request.getAmount());
order.setStatus(0);
order.setCreateTime(new Date());
orderMapper.insert(order);
return order;
}
}
优劣势分析
优点:
缺点:
适用场景
实现原理
乐观锁通常依赖版本号(version字段)或时间戳。每次更新时检查版本号是否与之前读取的一致,一致则更新并将版本号加1,不一致则说明数据已被其他请求修改,更新失败。这种方式避免了锁竞争,但需要业务方处理重试。
核心源码
数据库表设计:
CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '产品名称', `stock` int(11) NOT NULL COMMENT '库存', `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号', `update_time` datetime NOT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品表';
Service层实现:
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
@Transactional
public boolean deductStock(Long productId, int quantity) {
// 获取产品信息
Product product = productMapper.selectById(productId);
if (product == null) {
throw new BusinessException("产品不存在");
}
// 检查库存
if (product.getStock() < quantity) {
throw new BusinessException("库存不足");
}
// 更新库存,使用乐观锁
int result = productMapper.updateStock(productId, quantity, product.getVersion());
if (result == 0) {
// 更新失败,说明版本号已变化
throw new BusinessException("库存更新失败,请重试");
}
return true;
}
}
Mapper层实现:
public interface ProductMapper {
@Update("UPDATE product SET stock = stock - #{quantity}, version = version + 1, update_time = NOW() WHERE id = #{productId} AND version = #{version}")
int updateStock(@Param("productId") Long productId, @Param("quantity") int quantity, @Param("version") int version);
}
优劣势分析
优点:
缺点:
适用场景
| 方案 | 实现复杂度 | 性能 | 可靠性 | 适用场景 | 依赖 |
|---|---|---|---|---|---|
| 基于数据库唯一索引 | 低 | 中 | 高 | 插入操作 | 数据库 |
| 基于Token令牌 | 中 | 高 | 高 | 各种操作 | Redis |
| 基于分布式锁 | 高 | 中 | 高 | 并发操作 | Redis/Zookeeper |
| 基于Redis实现 | 中 | 高 | 高 | 各种操作 | Redis |
| 基于乐观锁 | 中 | 高 | 中 | 更新操作 | 数据库 |
不同场景适合不同的方案,以下是一些常用的选型建议:
在实际项目中,通常不会只使用单一方案,而是组合运用。例如以下支付接口,同时使用了Token令牌与分布式锁,形成双重保障:
@RestController
@RequestMapping("/api/payment")
public class PaymentController {
@Autowired
private IdempotentTokenService idempotentTokenService;
@Autowired
private PaymentService paymentService;
@GetMapping("/token")
public ResponseEntity getToken() {
String token = idempotentTokenService.generateToken();
return ResponseEntity.ok(token);
}
@PostMapping
public ResponseEntity processPayment(@RequestHeader("X-Idempotent-Token") String token,
@RequestBody PaymentRequest request) {
// 1. 验证Token
if (!idempotentTokenService.validateToken(token)) {
return ResponseEntity.badRequest().body(null);
}
// 2. 处理支付
Payment payment = paymentService.processPayment(request);
return ResponseEntity.ok(payment);
}
}
@Service
public class PaymentService {
@Autowired
private DistributedLockService distributedLockService;
@Autowired
private PaymentMapper paymentMapper;
public Payment processPayment(PaymentRequest request) {
String lockKey = "payment_lock:" + request.getOrderNo();
try {
// 3. 获取分布式锁
if (!distributedLockService.tryLock(lockKey, 30000)) {
throw new BusinessException("支付处理中,请稍后再试");
}
// 4. 检查是否已支付
Payment existingPayment = paymentMapper.selectByOrderNo(request.getOrderNo());
if (existingPayment != null && existingPayment.getStatus() == 1) {
return existingPayment;
}
// 5. 处理支付逻辑
Payment payment = new Payment();
payment.setOrderNo(request.getOrderNo());
payment.setAmount(request.getAmount());
payment.setStatus(1);
payment.setCreateTime(new Date());
paymentMapper.insert(payment);
return payment;
} finally {
// 6. 释放锁
distributedLockService.releaseLock(lockKey);
}
}
}
幂等性在API设计中的定位并非锦上添花,而是必需品。它直接关系到系统的可靠性和数据一致性,尤其在分布式、高并发的场景下,一旦缺失,可能导致数据混乱、用户投诉甚至资金损失。
在SpringBoot应用中,可以通过多种方式实现接口幂等性,从简单的数据库唯一索引,到灵活的Token令牌,再到高并发的Redis方案和分布式锁,以及无锁的乐观锁。每种方案都有各自的优缺点和适用场景,不存在万能方案。
在实际应用中,应根据具体的业务场景选择合适的实现方案,也可以结合多种方案来提高系统的可靠性和性能。例如在综合示例中,Token负责防重放,分布式锁负责防并发,两者配合,几乎能够拦截所有重复请求。
通过合理的幂等性设计,可以有效防止重复请求导致的问题,使系统更加稳定,用户体验也更加流畅。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述