API幂等性相关实现方式

在支付API的设计与实现中,幂等性是保障数据一致性和交易正确性的关键特性。以下是实现支付API幂等性的核心方法及最佳实践:

1. 唯一标识(IDempotent Key)机制

2. 状态机设计

  • 核心原则:将支付流程抽象为状态机(如:待支付→处理中→成功/失败),同一状态的重复请求不改变结果。
  • 关键实现
    • 使用数据库字段记录交易状态。
    • 通过乐观锁(如版本号)防止并发冲突。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 支付订单表结构示例
CREATE TABLE payment_orders (
id VARCHAR(36) PRIMARY KEY, -- 订单ID(幂等键)
status ENUM('PENDING', 'PROCESSING', 'SUCCESS', 'FAILED') NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
version INT DEFAULT 0 -- 乐观锁版本号
);

-- 更新状态的SQL(使用乐观锁)
UPDATE payment_orders
SET status = 'SUCCESS', version = version + 1
WHERE id = '${idempotency_key}'
AND status = 'PROCESSING'
AND version = ${current_version};

3. 分布式锁与缓存结合

  • 适用场景:高并发场景下防止重复处理。
  • 实现方式
    1. 使用Redis或ZooKeeper实现分布式锁。
    2. 缓存已处理的请求结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 示例:Spring Boot中使用Redis分布式锁
@Service
public class PaymentService {

@Autowired
private RedisTemplate<String, String> redisTemplate;

public PaymentResult processPayment(String idempotencyKey, PaymentRequest request) {
// 尝试获取分布式锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
"lock:payment:" + idempotencyKey, "locked", 30, TimeUnit.SECONDS);

if (!locked) {
// 未获取到锁,可能有其他请求正在处理
return getCachedResult(idempotencyKey);
}

try {
// 检查是否已处理
PaymentResult cachedResult = getCachedResult(idempotencyKey);
if (cachedResult != null) {
return cachedResult;
}

// 处理支付逻辑
PaymentResult result = executePayment(request);
// 缓存结果
cacheResult(idempotencyKey, result);
return result;
} finally {
// 释放锁
redisTemplate.delete("lock:payment:" + idempotencyKey);
}
}
}

4. 幂等性中间件

  • 推荐方案:使用开源中间件简化实现,如:
    • Resilience4j(Java):提供幂等性拦截器。
    • Spring Retry:结合唯一标识实现重试幂等。

5. 客户端最佳实践

  • 生成可靠的唯一ID:使用UUID或业务相关的唯一标识。
  • 持久化唯一ID:将ID与请求参数一起存储,确保重试时携带相同ID。
  • 处理超时策略:设置合理的超时时间,避免过早重试。

6. 测试与验证

  • 编写幂等性测试用例:模拟重复请求,验证结果一致性。
  • 压力测试:在高并发场景下验证幂等性保障机制的有效性。

注意事项

  • 幂等性范围:仅保证同一请求的多次调用结果一致,不处理业务逻辑变更。
  • 数据有效期:缓存结果需设置合理的过期时间(建议24小时)。
  • 异常处理:处理部分成功的边界情况(如支付已扣款但状态未更新)。

通过以上方案,可有效实现支付API的幂等性,确保交易在分布式环境下的一致性和可靠性。

应用示例

  • PaymentStatus.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.example.payment;

/**
* 支付订单状态枚举
*/
public enum PaymentStatus {
/**
* 待支付:订单已创建,但尚未开始支付流程
*/
PENDING("待支付"),

/**
* 处理中:已提交支付请求,正在等待支付结果
*/
PROCESSING("处理中"),

/**
* 支付成功:支付流程已完成,资金已到账
*/
SUCCESS("支付成功"),

/**
* 支付失败:支付过程中出现错误导致失败
*/
FAILED("支付失败"),

/**
* 已取消:用户主动取消支付或订单已关闭
*/
CANCELED("已取消"),

/**
* 已退款:订单已全额退款
*/
REFUNDED("已退款"),

/**
* 部分退款:订单已部分退款
*/
PARTIAL_REFUNDED("部分退款");

private final String displayName;

PaymentStatus(String displayName) {
this.displayName = displayName;
}

public String getDisplayName() {
return displayName;
}
}
  • PaymentOrder.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.example.payment;

import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Entity
@Table(name = "payment_orders")
public class PaymentOrder {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String idempotencyKey;

@Column(nullable = false)
private BigDecimal amount;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private PaymentStatus status;

@Column
private String transactionId;

@Column
private LocalDateTime createTime;

@Column
private LocalDateTime updateTime;

@Version
private Integer version;

public boolean isFinalState() {
return status == PaymentStatus.SUCCESS || status == PaymentStatus.FAILED;
}

// getters and setters
}
  • PaymentRepository.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.payment;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import javax.persistence.LockModeType;
import java.util.Optional;

@Repository
public interface PaymentRepository extends JpaRepository<PaymentOrder, Long> {

/**
* 根据幂等键查找订单
*/
Optional<PaymentOrder> findByIdempotencyKey(String idempotencyKey);

/**
* 使用悲观锁查找订单
*/
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM PaymentOrder p WHERE p.idempotencyKey = :key")
Optional<PaymentOrder> findByIdempotencyKeyWithLock(@Param("key") String idempotencyKey);

/**
* 更新订单状态(使用乐观锁)
*/
@Modifying
@Query("UPDATE PaymentOrder p SET p.status = :status, p.updateTime = CURRENT_TIMESTAMP, p.version = p.version + 1 " +
"WHERE p.id = :id AND p.status = :currentStatus AND p.version = :version")
int updateStatus(@Param("id") Long id,
@Param("status") PaymentStatus newStatus,
@Param("currentStatus") PaymentStatus currentStatus,
@Param("version") Integer version);
}
  • IdempotencyService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package com.example.payment;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

@Service
public class IdempotencyService {

private static final String IDEMPOTENCY_KEY_PREFIX = "idempotency:";
private static final String LOCK_KEY_PREFIX = "lock:";
private static final long LOCK_EXPIRE_TIME = 30;
private static final long RESULT_CACHE_TIME = 86400;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private PaymentRepository paymentRepository;

/**
* 执行具有幂等性保障的支付操作
* @param idempotencyKey 幂等键
* @param action 实际支付操作
* @return 支付结果
*/
public <T> T execute(String idempotencyKey, Supplier<T> action) {
// 检查是否已有缓存结果
T cachedResult = getCachedResult(idempotencyKey);
if (cachedResult != null) {
return cachedResult;
}

// 获取分布式锁,防止并发处理相同请求
boolean locked = acquireLock(idempotencyKey);
if (!locked) {
// 等待锁释放并重试
waitForLockRelease(idempotencyKey);
return execute(idempotencyKey, action);
}

try {
// 再次检查缓存,避免在等待锁期间其他请求已处理
cachedResult = getCachedResult(idempotencyKey);
if (cachedResult != null) {
return cachedResult;
}

// 检查数据库中是否已有该幂等键的记录
PaymentOrder existingOrder = paymentRepository.findByIdempotencyKey(idempotencyKey);
if (existingOrder != null && existingOrder.isFinalState()) {
T result = (T) mapOrderToResult(existingOrder);
cacheResult(idempotencyKey, result);
return result;
}

// 执行实际支付操作
T result = action.get();

// 缓存结果并保存到数据库
cacheResult(idempotencyKey, result);
saveOrder(idempotencyKey, result);

return result;
} finally {
// 释放锁
releaseLock(idempotencyKey);
}
}

private boolean acquireLock(String idempotencyKey) {
String lockKey = LOCK_KEY_PREFIX + idempotencyKey;
return redisTemplate.opsForValue()
.setIfAbsent(lockKey, "locked", LOCK_EXPIRE_TIME, TimeUnit.SECONDS);
}

private void releaseLock(String idempotencyKey) {
String lockKey = LOCK_KEY_PREFIX + idempotencyKey;
redisTemplate.delete(lockKey);
}

private <T> T getCachedResult(String idempotencyKey) {
String cacheKey = IDEMPOTENCY_KEY_PREFIX + idempotencyKey;
return (T) redisTemplate.opsForValue().get(cacheKey);
}

private <T> void cacheResult(String idempotencyKey, T result) {
String cacheKey = IDEMPOTENCY_KEY_PREFIX + idempotencyKey;
redisTemplate.opsForValue().set(cacheKey, result, RESULT_CACHE_TIME, TimeUnit.SECONDS);
}

private void waitForLockRelease(String idempotencyKey) {
try {
Thread.sleep(100); // 简单等待策略,实际应使用更优雅的重试机制
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

private void saveOrder(String idempotencyKey, Object result) {
// 根据支付结果创建或更新订单
// 实际实现中应根据业务需求进行处理
}

private Object mapOrderToResult(PaymentOrder order) {
// 将订单实体映射为结果对象
// 实际实现中应根据业务需求进行处理
return null;
}
}
  • PaymentService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.example.payment;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

@Autowired
private IdempotencyService idempotencyService;

public PaymentResult processPayment(String idempotencyKey, PaymentRequest request) {
return idempotencyService.execute(idempotencyKey, () -> {
// 实际支付处理逻辑
// 1. 验证请求参数
validateRequest(request);

// 2. 创建支付订单
PaymentOrder order = createOrder(request);

// 3. 调用支付渠道
PaymentResult result = callPaymentProvider(order);

// 4. 更新订单状态
updateOrderStatus(order, result);

return result;
});
}

private void validateRequest(PaymentRequest request) {
// 参数验证逻辑
}

private PaymentOrder createOrder(PaymentRequest request) {
// 创建订单逻辑
return null;
}

private PaymentResult callPaymentProvider(PaymentOrder order) {
// 调用支付渠道逻辑
return null;
}

private void updateOrderStatus(PaymentOrder order, PaymentResult result) {
// 更新订单状态逻辑
}
}
  • PaymentController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.payment;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

@RestController
@RequestMapping("/api/payments")
public class PaymentController {

@Autowired
private PaymentService paymentService;

@PostMapping
public PaymentResult createPayment(
@RequestHeader("X-Idempotency-Key") String idempotencyKey,
@RequestBody PaymentRequest request) {

// 生成幂等键(实际项目中应由客户端生成并传递)
if (idempotencyKey == null || idempotencyKey.isEmpty()) {
idempotencyKey = UUID.randomUUID().toString();
}

return paymentService.processPayment(idempotencyKey, request);
}
}
  • IdempotencyServiceTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.example.payment;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class IdempotencyServiceTest {

@Mock
private RedisTemplate<String, Object> redisTemplate;

@Mock
private PaymentRepository paymentRepository;

@InjectMocks
private IdempotencyService idempotencyService;

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}

@Test
void execute_shouldOnlyExecuteOnce_whenCalledConcurrently() throws Exception {
String idempotencyKey = "test-key";
AtomicInteger counter = new AtomicInteger(0);

// 模拟Redis锁操作
when(redisTemplate.opsForValue().setIfAbsent(anyString(), any(), anyLong(), any()))
.thenReturn(true).thenReturn(false); // 第一次获取锁成功,后续失败

// 模拟无缓存结果
when(redisTemplate.opsForValue().get(anyString())).thenReturn(null);

// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(5);

// 并发执行10次
CompletableFuture<?>[] futures = new CompletableFuture[10];
for (int i = 0; i < 10; i++) {
futures[i] = CompletableFuture.runAsync(() -> {
idempotencyService.execute(idempotencyKey, counter::incrementAndGet);
}, executor);
}

// 等待所有任务完成
CompletableFuture.allOf(futures).join();

// 验证只执行了一次
assertEquals(1, counter.get());
}
}