feat: add documentation for cross-domain collaboration and aggregation

- Introduced a new section on cross-domain collaboration and aggregation, detailing decision-making processes, contract module usage for cross-domain reads, and domain events for writes.
- Added guidelines for parallel aggregation using a dedicated thread pool and context propagation.
- Established rules for transaction boundaries, idempotency, optimistic locking, scheduled tasks, and caching strategies in a concurrent environment.
- Included examples and best practices for implementing these concepts in the application.
This commit is contained in:
Guangfei.Zhao
2026-08-14 16:03:47 +08:00
parent 444db49818
commit 74b02ed427
13 changed files with 2688 additions and 474 deletions
+70 -8
View File
@@ -70,7 +70,8 @@ spec:
## 启用 Spring Cloud Kubernetes 配置热更新
```groovy
// bootstrap/build.gradle
// bootstrap/build.gradle —— 版本由根工程的 spring-cloud-dependencies BOM2025.1.2)统一管理,
// 见 01-project-structure.md 的版本基线表
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-kubernetes-client-config'
}
@@ -87,7 +88,8 @@ spring:
- name: conti-backend-config
reload:
enabled: true
mode: polling # 定期轮询 ConfigMap 变化,无需重启 Pod
mode: polling # 怎么发现变化:定期轮询 ConfigMap(另一个选项 event 需要 watch 权限)
strategy: refresh # 发现变化后做什么:只刷新 @RefreshScope bean,不重启容器
period: 15s
```
@@ -101,6 +103,20 @@ class WorkbenchProperties {
}
```
**`mode``strategy` 是两件事,别混**`mode` 决定怎么感知 ConfigMap 变化,`strategy` 决定感知到之后做什么。只配 `mode` 而漏掉 `strategy`,行为会依赖框架默认值。
**并不是所有配置都能热更新**,这一点要在改配置之前想清楚,否则会出现"改了 ConfigMap、观察半天没生效"的困惑:
| 配置 | 能否热更新 | 说明 |
| --- | --- | --- |
| 自定义的 `@RefreshScope` + `@ConfigurationProperties`(如 `workbench.*`) | ✅ | 这是热更新真正的适用范围 |
| 日志级别 | ✅ | 也可以直接用 `/actuator/loggers` 端点改,更直接 |
| `resilience4j.*` 的熔断/重试参数 | ⚠️ | 这些实例在启动时创建,普通 `refresh` 刷不到。真要动,用 `strategy: restart_context`(会重建应用上下文,等于一次内部重启)或者干脆走发布流程 |
| 数据源、连接池、JPA 相关 | ❌ | 走发布流程 |
| Secret 里的值 | ❌ | 见下面"关键规则"Secret 变更需要重启 Pod |
上面 ConfigMap 示例里同时放了 `resilience4j``workbench` 两段,正是为了说明这个差别:`workbench.degradeMessage` 改完 15 秒内生效,`failure-rate-threshold` 不会。
## 本地开发怎么跑(不需要真的连 K8s)
`spring-cloud-starter-kubernetes-client-config` 启动时默认会尝试读 `~/.kube/config` 或 in-cluster 凭证去调 K8s API 拿 ConfigMap,本地电脑上没有这些东西的话,启动会报错或者卡住去连一个不存在的 API Server。本地开发不需要也不应该依赖真实 K8s,两种可选方案:
@@ -117,23 +133,26 @@ spring:
reload:
enabled: false
datasource:
url: jdbc:postgresql://localhost:5432/conti_backend
url: jdbc:mysql://localhost:3306/?connectionTimeZone=UTC&preserveInstants=true&rewriteBatchedStatements=true
username: conti
password: conti_local_password # 仅本地开发用,不是真实密钥
security:
jwt:
secret: local-dev-only-secret-not-for-real-use
active-key-id: local
keys:
# 仅本地开发用的假密钥;HS256 要求 base64 解码后 ≥ 32 字节,见 04-security-auth.md
local: bG9jYWwtZGV2LW9ubHktc2VjcmV0LW5vdC1mb3ItcmVhbC11c2UtMzJi
```
```bash
# 本地起依赖(DB 等),配合 docker-compose 用
docker compose up -d postgres
docker compose up -d mysql
# 用 local profile 启动
SPRING_PROFILES_ACTIVE=local ./gradlew :bootstrap:bootRun
```
`application-local.yml` 不提交敏感真实值(本来也没有,本地密码本身就是假的),可以放进代码库方便新人直接跑起来;`local` profile 和 dev/uat/prod 的关键区别就是 `spring.cloud.kubernetes.config.enabled=false`,其余代码逻辑完全一致——这也是为什么 [06-api-design.md](./06-api-design.md) 强调的"配置外置"很重要:业务代码不知道、也不需要知道配置到底来自 K8s 还是本地文件
`application-local.yml` 不提交敏感真实值(本来也没有,本地密码本身就是假的),可以放进代码库方便新人直接跑起来;`local` profile 和 dev/uat/prod 的关键区别就是 `spring.cloud.kubernetes.config.enabled=false`,其余代码逻辑完全一致——这也正是**配置外置**的价值所在:业务代码不知道、也不需要知道配置到底来自 K8s ConfigMap 还是本地文件,换配置来源不需要改一行 Kotlin
这是**个人本机调试**用的,跟团队共享的 **Dev 环境**不是一回事——Dev 环境跑在公司内网一台 Ubuntu 服务器的 k3s 集群上,走真实的 K8s ConfigMap/Secret(跟下面"方案二"是同一套思路,只是长期跑着给团队用,而不是临时验证),团队通过公司 VPN 访问,具体见 [09-build-deploy.md](./09-build-deploy.md#环境层级local个人本机vs-dev内网-ubuntu-k3s-集群vs-uatprodazure-aks)。
@@ -232,10 +251,53 @@ spec:
两种方式**不需要同时维护**——选一个用,文档里保留方式二只是留个参考路径,不是说两者要并存。
## 配置绑定与启动期校验
**所有配置一律绑定到 `@ConfigurationProperties` 类,不散着写 `@Value`。** `@Value` 散落在各处时,没人说得清这个应用到底需要哪些配置项,漏配一个只能等到运行到那行代码才报错。
```kotlin
@ConfigurationProperties(prefix = "integration.clients.f6")
@Validated
data class F6ClientProperties(
@field:NotBlank val baseUrl: String,
@field:Min(100) @field:Max(10_000) val readTimeoutMs: Long = 2000,
@field:Min(1) val maxConcurrentCalls: Int = 20,
)
```
配上 `@Validated` 之后,配置缺失或越界会在**启动时**直接失败,Pod 起不来,K8s 的就绪探针不通过,滚动更新会自动停住并保留旧版本(见 [09-build-deploy.md](./09-build-deploy.md))。这比"启动成功了,但半夜某个接口因为超时配成 0 而全线失败"要好得多——**配错了就别起来**是这里的核心原则。
同理,[04-security-auth.md](./04-security-auth.md) 里 JWT 密钥长度的校验也放在启动期,绝不允许带着弱密钥跑起来。
## 命名规范
| 对象 | 规范 | 示例 |
| --- | --- | --- |
| namespace | `retailapp-<env>` | `retailapp-uat` |
| ConfigMap | `conti-backend-config` | 每个环境一份,同名不同 namespace |
| Secret | `conti-backend-secret` | 同上 |
| ConfigMap 里的 key | `application-<profile>.yml` | `application-uat.yml` |
| Secret 里的 key | 全大写下划线,与 Spring 的 relaxed binding 对齐 | `DB_PASSWORD``spring.datasource.password`(配合 `${DB_PASSWORD}` 引用) |
| Key Vault secret 名 | 中划线小写,与配置路径对应 | `security-jwt-secret` |
| 自定义配置前缀 | 模块名小驼峰 | `workbench.*``integration.clients.*` |
**同名不同 namespace** 这一点是有意的:环境差异全部体现在 namespace 和 ConfigMap 内容上,Deployment yaml 在各环境之间只差镜像 tag 和 namespace,减少"UAT 好好的、生产漏配了一项"这类问题。
## 数据库与外部依赖的实例形态
| 依赖 | dev(内网 k3s | UAT / ProdAzure AKS |
| --- | --- | --- |
| MySQL | 集群内一个 MySQL 容器(数据可丢,随时重建) | **Azure Database for MySQL Flexible Server**,通过 **Private Endpoint** 接入 AKS 所在 VNet,不开公网访问 |
| 密钥 | k3s Secret,值由内网 CI 注入 | Azure Key Vault + Private Endpoint(见下一节) |
| 缓存 | 无(不引入 Redis,理由见 [04-security-auth.md](./04-security-auth.md) | 同左 |
生产 MySQL 的连接信息(host/账号)走 ConfigMap + Secret 注入,应用侧配置见 [03-persistence.md](./03-persistence.md)。**连接串里必须带 `sslMode=REQUIRED`**——Flexible Server 默认要求 TLS,漏了会连不上;同时也别为了图省事把它降级成 `DISABLED`
数据库账号分两个:Flyway 迁移用的账号有 DDL 权限(只在部署流程中使用),应用运行时账号只有 DML 权限,理由见 03。
## 待补充
- 具体 ConfigMap/Secret 命名规范和 namespace 划分细节
- 多环境 profile 的详细参数列表。
- 多环境 profile 的完整参数清单(等各 domain 的配置项定下来后汇总成一张表)
## 参考链接