132 lines
6.5 KiB
Markdown
132 lines
6.5 KiB
Markdown
# 05. 集成层设计(F6 / Mini 域)
|
||||
|
|
|
|||
|
|
## 决策
|
|||
|
|
|
|||
|
|
供应商(F6)和历史 Mini 域的调用统一收口在 `f6-integration` / `mini-clients` 模块,业务 domain 不直接持有 `WebClient` 或 HTTP 客户端;用 Resilience4j 统一管理超时、重试、熔断。
|
|||
|
|
|
|||
|
|
## 结构约定
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
platform-integration/
|
|||
|
|
WebClientConfig # 统一封装 WebClient(连接池、超时基线配置)
|
|||
|
|
Resilience4jDefaults # 超时/重试/熔断的公共默认配置
|
|||
|
|
|
|||
|
|
domains/f6-integration/
|
|||
|
|
负责:换票、供应商访问上下文准备、超时/重试/熔断策略、异常转换为内部标准错误码
|
|||
|
|
|
|||
|
|
domains/mini-clients/
|
|||
|
|
对 O2O/Warranty/Retail Store/ROOS 的只读客户端封装,供 workbench / bff-orchestration 调用
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## `WebClient` + Resilience4j 配置示例
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# application.yml
|
|||
|
|
resilience4j:
|
|||
|
|
timelimiter:
|
|||
|
|
instances:
|
|||
|
|
f6-api:
|
|||
|
|
timeout-duration: 2s
|
|||
|
|
mini-o2o:
|
|||
|
|
timeout-duration: 1s
|
|||
|
|
retry:
|
|||
|
|
instances:
|
|||
|
|
f6-api:
|
|||
|
|
max-attempts: 2
|
|||
|
|
wait-duration: 200ms
|
|||
|
|
retry-exceptions:
|
|||
|
|
- java.net.SocketTimeoutException
|
|||
|
|
- org.springframework.web.reactive.function.client.WebClientRequestException
|
|||
|
|
circuitbreaker:
|
|||
|
|
instances:
|
|||
|
|
f6-api:
|
|||
|
|
sliding-window-size: 20
|
|||
|
|
failure-rate-threshold: 50
|
|||
|
|
wait-duration-in-open-state: 10s
|
|||
|
|
permitted-number-of-calls-in-half-open-state: 5
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```kotlin
|
|||
|
|
// domains/f6-integration/.../F6ApiClient.kt
|
|||
|
|
@Component
|
|||
|
|
class F6ApiClient(
|
|||
|
|
private val webClient: WebClient, // 来自 platform-integration 的统一封装
|
|||
|
|
) {
|
|||
|
|
@CircuitBreaker(name = "f6-api", fallbackMethod = "fallbackProcurementList")
|
|||
|
|
@Retry(name = "f6-api")
|
|||
|
|
@TimeLimiter(name = "f6-api")
|
|||
|
|
fun fetchProcurementList(storeId: Long): Mono<ProcurementListResponse> =
|
|||
|
|
webClient.get()
|
|||
|
|
.uri("/f6/procurement/list?storeId={storeId}", storeId)
|
|||
|
|
.retrieve()
|
|||
|
|
.onStatus({ it.isError }) { resp ->
|
|||
|
|
resp.bodyToMono(String::class.java)
|
|||
|
|
.map { body -> F6IntegrationException("F6 采购列表调用失败: ${resp.statusCode()} $body") }
|
|||
|
|
}
|
|||
|
|
.bodyToMono(ProcurementListResponse::class.java)
|
|||
|
|
|
|||
|
|
// Resilience4j 约定:fallback 方法签名 = 原方法参数 + Throwable,返回类型一致
|
|||
|
|
fun fallbackProcurementList(storeId: Long, ex: Throwable): Mono<ProcurementListResponse> =
|
|||
|
|
Mono.just(ProcurementListResponse.degraded())
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```kotlin
|
|||
|
|
// 统一异常转换:F6IntegrationException -> 内部标准错误码,业务层不感知供应商原始协议
|
|||
|
|
class F6IntegrationException(message: String) : RuntimeException(message)
|
|||
|
|
|
|||
|
|
@RestControllerAdvice
|
|||
|
|
class F6ExceptionHandler {
|
|||
|
|
@ExceptionHandler(F6IntegrationException::class)
|
|||
|
|
fun handle(ex: F6IntegrationException): ResponseEntity<ApiResult<Nothing>> =
|
|||
|
|
ResponseEntity.status(HttpStatus.BAD_GATEWAY)
|
|||
|
|
.body(ApiResult.error(code = "F6_UNAVAILABLE", message = "供应商服务暂不可用,请稍后重试"))
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Mini 域客户端示例(内部系统,策略更宽松)
|
|||
|
|
|
|||
|
|
```kotlin
|
|||
|
|
// domains/mini-clients/.../O2OClient.kt
|
|||
|
|
@Component
|
|||
|
|
class O2OClient(private val webClient: WebClient) {
|
|||
|
|
|
|||
|
|
@TimeLimiter(name = "mini-o2o") // 只兜底超时,不需要熔断(内部系统,稳定性相对可控)
|
|||
|
|
fun fetchOrderSummary(storeId: Long): Mono<OrderSummary> =
|
|||
|
|
webClient.get()
|
|||
|
|
.uri("/o2o/orders/summary?storeId={storeId}", storeId)
|
|||
|
|
.retrieve()
|
|||
|
|
.bodyToMono(OrderSummary::class.java)
|
|||
|
|
.onErrorResume { Mono.just(OrderSummary.empty()) } // 局部降级,见 workbench 聚合规则
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 关键规则
|
|||
|
|
|
|||
|
|
- **F6 是外部供应商域**,稳定性不可控,必须配置超时 + 重试 + 熔断,且熔断后要有降级返回(`fallbackXxx` 方法),不能让异常直接穿透到 APP。
|
|||
|
|
- **异常统一转换**:F6 / Mini 域返回的异常或非标准错误,在 `f6-integration` / `mini-clients` 内部转换成内部标准错误码(如 `F6_UNAVAILABLE`),业务 domain 和最终 API 响应都不暴露供应商侧的原始协议细节。
|
|||
|
|
- **Mini 域调用相对可控**(内部系统),熔断策略可以比 F6 宽松(示例里只加超时兜底),但仍需要超时兜底,避免慢查询拖垮 `workbench` 聚合——对应架构图 Flow 3 "首页失败按 tile 降级"的要求。
|
|||
|
|
- 业务 domain(如 `workbench`)只依赖 `mini-clients` / `f6-integration` 暴露的接口,不自己 `new WebClient` 发请求。
|
|||
|
|
|
|||
|
|
## 附录:超时、重试、熔断分别解决什么问题
|
|||
|
|
|
|||
|
|
三者经常被一起提,但作用点不同,配置的时候容易搞混:
|
|||
|
|
|
|||
|
|
- **超时(Timeout)**:解决"对方一直不回应,我方请求线程/连接被一直占着"的问题。没有超时,一个慢下游能拖垮整个调用方的线程池。这是三者里最基础、必须有的一道防线。
|
|||
|
|
- **重试(Retry)**:解决"这次失败大概率是偶发的(网络抖动、瞬时过载)"的问题。重试的前提是**幂等**——`fetchProcurementList` 这种 GET 查询可以放心重试,但如果是"扣库存""创建订单"这类有副作用的调用,重试前要先确认接口本身幂等(比如带幂等 key),否则重试可能造成重复下单这类更严重的问题。
|
|||
|
|
- **熔断(Circuit Breaker)**:解决"对方已经持续故障,继续重试只是在浪费资源、拖慢自己"的问题。熔断器统计一个滑动窗口内的失败率,超过阈值后直接短路请求(进入 `OPEN` 状态,一段时间内不再真的发请求,直接走 fallback),过一段时间放几个探测请求(`HALF_OPEN`)判断对方是否恢复。
|
|||
|
|
|
|||
|
|
三者组合的顺序也有讲究:一次调用先看熔断器状态(`OPEN` 直接 fallback,不发请求)→ 没熔断就真的发请求 → 超时控制这次请求最多等多久 → 超时或失败了再看要不要重试。上面 Resilience4j 的注解顺序(`@CircuitBreaker` 在最外层,`@Retry`、`@TimeLimiter`在内层)就是按这个语义叠加的。
|
|||
|
|
|
|||
|
|
## 待补充
|
|||
|
|
|
|||
|
|
- 具体超时/重试参数需要结合 F6 实际 SLA 压测后调整,示例里的数值是起点,不是最终值。
|
|||
|
|
- 熔断后降级返回的数据结构约定(`degraded()` 具体字段)。
|
|||
|
|
- F6 换票具体协议细节(对接 [webview-ticket](./04-security-auth.md) 的会话失效联动)。
|
|||
|
|
|
|||
|
|
## 参考链接
|
|||
|
|
|
|||
|
|
- [Resilience4j 官方文档](https://resilience4j.readme.io/docs)
|
|||
|
|
- [Spring WebFlux WebClient](https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html)
|
|||
|
|
- [Martin Fowler: CircuitBreaker](https://martinfowler.com/bliki/CircuitBreaker.html)
|