版本:v9.0
日期:2026-05-15
参与方:架构师(A)、技术产品(P)、研发(D)
状态:已评审确认
阅读指引
本文档采用由粗到细、逐层展开的方式组织:
| 层次 | 章节 | 读者 | 回答的问题 |
|---|---|---|---|
| 第一层 | 1-2 | 所有人 | 系统要解决什么问题,核心约束是什么 |
| 第二层 | 3-4 | 架构师、TL | 整体如何组织,存储怎么分层 |
| 第三层 | 5-6 | 架构师、研发 | 核心概念定义,业务怎么流转 |
| 第四层 | 7-9 | 研发、运维 | 具体怎么实现,怎么配置,出了问题怎么查 |
| 第五层 | 10-11 | 所有人 | 分阶段怎么落地,关键决策为什么这样做 |
目录
术语与统一语言
区域与部署
| 术语 | 说明 |
|---|---|
| Region | 地理区域,本文指 EU(欧盟)、SEA(东南亚)、US(北美)三个独立数据中心集群 |
| AZ | Availability Zone,可用区,同 Region 内物理隔离的数据中心,AZ 间延迟 < 2ms |
| 权威节点 | Authoritative Node,某类数据的唯一写入主节点,其状态代表最终事实 |
| 影子引用 | Shadow Reference,原始数据的轻量副本,仅含非敏感字段,用于跨区查询路由 |
| Control Plane | 控制平面,负责配置下发、服务注册、策略管理 |
| Data Plane | 数据平面,负责实际业务请求处理 |
| 数据主体 | Data Subject,数据所描述的自然人(即用户),GDPR 核心概念 |
| 数据主权 Region | 数据主体注册时所在 Region,决定其 PII 数据的存储归属 |
合规元数据体系
| 术语 | 说明 |
|---|---|
| 合规元数据 | Compliance Metadata,描述数据字段敏感类型、处理规则、保留期限等的结构化信息 |
| 合规元数据注册中心 | Compliance Metadata Registry,运行时维护字段合规类型的内存缓存,从 DB 加载 |
| 字段合规元数据表 | compliance_field_metadata,持久化存储所有字段合规类型的数据库表(Global DB) |
| DataType | 数据敏感类型:PII / FINANCIAL / BEHAVIORAL / PUBLIC |
| Sensitivity | 敏感等级 1-5,影响日志脱敏强度(5=完全遮盖,1=不脱敏) |
| 合规围栏 | ComplianceFence,运行时拦截数据访问、执行合规决策的 AOP 层 |
| 合规代理读 | 跨 Region 访问 PII 数据时,通过数据所在 Region 的服务代理读取,数据本身不传输 |
| 文档性注解 | @ComplianceData 注解在 v4 中仅作开发期文档提示,运行时以 DB 元数据为准 |
一致性
| 术语 | 说明 |
|---|---|
| STRONG_LOCAL | 强一致(Region 内),Seata AT + MySQL MGR 保证,分区时拒绝请求 |
| STRONG_GLOBAL | 强一致(跨 Region),Redis 原子预占 + 消息异步落库权威节点 |
| SESSION | Session 一致,Vector Clock token 保证读己之写 |
| EVENTUAL | 最终一致,Canal + Kafka 异步复制,允许短暂不同步 |
| Vector Clock | 向量时钟,追踪分布式事件因果顺序 |
| CRDT | Conflict-free Replicated Data Type,无冲突可复制数据类型 |
| OR-Set | Observed-Remove Set,CRDT 的一种,并发 add/remove 时 add 语义优先 |
事务与消息
| 术语 | 说明 |
|---|---|
| Seata AT | 自动事务模式,框架自动生成 undolog,对业务代码透明 |
| Seata TCC | Try-Confirm-Cancel,业务方手动实现三个接口,精细控制 |
| Outbox Pattern | 发件箱模式,业务写入与消息记录在同一本地事务,由 Canal CDC 异步投递 Kafka |
| CDC | Change Data Capture,变更数据捕获,Canal 监听 binlog 实现 |
| 幂等 | 同一操作执行多次与执行一次效果相同 |
| 重试 Topic 链 | Kafka 无原生重试,自建 retry..1/2/3 → dlq. 的消息重试机制 |
| DLQ | Dead Letter Queue,死信队列,消息超过最大重试次数后进入,触发 P1 告警 |
| MirrorMaker 2 | Kafka 官方跨集群复制工具,用于跨 Region Topic 同步 |
存储
| 术语 | 说明 |
|---|---|
| MGR | MySQL Group Replication,MySQL 官方高可用集群,本文用单主模式 |
| Canal | 阿里开源 MySQL binlog 订阅工具,本文承担 Outbox 投递和非 PII 数据同步 |
| Global Coordination DB | 全局协调库,MySQL MGR 部署于 SEA,存储影子引用、幂等记录、合规元数据等 |
| Regional MySQL | 区域业务库,各 Region 独立,存储本区业务数据和 PII |
| 预占 | Pre-occupy,下单时先 Redis 原子扣减库存,实际 DB 写入异步进行 |
| 水位线 | Watermark,库存剩余量告警阈值,低于此值触发动态补充 |
可观测性
| 术语 | 说明 |
|---|---|
| SkyWalking | Apache 开源 APM,Java Agent 零侵入链路追踪 |
| Span | 链路追踪最小单元,代表一次操作 |
| Trace | 由多个 Span 组成的完整请求调用链 |
| P99 延迟 | 99% 请求延迟低于此值,反映尾部延迟 |
| SLO | Service Level Objective,服务级别目标 |
| RTO | Recovery Time Objective,故障恢复时间目标 |
第一层:背景与目标
1. 业务背景
1.1 现状与问题
国际化电商平台覆盖 EU、SEA、US 三大 Region,现有单数据中心架构存在五类问题:
问题一:访问延迟高
EU / US 用户访问 SEA 数据中心,跨洲 RTT 150-300ms
每增加 100ms 延迟,转化率下降约 7%
问题二:合规风险
EU 用户 PII 数据存储在 SEA,违反 GDPR
最高罚款全球营业额 4%
问题三:可用性不足
单数据中心故障 → 全球服务中断
年故障时长约 8.7 小时(99.9%)
问题四:大促扩展瓶颈
黑五、Ramadan 等大促峰值是日常 50 倍
单中心最高承载 10x
问题五:新站点上线慢
每个新 Region 需研发介入,周期 2 周+
错过市场进入时间窗口
1.2 目标
| 指标 | 当前 | 目标 | 验收方式 |
|---|---|---|---|
| P99 访问延迟 | 350ms | < 100ms | SkyWalking |
| 服务可用性 | 99.9% | 99.99% | Prometheus |
| 合规覆盖率 | 0% | 100%(GDPR/PDPA/PIPL) | 合规审计扫描 |
| 新 Region 上线 | 14 天 | 3 天 | checklist 耗时 |
| 大促峰值承载 | 10x | 50x | 压测报告 |
| 库存超卖率 | 无监控 | < 0.01% | 对账日报 |
1.3 技术选型约束
可直接使用(团队有生产经验):
MySQL 8 · Redis Cluster · Kafka
Seata AT · Spring Boot / Spring Cloud Alibaba
主动排除(无生产经验,风险不可控):
TiDB · RocketMQ(统一到 Kafka)
新引入(门槛可控,安排专人负责):
MySQL Group Replication(MGR) — 1 人专项
Canal(binlog 订阅) — 1 人专项
MirrorMaker 2 — 与 Canal 同一人负责
SkyWalking — Java Agent,零侵入
1.4 三方核心诉求
架构师(A):核心机制强制在框架层,不依赖人的自觉。
每个组件故障都有清晰排查路径,出问题能接住。
技术产品(P):新 Region 上线 = 填写配置文件,研发不介入。
合规规则变更由合规团队自助完成,不走研发排期。
大促流程可提前预演,不出现临时决策。
研发(D):本地一条命令启动完整环境,分布式细节框架屏蔽。
查日志直接看到字段合规类型,不用翻代码。
出了线上问题有标准排查手册。
2. 核心问题与约束
2.1 三大核心矛盾
┌──────────────────────────────────────────────────────────────────┐
│ 矛盾一:合规不动 vs 数据随用户走 │
│ 法规:GDPR 要求 EU 用户 PII 必须存储在 EU 境内 │
│ 业务:用户在 SEA 下单,收货地址(PII)需要跨区查询 │
│ 取舍:数据分级 + 影子引用 + 合规代理读 │
├──────────────────────────────────────────────────────────────────┤
│ 矛盾二:强一致 vs 高可用 │
│ 强一致:支付扣款、库存扣减不能有数据丢失 │
│ 高可用:网络分区时系统仍需对外服务 │
│ 取舍:强一致边界收缩到 Region 内 │
│ 跨 Region 用 Redis 预占 + 消息最终一致 │
├──────────────────────────────────────────────────────────────────┤
│ 矛盾三:数据集中 vs 数据分散 │
│ 集中:强一致要求单一权威节点协调所有写入 │
│ 分散:低延迟要求数据就近副本 │
│ 取舍:库存单写权威节点(集中) │
│ + 各 Region Redis 预占(分散) │
│ + Canal 异步复制只读副本(分散) │
└──────────────────────────────────────────────────────────────────┘
2.2 数据分级矩阵
| 级别 | 代表字段 | 存储位置 | 一致性级别 | 跨区读策略 | 合规约束 |
|---|---|---|---|---|---|
| PII | 姓名、地址、手机 | Regional MySQL,锁定本区 | STRONG_LOCAL | 代理读,审计日志 | GDPR/PDPA/PIPL |
| FINANCIAL | 支付流水、退款 | SEA-MySQL 权威 | STRONG_GLOBAL | 直读权威节点 | 金融监管留存 5 年 |
| BEHAVIORAL | 购物车、浏览、评价 | Redis + Regional MySQL | EVENTUAL | Canal 副本直读 | 无特殊约束 |
| PUBLIC | 商品、分类、配置 | 全局同步 | EVENTUAL | 直读本地副本 | 无约束 |
2.3 合规元数据设计原则
原则一:注解只标类型,规则只在配置,元数据只在数据库
@ComplianceData(type = DataType.PII) → 文档提示,不参与运行时决策
合规规则(谁可以读、如何读)→ Apollo 配置中心
字段合规类型(哪个字段是什么类型)→ compliance_field_metadata 表
原则二:DB 是唯一权威来源
代码注解与 DB 不一致时,DB 优先
CI 校验:注解降级方向仅警告,注解升级方向阻断发布
原则三:合规元数据变更不走研发流程
合规团队通过管理 API 提申请 → 两人审批 → 到期自动生效
变更历史永久留档,满足 GDPR 审计要求
原则四:元数据对所有角色可见
研发:代码注解(开发期提示)
DBA:MySQL 字段 COMMENT 自动同步([PII-5] 收货人姓名)
运维:日志自动脱敏并标注类型
合规团队:管理界面统一查询、修改、审计
2.4 一致性策略定义
| 策略 | 实现机制 | 适用场景 | 分区时行为 |
|---|---|---|---|
STRONG_LOCAL |
Seata AT + Region 内 MGR | 支付写入、PII 写入 | 拒绝,返回 503 |
STRONG_GLOBAL |
Redis 原子预占 + 异步落库权威节点 | 库存扣减 | Redis 继续,落库消息堆积 |
SESSION |
Vector Clock token + 本地从库读 | 订单查询、用户信息 | 降级,返回 X-Stale: true |
EVENTUAL |
Canal + Kafka 异步复制 | 购物车、推荐、搜索 | 继续服务陈旧数据 |
第二层:整体架构
3. 系统全景
3.1 整体架构图
╔═══════════════════════════════════════════════════════════════════════╗
║ 用户访问入口 ║
║ EU 用户 ──┐ ║
║ ├──► GeoDNS(就近解析)──► Anycast VIP ──► Regional GW ║
║ SEA 用户 ─┤ ║
║ US 用户 ──┘ ║
╚═══════════════════════════════════════════════════════════════════════╝
│
╔══════════════════════════════════▼════════════════════════════════════╗
║ Gateway 层(Spring Cloud Gateway) ║
║ 限流熔断(Sentinel) │ 认证鉴权(Sa-Token) │ Region路由 │ 灰度分流 ║
║ 注入:X-Region / X-Trace-Id / X-Vector-Clock / X-User-Tags ║
╚══════════════════════════════════╦════════════════════════════════════╝
║
╔══════════════════════════════════▼════════════════════════════════════╗
║ Service 层(Spring Boot 3.x) ║
║ ║
║ OrderService │ InventoryService │ UserService │ CartService │ ... ║
║ ║
║ ─────────────── 框架能力层(Starter 统一提供)────────────────── ║
║ ConsistencyPolicy AOP ComplianceFence AOP Idempotent AOP ║
║ VectorClock Filter FeatureFlag Engine RegionContext ║
║ ComplianceMetadataRegistry(运行时合规元数据,从 DB 加载) ║
╚══════════════════════════════════╦════════════════════════════════════╝
║
╔══════════════════════════════════▼════════════════════════════════════╗
║ 消息层(Kafka) ║
║ ║
║ Outbox CDC(Canal → Kafka) 业务事件 Topic 重试 Topic 链 DLQ ║
║ MirrorMaker 2(跨 Region Topic 同步:cart.crdt.sync / catalog.*) ║
╚══════════════════════════════════╦════════════════════════════════════╝
║
╔══════════════════════════════════▼════════════════════════════════════╗
║ 存储层(MySQL 生态) ║
║ ║
║ ┌─────────────────────────────────────────────────────────────┐ ║
║ │ Global Coordination DB(MySQL 8 MGR,SEA 机房,3 节点) │ ║
║ │ order_ref │ compliance_field_metadata │ idempotent_record │ ║
║ │ user_region_mapping │ inventory_warmup │ outbox │ ║
║ └─────────────────────────────────────────────────────────────┘ ║
║ ║
║ ┌────────────┐ ┌────────────┐ ┌────────────┐ ║
║ │ EU-MySQL │ │ SEA-MySQL │ │ US-MySQL │ ║
║ │ 1主2从 │ │ 1主2从 │ │ 1主2从 │ ║
║ │ EU业务数据 │ │ SEA业务 │ │ US业务数据 │ ║
║ │ EU PII │ │ 全局库存 │ │ US PII │ ║
║ └────────────┘ └────────────┘ └────────────┘ ║
║ │ Canal+Kafka(非PII字段,EVENTUAL) │ ║
║ └─────────────────┬─────────────────────┘ ║
║ │ 只读副本同步 ║
║ ┌────────────┐ ┌────▼───────┐ ┌────────────┐ ║
║ │ EU-Redis │ │ SEA-Redis │ │ US-Redis │ ║
║ │ Cluster │ │ Cluster │ │ Cluster │ ║
║ │ 库存预占 │ │ 库存预占 │ │ 库存预占 │ ║
║ │ 幂等一级 │ │ 幂等一级 │ │ 幂等一级 │ ║
║ │ Cart CRDT │ │ Cart CRDT │ │ Cart CRDT │ ║
║ └────────────┘ └────────────┘ └────────────┘ ║
╚═══════════════════════════════════════════════════════════════════════╝
║
╔══════════════════════════════════▼════════════════════════════════════╗
║ Control Plane(SEA 部署) ║
║ Apollo(配置) Seata Server SkyWalking OAP Prometheus+Grafana ║
║ Canal Server(HA) XXL-Job(调度) Nacos(注册中心) ║
╚═══════════════════════════════════════════════════════════════════════╝
3.2 Region 间数据流动规则
允许的跨 Region 数据流动:
非 PII 业务字段 → Canal + Kafka 异步复制(EVENTUAL)
影子引用(OrderRef)→ Canal 复制到 Global Coordination DB
购物车 CRDT → MirrorMaker 2 双向同步 cart.crdt.sync
商品目录 → MirrorMaker 2 单向同步 catalog.*
禁止的跨 Region 数据流动:
PII 字段 → 禁止直接复制,只允许代理读(数据不离开源 Region)
支付详情 → 禁止跨区复制(金融合规)
Redis 数据 → 各 Region 独立,不跨区同步
同步写操作 → 禁止跨 Region 同步写(延迟不可接受)
3.3 Kafka 消息平台设计
统一消息平台(Canal + 业务消息 共用 Kafka,替代 RocketMQ):
Topic 命名规范:
Canal 数据变更: canal.{database}.{table}
业务事件: {domain}.{event}
Outbox 中继: canal.{db}.outbox
延迟消息: delay-{duration}(1s/10s/1m/5m/30m/1h)
重试: retry.{domain}.{event}.{n}(n=1,2,3)
死信: dlq.{domain}.{event}
MirrorMaker 2 跨 Region 同步:
SEA → EU/US: cart.crdt.sync, inventory.sync, catalog.*
EU → SEA: cart.crdt.sync(购物车 CRDT 双向)
PII 相关 Topic 严禁进入 MM2 同步规则
RocketMQ 能力替代映射:
事务消息 → Outbox Pattern + Canal CDC(延迟 < 50ms)
延迟消息 → 订单超时用 XXL-Job 扫描,其他用分级延迟 Topic
自动重试 → 重试 Topic 链(retry.*.1/2/3)
死信队列 → dlq.* Topic + P1 告警
顺序消息 → 分区键路由(orderId/userId/skuId 同分区)
4. 存储架构
4.1 存储分层与职责
┌─────────────────────────────────────────────────────────────────────┐
│ Global Coordination DB(全局协调库) │
│ 技术:MySQL 8 MGR 单主,3 节点跨 AZ,部署在 SEA │
│ │
│ 存储内容(全部为非 PII,全局可访问): │
│ · order_ref 订单影子引用(orderId/status/region) │
│ · compliance_field_metadata 字段合规元数据注册表(唯一权威) │
│ · compliance_change_approval 合规类型变更审批流 │
│ · compliance_field_metadata_history 变更历史(永久保留) │
│ · user_region_mapping 用户归属 Region 映射 │
│ · idempotent_record 幂等兜底(Redis 降级时使用) │
│ · outbox 事务性消息发件箱(Outbox Pattern) │
│ · inventory_warmup 大促预热分配记录 │
│ · promotion_usage 促销核销记录 │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Regional MySQL(区域业务库) │
│ 技术:MySQL 8 主从,1 主 2 从,跨 AZ 部署 │
│ 各 Region 独立,SEA-MySQL 额外承担全局库存权威 │
│ │
│ EU-MySQL:EU 业务数据 + EU PII(不出境) │
│ SEA-MySQL:SEA 业务数据 + 全局库存权威(inventory_global) │
│ US-MySQL:US 业务数据 + US PII(不出境) │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Redis Cluster(区域缓存) │
│ 技术:Redis 6 Cluster,3 主 3 从,各 Region 独立,不跨区同步 │
│ │
│ inv:pre:{skuId} 实时可售库存(预占防超卖) │
│ inv:warmup:{activityId}:{reg} 大促预热分配库存 │
│ inv:lock:{orderId}:{skuId} 订单预占锁定(用于回滚释放) │
│ cart:orset:{userId} 购物车 CRDT(OR-Set 序列化) │
│ idempotent:{bizKey} 幂等记录一级(快速判重) │
└─────────────────────────────────────────────────────────────────────┘
4.2 库存权威节点设计
设计决策:全局库存写入集中在 SEA-MySQL
写入路径(大促): 下单 → Redis 原子预占(< 1ms)
→ RocketMQ 事务消息 → SEA-MySQL 异步落库
写入路径(非大促):下单 → Redis 原子预占(< 1ms)
→ 直接写 SEA-MySQL(~100ms,可接受)
故障降级:
SEA 主库故障(AZ 级别)→ Sentinel 自动切换(< 30s),Redis 无感知
SEA 整区故障(Region 级别)→ 手动切换脚本,RTO < 15 分钟
4.3 Outbox Pattern 设计
替代 RocketMQ 事务消息,保证本地事务与消息发送原子性:
业务写入 + outbox 写入(同一本地事务)
│ binlog 变更
▼
Canal 监听 outbox 表
│ < 50ms 延迟
▼
Kafka Producer 发送(幂等生产者)
│
▼
业务消费者(@Idempotent 幂等消费)
Canal 故障兜底:
OutboxRelay 定时任务每 100ms 轮询 PENDING 状态消息
确保 Canal 故障期间消息最终被投递
第三层:领域模型与核心链路
5. 领域模型
5.1 统一语言约定
下列术语在代码、数据库、文档、口头交流中保持一致:
| 领域概念 | 统一用词 | 禁止混用的同义词 |
|---|---|---|
| 订单主体 | OrderCore |
order_main, order_info |
| 订单影子引用 | OrderRef |
order_shadow, order_global |
| 全局库存 | InventoryGlobal |
stock_global, inventory |
| 库存预占 | PreOccupy |
reserve, lock_stock |
| 购物车 |
Cart(OR-Set 实现) |
basket, shopping_bag |
| 合规围栏 | ComplianceFence |
data_barrier, data_fence |
| 合规元数据注册中心 | ComplianceMetadataRegistry |
compliance_cache |
| 一致性策略 | ConsistencyPolicy |
sync_policy |
| 区域上下文 | RegionContext |
locale_context, site_context |
| 向量时钟 | VectorClock |
logical_clock |
| 幂等记录 | IdempotentRecord |
dedup_record |
| 大促预热 | StockWarmup |
promotion_prepare |
| 事务发件箱 | Outbox |
message_box, tx_message |
5.2 聚合边界
Order 聚合:
聚合根:OrderCore(含 PII,存 Regional MySQL,不出境)
值对象:OrderItem(存 Regional MySQL)
影子: OrderRef(无 PII,存 Global Coordination DB,Canal 同步)
不变量:同一 OrderCore 的 OrderItem 属于同一 Region
状态变更必须通过状态机,不允许直接赋值
Inventory 聚合:
聚合根:InventoryGlobal(SEA-MySQL,唯一写入入口)
前置缓冲:Redis 预占(inv:pre:{skuId})
流水: InventoryPreOccupy(存 SEA-MySQL)
不变量:available >= 0(不允许超卖)
totalStock = available + preOccupied + locked + sold
Cart 聚合(CRDT):
聚合根:ORSetCart(以 userId 标识,存 Redis)
不变量:value() 中 qty 之和 >= 0
merge() 操作幂等、可交换、可结合
ComplianceMetadata 聚合:
聚合根:FieldMetadata(存 Global Coordination DB)
历史: FieldMetadataHistory(永久保留,不可删除)
审批: ChangeApproval(变更必须经过两人审批)
不变量:data_type 变更必须有 change_reason 和 approver
5.3 核心实体定义
OrderCore(订单主体)
OrderCore {
orderId: 全局唯一,Snowflake(含 Region 位)
userId: 用户 ID(非 PII,可跨区流动)
region: 下单所在 Region
status: OrderStatus 状态机
currency: 货币代码
totalAmount: 订单总金额(含税)
[PII-5] receiverName: 收货人姓名,不出境
[PII-4] receiverPhone: 收货人手机,不出境
[PII-3] receiverAddr: 收货地址,不出境
version: 乐观锁版本号
}
OrderStatus 状态机(不可跳过,不可逆):
PENDING_PAYMENT → PAID / CANCELLED
PAID → SHIPPED / CANCELLED
SHIPPED → COMPLETED
OrderRef(订单影子引用)
OrderRef {
orderId: 关联 OrderCore.orderId
userId: 关联 OrderCore.userId
region: 数据所在 Region(跨区代理读的路由依据)
status: 订单状态(Canal 异步同步自 OrderCore)
currency: 货币代码
totalAmount: 订单总金额(整数脱敏)
}
说明:OrderRef 不含任何 PII 字段,存 Global Coordination DB,全局可读
FieldMetadata(字段合规元数据)
FieldMetadata {
appName: 服务名(order-service)
tableName: 数据库表名(order_core)
fieldName: 字段名 snake_case(receiver_name)
javaFieldName: Java 字段名 camelCase(receiverName)
dataType: PII / FINANCIAL / BEHAVIORAL / PUBLIC
description: 字段业务含义说明
sensitivity: 敏感等级 1-5(影响日志脱敏强度)
status: ACTIVE / DEPRECATED / PENDING_REVIEW
effectiveFrom: 生效时间
effectiveTo: 失效时间(NULL = 永久有效)
changeReason: 变更原因
regulationRef: 关联法规条款(GDPR-Art4)
}
注意:DB 是唯一权威来源。
@ComplianceData 注解只作开发期文档提示,运行时不参与决策。
ORSetCart(购物车 CRDT)
ORSetCart {
userId: 购物车所属用户
addSet: Map<skuId, Set<"{epoch}:{uuid}:{qty}">>
removeSet: Map<skuId, Set<tag>>
epoch: GC 纪元(每 24h 推进,压缩历史 tag)
}
操作语义:
add(skuId, qty) → 生成新 tag 加入 addSet
remove(skuId) → 将已知 tag 移入 removeSet(并发新 add 不受影响)
value() → addSet - removeSet 的有效商品及数量
merge(other) → 各集合取并集(幂等、可交换、可结合)
gc() → epoch+1,重建 addSet,清理历史 tag
5.4 值对象定义
VectorClock {
clocks: Map<nodeId, Long>
tick() 本地写时递增本节点计数
merge(other) 取各分量最大值
happenedAfter() 因果顺序判断
serialize() HTTP Header 序列化(Base64 JSON)
}
ConsistencyContext {
level: 一致性级别
maxStalenessMs: SESSION 级别允许的最大陈旧毫秒数
vectorClock: 因果令牌
traceId: 全局链路追踪 ID
sourceRegion: 请求来源 Region
fallback: 分区时降级策略(REJECT / SERVE_STALE / RETURN_CACHED)
}
RegionContext(ThreadLocal,请求级别){
currentRegion: 当前服务所在 Region(环境变量 REGION_ID 注入)
userHomeRegion: 用户数据主权 Region(从 user_region_mapping 查询)
dataOwnerRegion: 当前操作数据的归属 Region
}
6. 核心链路
实线(→)= 同步调用;虚线(⇢)= 异步消息
6.1 下单链路
用户提交订单(EU Region)
│
→ [Gateway] 验证 JWT,注入 X-Region/X-Trace/X-Vector-Clock,Sentinel 限流
│
→ [OrderService.createOrder()]
│
① 幂等检查(Redis,< 1ms)
key = clientOrderId
PROCESSING → 返回 409
DONE → 返回缓存结果
未命中 → 写入 PROCESSING,继续
│
② Redis 库存预占(EU-Redis,< 1ms)
Lua 原子:DECRBY inv:pre:{skuId} {qty}
remaining < 0 → 立即返回库存不足(不进事务)
remaining >= 0 → 记录 inv:lock:{orderId}:{skuId}
│
③ 本地事务(Seata AT,EU Region 内)
@GlobalTransactional(timeoutMills=5000)
├─ INSERT order_core → EU-MySQL(status=PENDING_PAYMENT)
└─ INSERT outbox → Global DB(topic=order.created,同一事务)
│
④ 事务提交后 Canal CDC 触发
Canal 监听 outbox 表变更(< 50ms)
Kafka Producer 幂等发送 order.created
UPDATE outbox SET status=SENT
│
⑤ 幂等更新:PROCESSING → DONE
│
→ 返回 {orderId, status, X-Vector-Clock}
⇢ 异步消费 order.created:
Consumer A:INSERT order_ref → Global DB(@Idempotent)
Consumer B:INSERT stock.deduct → SEA-MySQL 权威落库(@Idempotent)
Consumer C:通知 WMS 仓储分配(@Idempotent)
异常处理:
② Redis 预占成功,③ Seata 事务失败
→ Seata AT 回滚 order_core + outbox
→ 回调中释放 Redis 预占:INCRBY inv:pre:{skuId}
④ Consumer B 落库失败
→ Kafka 重试 Topic 链(retry.order.created.1/2/3)
→ 超过 3 次 → dlq.order.created → P1 告警 + 运维手动处理
订单 30 分钟未付款
→ XXL-Job 扫描超时订单 → 发 order.timeout 消息
→ OrderService 消费 → status=CANCELLED + 释放 Redis 预占
6.2 库存扣减链路(大促场景)
大促前 T-30min(XXL-Job 触发 StockWarmup.warmup):
① SELECT ... FOR UPDATE 锁定 InventoryGlobal,快照 available
② 按 Region 流量权重分配(EU:30% SEA:50% US:20%)
③ Pipeline 批量写各区 Redis:
SET inv:warmup:{activityId}:{region} {quota}
SET inv:wmk:{activityId}:{region} {quota*0.2}(水位线)
④ UPDATE InventoryGlobal SET available=0(全部锁入预热)
⑤ 状态机:IDLE → ACTIVE
大促中:
deduct(skuId, qty, orderId)
→ 查 Apollo 开关 warmup_active?
→ 大促:Lua 原子 DECRBY inv:warmup:{activityId}:{region} {qty}
remaining < watermark → 异步触发动态调拨
remaining < 0 → 库存不足,拒绝
→ 非大促:DECRBY inv:pre:{skuId},异步落库 SEA-MySQL
动态调拨(XXL-Job 每 5s):
某区剩余 < 水位线 → 从全局预留池借出 → INCRBY
大促结束对账:
各区 Redis GETDEL 剩余量 → 计算实际消耗
→ UPDATE InventoryGlobal SET available=totalStock-实际消耗
→ 状态机:ACTIVE → SETTLED,生成对账报告
6.3 跨区 PII 读取链路
SEA-OrderService 查 EU 用户订单详情
│
→ ComplianceFence.intercept()
① 查 ComplianceMetadataRegistry:
getByEntityField(OrderCore, "receiverName")
→ DataType=PII,sensitivity=5
② 查 CompliancePolicyEngine:
规则来自 Apollo compliance-rules namespace
PII.crossRegionRead = PROXY_ONLY
currentRegion=SEA ≠ dataOwnerRegion=EU
→ 决策:requireProxy(targetRegion="EU")
│
→ RegionProxyClient.proxyRead("EU", pjp)
HTTP GET eu-order-service/internal/proxy/order/{orderId}
Headers:
X-Internal-Token: HMAC-SHA256 签名
X-Compliance-Purpose: ORDER_DISPLAY(白名单内)
X-Requester-Service: sea-order-service
X-Requester-Region: SEA
│
→ EU-OrderService 处理:
验证 X-Internal-Token 签名
验证 X-Compliance-Purpose 在白名单
写入合规审计日志(EU-MySQL,不出境)
返回 OrderCore(含 PII)
│
→ SEA 服务使用数据,响应结束后 PII 不持久化
6.4 合规元数据变更链路
场景:法务要求将 phoneNumber 从 PII 降级到 BEHAVIORAL
合规团队通过管理 API 提交变更申请
→ compliance_change_approval 写入 PENDING
→ 通知两位合规审批人
│
两位审批人分别审批通过
→ approval.status = APPROVED
→ XXL-Job 到 effectiveTime 时执行变更:
UPDATE compliance_field_metadata
SET data_type='BEHAVIORAL', change_reason=?, updated_by=?
WHERE table_name='user_profile' AND field_name='phone_number'
INSERT compliance_field_metadata_history(不可删除)
发布 Apollo compliance-metadata 变更通知
│
各服务 ComplianceMetadataRegistry.reload()(热更新,无需重启)
MySQL 字段 COMMENT 自动更新:
[BEHAVIORAL-2] 收货人手机(原 PII-4,PDPA_MY-2026 修订)
变更生效验证(运维 API):
GET /internal/compliance/fields?tableName=user_profile
→ 确认 phone_number 的 data_type 已变更
→ 观察合规违规指标 compliance_violation_total(应为 0)
6.5 购物车 CRDT 同步链路
EU 用户在 EU 区加购 SKU_A 数量 2,同时在 SEA 区加购 SKU_B 数量 1:
EU-CartService.add("U001", "SKU_A", 2):
① loadCart → ORSetCart from EU-Redis
② cart.add("SKU_A", 2) → addSet["SKU_A"].add("0:uuid1:2")
③ SET cart:orset:U001 {serialize} → EU-Redis
④ ⇢ Kafka: cart.crdt.sync {userId:"U001", payload, sourceRegion:"EU"}
→ MirrorMaker 2 同步到 SEA-Kafka
SEA-CartSyncConsumer 消费 EU 发出的事件:
sourceRegion("EU") ≠ currentRegion("SEA") → 需要 merge
local = loadCart("U001") from SEA-Redis → {SKU_B:1}
remote = deserialize(payload) → {SKU_A:2}
merged = local.merge(remote) → {SKU_A:2, SKU_B:1}
SET cart:orset:U001 → SEA-Redis
最终两区一致:{SKU_A:2, SKU_B:1},无冲突,无数据丢失
GC(XXL-Job 每日 03:00):
扫描 tag 总数 > 1000 的购物车
cart.gc() → epoch+1,重建,⇢ Kafka 广播 GC 后快照
6.6 全链路追踪链路
请求注入追踪上下文(Gateway):
X-Trace-Id: {regionPrefix}-{timestamp}-{random}
X-Vector-Clock: base64({"sea-node1":42})
X-Region: SEA
SkyWalking Java Agent 自动采集(零侵入):
HTTP Span / MySQL Span(含 SQL)/ Redis Span
Kafka 生产消费 Span / Seata 事务 Span
跨 Region Trace 不断链:
HTTP 跨区:Header 透传 X-Trace-Id
Kafka 跨区消息:Message Header 透传
Trace 存储:
各区 SkyWalking OAP → 完整 Span(PII 字段脱敏后记录)
Global Trace Index → 仅 Span ID + 父子关系 + 时间戳
根因分析:Global Index 定位 Region → 跳转该区 OAP UI
日志脱敏(ComplianceAwareLogger):
查 ComplianceMetadataRegistry 自动识别敏感字段
按 sensitivity 级别脱敏:
5 → ***(完全遮盖)
4 → 张***(保留首字符)
3 → 广州***路(保留首尾各 30%)
日志格式:
{"receiverName":{"type":"PII","masked":"***"},
"totalAmount":{"type":"FINANCIAL","masked":"1**.**"}}
第四层:落地细节
7. 基础设施配置
7.1 MySQL MGR 关键配置
# Global Coordination DB(SEA 机房,my.cnf)
[mysqld]
server_id = 1
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_format = ROW
binlog_row_image = FULL
log_slave_updates = ON
loose-group_replication_single_primary_mode = ON
loose-group_replication_group_seeds = "az1:33061,az2:33061,az3:33061"
# Regional MySQL(以 EU 为例)
[mysqld]
server_id = 100
log_bin = mysql-bin
binlog_format = ROW # Canal 依赖
binlog_row_image = FULL # Canal 需要完整行镜像
sync_binlog = 1 # 每次提交刷盘
innodb_flush_log_at_trx_commit = 1 # 强 fsync
# 从库
read_only = ON
super_read_only = ON
slave_parallel_workers = 4
slave_parallel_type = LOGICAL_CLOCK
7.2 Canal 配置与高可用
# canal.properties
canal.zkServers=zk1:2181,zk2:2181,zk3:2181
canal.serverMode=kafka
canal.kafka.bootstrap.servers=kafka1:9092,kafka2:9092
# canal.instance.properties(EU 实例)
canal.instance.master.address=eu-az1-mysql:3306
# 白名单:只订阅业务表和 outbox 表
canal.instance.filter.regex=ecommerce_eu\\.order_core,ecommerce_eu\\.outbox,ecommerce_eu\\.user_behavior
# 黑名单:显式排除所有 PII 表(黑名单优先于白名单)
canal.instance.filter.black.regex=ecommerce_eu\\.user_profile,ecommerce_eu\\.payment_detail
canal.mq.topic=canal.{database}.{table}
canal.mq.partitionsNum=12
canal.mq.partitionHash=orderId,userId
7.3 Kafka 生产者与消费者配置
// 生产者(幂等配置)
props.put(ENABLE_IDEMPOTENCE_CONFIG, true); // 幂等生产者
props.put(ACKS_CONFIG, "all"); // 需要所有副本确认
props.put(RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);
props.put(COMPRESSION_TYPE_CONFIG, "lz4"); // 压缩,降低跨区传输成本
// 消费者(手动提交 offset)
props.put(ENABLE_AUTO_COMMIT_CONFIG, false);
props.put(AUTO_OFFSET_RESET_CONFIG, "earliest");
props.put(MAX_POLL_RECORDS_CONFIG, 100);
// 手动 ACK 模式
factory.getContainerProperties()
.setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
7.4 Kafka 重试 Topic 链
// 统一消费基类:业务只实现 process(),框架处理重试路由
public abstract class RetryAwareKafkaConsumer<T> {
// 消费失败自动路由到下一级 retry Topic
protected void consume(ConsumerRecord<String, String> record,
Class<T> eventClass, Acknowledgment ack) {
try {
process(JSON.parseObject(record.value(), eventClass));
ack.acknowledge();
} catch (NonRetryableException e) {
retryRouter.routeToDLQ(record, e); // 不可重试,直接 DLQ
ack.acknowledge();
} catch (Exception e) {
retryRouter.routeToRetry(record, e); // 路由到 retry.*.{n+1}
ack.acknowledge();
}
}
protected abstract void process(T event) throws Exception;
}
// 重试间隔:retry.*.1=10s,retry.*.2=30s,retry.*.3=1min
// 超过 3 次 → dlq.*(P1 告警)
7.5 MirrorMaker 2 配置
# mm2.properties
clusters = sea, eu, us
sea.bootstrap.servers = sea-kafka1:9092,sea-kafka2:9092
eu.bootstrap.servers = eu-kafka1:9092,eu-kafka2:9092
sea->eu.enabled = true
sea->eu.topics = cart\\.crdt\\.sync,inventory\\.sync,catalog\\..*
# 严禁 PII 相关 Topic 进入 MM2 同步
sea->eu.topics.blacklist = order\\..*,payment\\..*,user\\.pii\\..*
eu->sea.enabled = true
eu->sea.topics = cart\\.crdt\\.sync # 购物车 CRDT 双向同步
7.6 Seata 关键配置
seata:
client:
rm:
lock:
retry-interval: 10 # 全局锁重试间隔 10ms
retry-times: 30 # 最多 30 次(总 300ms)
undo:
log-delete-period: 30000 # 每 30s 清理过期 undolog
log-save-days: 7
tm:
default-global-transaction-timeout: 5000
7.7 合规元数据注册中心配置
// 启动加载 + Apollo 热更新
@Component
public class ComplianceMetadataRegistry {
@PostConstruct
public void load() { reload(); }
// Apollo compliance-metadata namespace 变更时自动刷新
@ApolloConfigChangeListener("compliance-metadata")
public void onConfigChange(ConfigChangeEvent e) { reload(); }
public void reload() {
// 从 Global Coordination DB 加载所有 ACTIVE 元数据
List<FieldMetadata> all = metadataMapper.selectActive(appName);
// 构建三级 Map:appName → tableName → fieldName → metadata
this.cache = buildCache(all);
}
// 查询接口:运行时使用
public Optional<FieldMetadata> getByTableField(
String tableName, String fieldName) { ... }
public Optional<FieldMetadata> getByEntityField(
Class<?> entityClass, String javaFieldName) { ... }
}
7.8 新 Region 上线配置(Apollo)
# namespace: region.MY(马来西亚新站点)
region:
id: MY
name: Malaysia
timezone: Asia/Kuala_Lumpur
currency: MYR
languages: [ms, en, zh]
compliance:
dataResidency: MY
regulations: [PDPA_MY]
consistency:
defaultLevel: EVENTUAL
paymentLevel: STRONG_LOCAL
inventoryLevel: STRONG_GLOBAL
topology:
primaryCluster: aws-ap-southeast-1
inventoryAuthorityRegion: SEA
fallbackRegion: SG
inventory:
warmupWeight: 0.08
datasource:
writeUrl: jdbc:mysql://my-az1-mysql:3306/ecommerce_my
readUrl: jdbc:mysql://my-az2-mysql:3306/ecommerce_my
redis: my-redis-cluster:6379
7.9 本地开发环境
# docker-compose.local.yml(一键启动)
services:
mysql-global: # Global Coordination DB
image: mysql:8.0
ports: ["3306:3306"]
mysql-regional: # Regional MySQL
image: mysql:8.0
ports: ["3307:3306"]
redis: # Redis(单节点模拟 Cluster)
image: redis:7-alpine
ports: ["6379:6379"]
kafka: # Kafka(含 Zookeeper)
image: confluentinc/cp-kafka:7.5.0
ports: ["9092:9092"]
seata: # Seata Server
image: seataio/seata-server:2.0.0
ports: ["8091:8091"]
apollo: # Apollo 配置中心
image: nobodyiam/apollo-quick-start:2.1.0
ports: ["8080:8080"]
skywalking-oap: # SkyWalking(H2 存储)
image: apache/skywalking-oap-server:9.7.0
ports: ["11800:11800", "12800:12800"]
// Local Profile:屏蔽分布式复杂度
@Configuration
@Profile("local")
public class LocalDevConfig {
@Bean @Primary
public ConsistencyRouter localRouter() {
return new LocalPassthroughRouter(); // 所有请求走本地 MySQL
}
@Bean @Primary
public ComplianceFenceAspect localFence() {
return new WarnOnlyComplianceFenceAspect(); // 只打 WARN,不拒绝
}
@Bean @Primary
public ComplianceMetadataRegistry localRegistry() {
// 优先读 DB,DB 无数据时从代码注解兜底
return new AnnotationFallbackRegistry(delegate);
}
@Bean @Primary
public FeatureFlagEngine localFF() {
// 环境变量强制开关:export FF_NEW_CHECKOUT=true
return (name, ctx) -> Boolean.parseBoolean(
System.getenv("FF_" + name.toUpperCase()));
}
}
8. 研发规范
8.1 注解使用规范
必须使用(PR 门禁检查):
@ConsistencyPolicy(level = ?)
· 所有 Service 层跨区数据写入方法
· 禁止对支付/库存扣减使用 EVENTUAL 级别
@ComplianceData(type = DataType.?, description = "...")
· 实体类中所有含个人信息的字段
· 字段名含 Name/Phone/Email/Address/IdCard/BankCard 时必须标注
· 注意:运行时以 compliance_field_metadata 表为准,注解仅作文档提示
@Idempotent(keyExpression = "...")
· 所有 @GlobalTransactional 方法
· 所有 @KafkaListener 方法
@GlobalTransactional
· 跨表或跨服务写操作,必须配合 @Idempotent
禁止的模式:
× 支付/库存扣减使用 ConsistencyLevel.EVENTUAL
× 直接操作含 PII 字段的实体类 Mapper(必须经 ComplianceFence)
× @KafkaListener 不加 @Idempotent
× @GlobalTransactional 方法通过 this 内部调用(AOP 失效)
× Seata AT 事务中操作 TEXT/BLOB 大字段(undolog 膨胀)
8.2 ArchUnit 门禁规则
@AnalyzeClasses(packages = "com.company.ecommerce")
class ArchitectureEnforcementTest {
@ArchTest // 跨区 Service 方法必须声明一致性策略
static final ArchRule crossRegionPolicy = methods()
.that().areDeclaredInClassesThat()
.areAnnotatedWith(CrossRegionService.class)
.should().beAnnotatedWith(ConsistencyPolicy.class);
@ArchTest // PII 字段必须标注(注解兜底,DB 为主)
static final ArchRule piiAnnotated = fields()
.that().haveNameMatching(
".*(Name|Phone|Email|Address|IdCard|BankCard).*")
.should().beAnnotatedWith(ComplianceData.class);
@ArchTest // 全局事务方法必须幂等
static final ArchRule txIdempotent = methods()
.that().areAnnotatedWith(GlobalTransactional.class)
.should().beAnnotatedWith(Idempotent.class);
@ArchTest // Kafka 消费方法必须幂等
static final ArchRule listenerIdempotent = methods()
.that().areAnnotatedWith(KafkaListener.class)
.should().beAnnotatedWith(Idempotent.class);
@ArchTest // Service 层禁止直接访问 PII Mapper
static final ArchRule noDirectPii = noClasses()
.that().resideInAPackage("..service..")
.should().accessClassesThat()
.areAnnotatedWith(PiiMapper.class);
}
8.3 合规元数据与代码注解一致性校验(CI)
// CI 流程中执行:扫描注解与 DB 对比
@Test
void complianceMetadataConsistencyCheck() {
List<FieldMetadata> fromCode =
importer.scanFromCode("com.company.ecommerce");
List<FieldMetadata> fromDB =
metadataMapper.selectAll();
List<String> issues = importer.checkConsistency(fromCode, fromDB);
// 升级方向(DB 要求更严格,代码注解宽松)→ 阻断发布
List<String> blockers = issues.stream()
.filter(i -> i.contains("[升级方向不一致]"))
.collect(Collectors.toList());
assertTrue(blockers.isEmpty(),
"合规元数据升级方向不一致,必须先更新代码注解:\n"
+ String.join("\n", blockers));
// 降级方向(注解比 DB 严格)→ 仅警告
issues.stream()
.filter(i -> i.contains("[降级方向]"))
.forEach(w -> log.warn("[CI] 合规元数据警告:{}", w));
}
8.4 Schema 变更规范
阶段一:扩展(只加列,允许 NULL 或有默认值)
各 Region MySQL 逐个执行(EU → SEA → US),每区间隔 30 分钟观察
阶段二:代码发布(新代码读写新列)
灰度:1% → 10% → 50% → 100%
阶段三:清理(至少 2 个迭代后)
DROP COLUMN(需单独排期)
禁止:RENAME COLUMN / DROP 正在使用的列 / 添加 NOT NULL 无默认值列
8.5 超时与重试配置
timeout:
http.connect: 1000ms
http.read: 3000ms
http.read-payment: 10000ms # 等银行响应
db.query: 5000ms
db.slow-threshold: 100ms # 超过记录慢查询
redis.command: 500ms
seata.global: 5000ms
retry:
http.max-attempts: 3
http.backoff-delay: 100ms
http.backoff-multiplier: 2.0
http.jitter: true # 防同步重试风暴
seata-lock.max-attempts: 3
9. 监控与运维
9.1 监控分层模型
L4 业务监控:订单转化率 / 库存超卖率 / 支付成功率
L3 应用监控:接口 P99 / 错误率 / Seata 回滚率 / Outbox 积压
L2 中间件: Kafka 消费延迟 / Canal 复制延迟 / Redis 命中率
L1 基础设施:MySQL 主从延迟 / 慢查询 / Redis 内存 / JVM GC
告警响应时间目标:
L4/L3 → P1,5 分钟内响应
L2 → P2,30 分钟内响应
L1 → P2/P3,视严重程度
9.2 关键告警规则
# P1 告警(立即响应)
- 接口错误率 > 1% 持续 1 分钟
- 库存 available < 0(超卖)
- Outbox PENDING 积压 > 1000 条持续 5 分钟
- 死信队列(dlq.*)有新消息
- 合规违规事件 compliance_violation_total 增加
# P2 告警(30 分钟内响应)
- P99 延迟 > 500ms 持续 3 分钟
- MySQL 主从延迟 > 10s
- Canal 复制延迟 > 30s
- Kafka 消费积压 > 5000 条持续 5 分钟
- Seata 事务回滚率 > 5% 持续 5 分钟
- Redis 内存使用率 > 80%
- 合规元数据变更未在 24h 内同步到所有服务
# P3 告警(当日处理)
- Canal 主备切换事件
- Seata undolog 表行数 > 100 万
- 合规字段注解与 DB 不一致
9.3 Grafana Dashboard 分层
Dashboard 1:全局概览(5 分钟刷新)
各 Region 实时 QPS / P99 延迟 / 错误率
库存健康状态 / Kafka 积压 / Outbox 积压 / 活跃告警
Dashboard 2:Region 下钻(1 分钟刷新,可选 Region)
接口延迟热图 / Seata 事务成功率
MySQL 慢查询 / 主从延迟
Redis 命中率 / 内存
Kafka Topic 消费延迟 / Canal 复制延迟
Dashboard 3:库存专项
SKU 库存水位(Top 20)/ 各区 Redis 预占 vs MySQL 对比
大促预热状态 / 超卖记录 / 动态调拨记录
Dashboard 4:合规审计
跨区 PII 访问次数(按 fromRegion/toRegion/目的)
合规违规事件(应为 0)/ 合规元数据变更记录
9.4 典型故障排查手册
故障一:下单失败
Step 1:获取 TraceId
ELK 查询:orderId=xxx AND level=ERROR → 获取 traceId
Step 2:SkyWalking 查完整链路
搜索 traceId → 定位红色失败 Span
Step 3:按失败节点分类
Gateway 失败:
401 → JWT 问题 / 429 → Sentinel 限流 / 503 → 下游不可用
Redis 预占失败(InsufficientStockException):
redis-cli GET "inv:pre:{skuId}"
0 → 真实库存不足(正常)
负数 → 预占逻辑 BUG(立即 P1 告警)
Seata 事务超时:
Seata 控制台 http://seata-server:7091
找到 xid → 查 Branch 状态
TIMEOUT → 优化慢 SQL 或调大 timeoutMills
LOCK_CONFLICT → 该 SKU 应走 Redis 预占路径
Outbox PENDING 积压:
SELECT COUNT(*) FROM outbox WHERE status=0 AND create_time < NOW()-300s
积压 → Canal 故障,查 canal.log
执行运维工具:POST /internal/ops/outbox/relay(手动补发)
故障二:Kafka 消费积压
Step 1:定位积压 Topic
kafka-consumer-groups.sh --describe --group {group}
Step 2:分析原因
消费者 Pod 崩溃 → kubectl logs {pod} --previous
单条消息处理慢 → SkyWalking 查 Consumer Span 耗时
消息量突增 → 对比历史 QPS,临时扩容消费者实例
下游依赖不可用 → 查消费者日志异常类型
Step 3:DLQ 处理
POST /internal/ops/dlq/replay?topic=order.created&limit=100
观察 dlq.order.created 消息是否成功重放
故障三:Canal 复制延迟
Step 1:快速诊断
ps aux | grep canal
tail -200 /home/canal/logs/canal/canal.log | grep -E "ERROR|WARN|delay"
Step 2:判断延迟来源
Canal → Kafka 慢:检查 Kafka 集群负载
MySQL binlog 追不上:增大 Canal 线程数
ZK 主备切换:等待 1-2 分钟自动收敛
Step 3:Outbox 积压影响评估
SELECT COUNT(*) FROM outbox WHERE status=0
如 > 1000 → 执行手动补发:POST /internal/ops/outbox/relay
故障四:合规元数据不一致
症状:服务启动日志 "[Compliance] Metadata loaded: 0 fields"
或合规违规告警
Step 1:检查 Global Coordination DB 连通性
SHOW STATUS WHERE Variable_name='Threads_connected';
Step 2:检查元数据表数据
SELECT COUNT(*) FROM compliance_field_metadata WHERE status='ACTIVE';
0 → 元数据未初始化,执行导入工具生成 SQL
Step 3:强制刷新各服务缓存
POST /internal/ops/compliance/reload
GET /internal/compliance/fields → 确认数据已加载
Step 4:验证 MySQL 字段注释同步
DESCRIBE order_core;
确认 receiver_name 的 Comment 包含 [PII-5]
9.5 运维工具 API
@RestController
@RequestMapping("/internal/ops")
@PreAuthorize("hasRole('OPS')")
public class OpsController {
// 手动重发 DLQ 消息
@PostMapping("/dlq/replay")
public ReplayResult replayDLQ(@RequestParam String topic,
@RequestParam(defaultValue="100") int limit)
// Outbox 消息强制补发(Canal 故障恢复后使用)
@PostMapping("/outbox/relay")
public RelayResult forceRelayOutbox(@RequestParam(defaultValue="500") int limit)
// 库存手动对账
@PostMapping("/inventory/reconcile")
public ReconcileResult reconcileInventory(@RequestParam String skuId)
// 合规元数据强制刷新
@PostMapping("/compliance/reload")
public void reloadComplianceMetadata()
// 查看当前合规规则(验证变更是否生效)
@GetMapping("/compliance/fields")
public List<FieldMetadataVO> listFields(
@RequestParam(required=false) String tableName,
@RequestParam(required=false) DataType dataType)
// 健康检查(K8s probe)
@GetMapping("/health")
public HealthResult health()
// 包含:mysql/redis/kafka/seata 健康状态
// outboxBacklog / region / complianceMetadataLoaded
}
9.6 库存对账日报 SQL
-- XXL-Job 每日 02:00 执行
SELECT
g.sku_id,
g.available AS mysql_available,
COALESCE(p.total_pre_occupied, 0) AS redis_pre_occupied,
g.available + COALESCE(p.total_pre_occupied, 0) AS total_usable,
CASE
WHEN g.available < 0 THEN '【超卖!立即处理】'
WHEN ABS(g.available + COALESCE(p.total_pre_occupied,0)
- g.total_stock) > g.total_stock * 0.05
THEN '【偏差超5%,需核查】'
ELSE '正常'
END AS health_status
FROM inventory_global g
LEFT JOIN (
SELECT sku_id, SUM(qty) AS total_pre_occupied
FROM inventory_pre_occupy WHERE status = 1
GROUP BY sku_id
) p ON g.sku_id = p.sku_id
WHERE g.available < 0
OR ABS(g.available + COALESCE(p.total_pre_occupied,0)
- g.total_stock) > g.total_stock * 0.05;
第五层:规划与决策
10. 落地路线图
Phase 1(M0-M3)框架搭建,SEA 单 Region 跑通
研发框架 Starter 组件(优先级排序):
P0:starter-region-context(RegionContext + Filter)
P0:starter-idempotent(Redis + DB 双写降级)
P0:starter-consistency(ConsistencyPolicy AOP)
P1:starter-compliance-metadata(Registry + Fence AOP)
P1:starter-vector-clock(因果一致性)
P1:starter-feature-flag(组合条件开关)
基础设施:
· Canal + Kafka 本地环境(docker-compose)
· ArchUnit 门禁规则 + CI 合规一致性校验
· Apollo 多 Region 命名空间规范
· SkyWalking 接入(SEA 先行)
· 合规元数据初始化导入工具(扫描注解 → 生成 SQL)
验收:SEA 单 Region 下单链路压测(10x 流量),合规元数据加载验证
Phase 2(M3-M6)多 Region 存储上线
存储层:
· EU / US Regional MySQL 主从部署
· Global Coordination DB(MySQL MGR)含 compliance_field_metadata 表
· Canal + Kafka 复制链路(EU/US 非 PII 字段 → 全局)
· Redis Cluster 各 Region 独立部署
· MirrorMaker 2 跨 Region Topic 同步
合规体系:
· ComplianceFence 上线(EU PII 不出境强制验证)
· 合规元数据管理 API + 审批流上线
· MySQL 字段 COMMENT 自动同步
· 日志脱敏 + 合规类型标注
· Canal PII 黑名单配置审计纳入上线 checklist
验收:跨区 PII 代理读 E2E 测试通过,合规审计视图可用
Phase 3(M6-M9)大促能力建设
库存能力:
· 三级库存模型(预热 + 动态调拨 + 对账)
· 库存权威节点故障切换脚本验证(每季度演练)
业务能力:
· CRDT OR-Set 购物车(含 Epoch GC)
· 支付链路 Seata TCC 改造
· 新站点 MY 配置驱动上线(3 天内完成验证)
· Outbox + Canal CDC 全量替换 RocketMQ 事务消息
验收:大促全流程压测(50x 流量),合规变更热更新验证
Phase 4(M9-M12)运维成熟
平台能力:
· Istio 服务网格接入(替换 Spring Cloud 路由层)
· 全局 SLO Dashboard(Grafana 四层仪表盘)
· 混沌工程演练(AZ 故障 / 网络分区 / Region 隔离)
· 库存对账全自动化(日报 + P1 告警)
· 合规元数据审计报告自动生成(满足 GDPR 第 30 条记录要求)
验收:99.99% 可用性 SLA 连续 30 天达标
10.1 关键风险对策
| 风险 | 影响 | 对策 |
|---|---|---|
| Canal 团队经验不足 | 复制链路故障无人处理 | M1 安排 1 人专项,M2 前完成 HA 演练 |
| Seata undolog 膨胀 | 存储耗尽 | 定时清理配置 + 热点 SKU 绕过 Seata |
| SEA 整区故障 | 库存写入中断 | 切换脚本每季度演练,RTO < 15min |
| CRDT addSet 无限增长 | Redis 内存溢出 | Epoch GC 每日执行,监控 cart key 大小 |
| 合规元数据审批周期长 | 变更被卡住 | 预设审批 SLA(24h),超时自动升级提醒 |
| 注解与 DB 长期不一致 | 误导研发 | CI 阻断升级方向不一致,每迭代末统一同步 |
11. 架构决策记录
ADR-001:放弃 TiDB,使用 MySQL 生态
日期:2026-05-15 | 状态:已确认
背景:原方案使用 TiDB,但团队无生产经验,三套组件(PD/TiKV/TiDB Server)出故障无人能接。
决策:MySQL 8 + MGR + Canal + Redis,在应用层用"库存单写权威节点 + Redis 预占 + 消息最终一致"替代 TiDB 跨区原生强一致。
权衡:放弃自动分片和原生跨区强一致;接受极小概率超卖(≤ 0.01%)和 SEA 整区故障 RTO 15 分钟;收益是团队完全可控,出故障知道在哪查。
ADR-002:统一消息平台为 Kafka,移除 RocketMQ
日期:2026-05-15 | 状态:已确认
背景:Canal 已用 Kafka,维护两套 MQ 成本高;RocketMQ 事务消息、延迟消息、自动重试等特性均可在 Kafka 生态中替代。
决策:全面使用 Kafka,RocketMQ 特性替代方案如下:事务消息 → Outbox + Canal CDC;延迟消息 → XXL-Job 扫描(订单超时)+ 分级延迟 Topic;自动重试 → 重试 Topic 链;死信队列 → dlq.* Topic。
权衡:Outbox Pattern 增加了一张表和 Canal 监听逻辑;但统一运维平台,减少了一套 MQ 的运维负担,且 MirrorMaker 2 使跨区消息同步更简洁。
ADR-003:库存权威节点选 SEA,不做跨区 Quorum 写
日期:2026-05-15 | 状态:已确认
背景:跨区 Quorum 写延迟 150-300ms,用户体验不可接受;SEA 是主流量 Region(50% 订单)。
决策:库存单写权威在 SEA-MySQL,其他 Region 通过 Redis 预占 + 异步消息落库。SEA 整区故障时手动切换(RTO < 15 分钟)。
ADR-004:Seata 分级使用
日期:2026-05-15 | 状态:已确认
决策:支付链路 TCC;订单/库存(非热点)AT;热点库存 Redis 预占绕过 Seata;其他服务 Outbox + Kafka 最终一致。
ADR-005:合规元数据外置到数据库,注解降级为文档提示
日期:2026-05-15 | 状态:已确认
背景:@ComplianceData 注解是编译期元数据,无法动态调整合规类型;DBA 和运维人员无法从数据库直接判断字段合规类型;合规规则变更需走研发排期,周期 2 周+。
决策:
- 合规元数据存储在
compliance_field_metadata表(Global Coordination DB),运行时由ComplianceMetadataRegistry加载 -
@ComplianceData注解保留但降级为文档提示,运行时以 DB 为准 - DB 与注解不一致时,DB 绝对优先;升级方向不一致阻断 CI,降级方向仅警告
- MySQL 字段 COMMENT 自动同步合规类型([PII-5] 收货人姓名)
- 合规类型变更由合规团队通过管理 API 提交,两人审批后自动生效,无需研发介入
权衡:增加了 compliance_field_metadata 表的维护成本;换取合规规则变更与代码发布解耦,变更延迟从 2 周降到 24 小时内。
ADR-006:Canal PII 表使用显式黑名单
日期:2026-05-15 | 状态:已确认
背景:Canal 默认订阅所有表,遗漏 PII 过滤导致合规违规;白名单要求每张新表手动添加,容易遗漏。
决策:Canal 采用"白名单(只订阅业务表)+ 黑名单(显式排除 PII 表)"双重保障,黑名单优先。新增 PII 表时必须同步更新黑名单,纳入上线 checklist 强制项。
ADR-007:CRDT OR-Set 购物车 + Epoch GC
日期:2026-05-15 | 状态:已确认
背景:购物车需要支持多 Region 并发操作不冲突;传统锁在多活架构下不可行;OR-Set 的 tag 集合在长期使用后无限累积。
决策:使用 CRDT OR-Set 实现购物车,add 语义优先;引入 Epoch GC(每 24h 一次)压缩历史 tag,防 Redis 内存无限增长。