Java 后端对接第三方 API:超时、重试、幂等性处理

后端系统经常需要对接第三方 API:支付、短信、推送、地图等。对接过程中最头疼的问题是网络不稳定、超时、重复调用等。本文总结一套完整的对接方案。

HTTP 超时配置

常见超时类型

超时类型 说明 典型值
ConnectTimeout 建立连接超时 3-5s
ReadTimeout 读取数据超时 10-30s
WriteTimeout 写入数据超时 10s

OkHttp 配置

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
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import java.util.concurrent.TimeUnit;

public class OkHttpConfig {

public static OkHttpClient createClient() {
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);

return new OkHttpClient.Builder()
// 连接超时
.connectTimeout(5, TimeUnit.SECONDS)
// 读取超时
.readTimeout(30, TimeUnit.SECONDS)
// 写入超时
.writeTimeout(30, TimeUnit.SECONDS)
// 重试次数(默认 true)
.retryOnConnectionFailure(true)
// 添加日志拦截器
.addInterceptor(logging)
// 添加拦截器(公共参数等)
.addInterceptor(chain -> {
Request original = chain.request();
Request request = original.newBuilder()
.header("Authorization", "Bearer " + getToken())
.header("X-Request-Id", UUID.randomUUID().toString())
.method(original.method(), original.body())
.build();
return chain.proceed(request);
})
.build();
}
}

RestTemplate 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 5秒
factory.setReadTimeout(30000); // 30秒

RestTemplate template = new RestTemplate(factory);

// 添加错误处理
template.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public boolean hasError(ClientHttpResponse response) {
// 2xx 之外都算错误
return response.getStatusCode().value() >= 300;
}
});

return template;
}
}

Spring WebClient 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class WebClientConfig {

@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Authorization", "Bearer xxx")
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.responseTimeout(Duration.ofSeconds(30))
))
.build();
}
}

重试策略设计

重试的原则

  1. 幂等操作才能重试:GET 查询可以重试,POST 创建需要谨慎
  2. 指数退避:避免惊群效应
  3. 最大次数限制:防止无限重试
  4. 只重试临时错误:Timeout、ServiceUnavailable、GatewayTimeout

5xx 错误 vs 4xx 错误

错误类型 是否重试 原因
5xx 服务器错误 重试 服务端问题,可能临时
400 Bad Request 不重试 请求本身有问题
401 Unauthorized 不重试 认证失败,重试也没用
404 Not Found 不重试 资源不存在
429 Rate Limit 重试(带退避) 限流,等待后重试

Spring Retry

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@EnableRetry
public class RetryConfig {

@Bean
public RetryTemplate retryTemplate() {
RetryTemplate template = new RetryTemplate();

// 指数退避策略
ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy();
backOff.setInitialInterval(1000); // 初始间隔 1 秒
backOff.setMultiplier(2.0); // 每次翻倍
backOff.setMaxInterval(30000); // 最大间隔 30 秒
template.setBackOffPolicy(backOff);

// 重试次数
SimpleRetryPolicy retry = new SimpleRetryPolicy();
retry.setMaxAttempts(3);
template.setRetryPolicy(retry);

return template;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class ThirdPartyService {

@Autowired
private RetryTemplate retryTemplate;

public String callApi(String param) {
return retryTemplate.execute(context -> {
// 获取重试次数
int attempt = context.getRetryCount();
log.info("第 {} 次尝试调用", attempt + 1);

try {
return httpClient.post(param);
} catch (ServiceUnavailableException e) {
// 只有 5xx 错误才重试
throw e; // 继续重试
}
});
}
}

注解方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class ThirdPartyService {

@Retryable(
value = {ServiceUnavailableException.class, TimeoutException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 30000)
)
public String callApi(String param) {
return httpClient.post(param);
}

// 恢复方法
@Recover
public String recover(ServiceUnavailableException e, String param) {
log.error("重试 3 次后仍然失败: {}", param, e);
return fallbackMethod(param);
}
}

幂等性设计原则

什么是幂等性

幂等:同一操作执行一次和执行多次,效果相同。

操作 幂等性 原因
GET 幂等 只读取
PUT 幂等 更新相同资源
DELETE 幂等 删除一次或多次结果相同
POST 不幂等 每次创建新资源

常见幂等方案

1. 去重表(最常用)

1
2
3
4
5
6
7
8
9
CREATE TABLE idempotent_keys (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_key VARCHAR(64) NOT NULL, -- 业务 key,如 order_no
status TINYINT NOT NULL DEFAULT 0, -- 0:处理中, 1:成功, 2:失败
result TEXT, -- 结果
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_biz_key (biz_key)
);
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
@Service
public class IdempotentService {

@Autowired
private IdempotentKeyMapper keyMapper;

@Transactional
public Result<String> processWithIdempotency(String orderNo) {
// 1. 检查是否已处理
IdempotentKey record = keyMapper.selectByBizKey(orderNo);

if (record != null) {
if (record.getStatus() == 1) {
// 已成功,直接返回结果
return Result.ok(record.getResult());
} else if (record.getStatus() == 0) {
// 处理中,防止重复
throw new BusinessException("请求正在处理中");
}
}

// 2. 记录处理中
keyMapper.insert(IdempotentKey.builder()
.bizKey(orderNo)
.status(0)
.build());

try {
// 3. 执行业务
String result = doProcess(orderNo);

// 4. 更新为成功
keyMapper.updateStatus(orderNo, 1, result);

return Result.ok(result);
} catch (Exception e) {
// 5. 更新为失败
keyMapper.updateStatus(orderNo, 2, null);
throw e;
}
}
}

2. Redis 幂等

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
@Service
public class IdempotentService {

@Autowired
private StringRedisTemplate redisTemplate;

private static final String IDEMPOTENT_PREFIX = "idempotent:";
private static final Duration EXPIRE_TIME = Duration.ofHours(24);

public String executeWithIdempotency(String bizKey, Supplier<String> action) {
String key = IDEMPOTENT_PREFIX + bizKey;

// 1. 检查是否已存在
String result = redisTemplate.opsForValue().get(key);
if (result != null) {
return result;
}

// 2. 设置处理中标记(防止重复执行)
Boolean set = redisTemplate.opsForValue()
.setIfAbsent(key, "PROCESSING", Duration.ofSeconds(30));

if (!Boolean.TRUE.equals(set)) {
throw new BusinessException("请求正在处理中");
}

try {
// 3. 执行
result = action.get();

// 4. 设置结果
redisTemplate.opsForValue().set(key, result, EXPIRE_TIME);

return result;
} catch (Exception e) {
// 删除处理中标记,允许重试
redisTemplate.delete(key);
throw e;
}
}
}

3. Token 机制(防重复提交)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
public class SubmitController {

@Autowired
private IdempotentService idempotentService;

@PostMapping("/api/submit")
public Result<String> submit(@RequestBody SubmitRequest request,
@RequestHeader(value = "X-Idempotency-Key", required = false) String key) {
if (key == null) {
return Result.error("缺少幂等 key");
}

return idempotentService.executeWithIdempotency(key,
() -> doSubmit(request));
}

private String doSubmit(SubmitRequest request) {
// 实际处理逻辑
return "success";
}
}

实战:支付回调处理

支付回调是最典型的幂等处理场景。

问题分析

1
2
3
4
5
6
7
8
9
10
11
第三方支付系统

│ 回调通知

┌─────────────────────────────────────┐
│ 1. 网络问题,首次回调失败 │
│ 2. 第三方重试,再次回调 │
│ 3. 业务处理成功,但返回失败 │
│ 4. 第三方再次重试 │
│ 5. 重复扣款! │
└─────────────────────────────────────┘

解决方案

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
@Service
public class PaymentCallbackService {

@Autowired
private PaymentMapper paymentMapper;

@Autowired
private IdempotentService idempotentService;

@Transactional
public void handleCallback(PaymentCallbackRequest request) {
String orderNo = request.getOrderNo();
String transactionId = request.getTransactionId();

// 1. 幂等检查
Payment payment = paymentMapper.selectByOrderNo(orderNo);

if (payment != null) {
// 已处理过,检查状态
if ("SUCCESS".equals(payment.getStatus())) {
log.info("订单 {} 已处理,直接返回成功", orderNo);
return;
}

// 如果是失败状态的重试,需要特殊处理
if ("FAILED".equals(payment.getStatus())) {
log.warn("订单 {} 之前处理失败,不允许重试", orderNo);
throw new BusinessException("订单已标记为失败");
}
}

// 2. 验证签名
if (!verifySign(request)) {
throw new BusinessException("签名验证失败");
}

// 3. 幂等锁
idempotentService.executeWithIdempotency("payment:" + orderNo, () -> {
// 4. 执行业务(查询余额、发放商品等)
doProcessPayment(orderNo, transactionId, request.getAmount());
return null;
});

// 5. 返回成功
log.info("订单 {} 支付成功", orderNo);
}

private void doProcessPayment(String orderNo, String transactionId, BigDecimal amount) {
// 更新支付状态
Payment payment = paymentMapper.selectByOrderNo(orderNo);
if (payment == null) {
payment = new Payment();
payment.setOrderNo(orderNo);
payment.setStatus("SUCCESS");
payment.setTransactionId(transactionId);
payment.setAmount(amount);
paymentMapper.insert(payment);
} else {
payment.setStatus("SUCCESS");
payment.setTransactionId(transactionId);
paymentMapper.update(payment);
}

// 发放商品
orderService.deliverGoods(orderNo);
}
}

第三方回调安全

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
@Service
public class PaymentCallbackService {

// 签名验证
private boolean verifySign(PaymentCallbackRequest request) {
// 常用算法:MD5、RSA、HMAC
// 1. 按字典序拼接参数
// 2. 拼接密钥
// 3. 计算签名
// 4. 与请求中的签名比对

String sign = request.getSign();
String computed = computeSign(request);

return sign != null && sign.equals(computed);
}

// 防止 replay attack
private boolean checkTimestamp(String timestamp) {
long requestTime = Long.parseLong(timestamp);
long now = System.currentTimeMillis() / 1000;

// 5 分钟内有效
return Math.abs(now - requestTime) < 300;
}
}

总结

第三方 API 对接核心要点:

方面 方案
超时配置 ConnectTimeout 5s,ReadTimeout 30s
重试策略 指数退避、只重试 5xx、max 3 次
幂等设计 去重表 / Redis / Token
支付回调 先查状态、再验证签名、最后处理

安全注意事项:

  1. 签名验证:回调必须验证签名
  2. 时间戳检查:防止 replay attack
  3. IP 白名单:限制回调来源
  4. 日志审计:记录所有回调请求