后端系统经常需要对接第三方 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) .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); factory.setReadTimeout(30000);
RestTemplate template = new RestTemplate(factory);
template.setErrorHandler(new DefaultResponseErrorHandler() { @Override public boolean hasError(ClientHttpResponse response) { 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(); } }
|
重试策略设计
重试的原则
- 幂等操作才能重试:GET 查询可以重试,POST 创建需要谨慎
- 指数退避:避免惊群效应
- 最大次数限制:防止无限重试
- 只重试临时错误: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); backOff.setMultiplier(2.0); backOff.setMaxInterval(30000); 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) { 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, status TINYINT NOT NULL DEFAULT 0, 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) { 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("请求正在处理中"); } }
keyMapper.insert(IdempotentKey.builder() .bizKey(orderNo) .status(0) .build());
try { String result = doProcess(orderNo);
keyMapper.updateStatus(orderNo, 1, result);
return Result.ok(result); } catch (Exception e) { 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;
String result = redisTemplate.opsForValue().get(key); if (result != null) { return result; }
Boolean set = redisTemplate.opsForValue() .setIfAbsent(key, "PROCESSING", Duration.ofSeconds(30));
if (!Boolean.TRUE.equals(set)) { throw new BusinessException("请求正在处理中"); }
try { result = action.get();
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();
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("订单已标记为失败"); } }
if (!verifySign(request)) { throw new BusinessException("签名验证失败"); }
idempotentService.executeWithIdempotency("payment:" + orderNo, () -> { doProcessPayment(orderNo, transactionId, request.getAmount()); return null; });
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) {
String sign = request.getSign(); String computed = computeSign(request);
return sign != null && sign.equals(computed); }
private boolean checkTimestamp(String timestamp) { long requestTime = Long.parseLong(timestamp); long now = System.currentTimeMillis() / 1000;
return Math.abs(now - requestTime) < 300; } }
|
总结
第三方 API 对接核心要点:
| 方面 |
方案 |
| 超时配置 |
ConnectTimeout 5s,ReadTimeout 30s |
| 重试策略 |
指数退避、只重试 5xx、max 3 次 |
| 幂等设计 |
去重表 / Redis / Token |
| 支付回调 |
先查状态、再验证签名、最后处理 |
安全注意事项:
- 签名验证:回调必须验证签名
- 时间戳检查:防止 replay attack
- IP 白名单:限制回调来源
- 日志审计:记录所有回调请求