现代分布式模式

共 21 题
📑 题目列表 21 题
#
★★★

1. 幂等键(Idempotency Key)模式中接口层用请求唯一标识实现幂等的通用框架?

请解释幂等键(Idempotency Key)模式,说明接口层如何用请求唯一标识实现幂等,并给出一个通用框架的设计思路?

  • 幂等键的生成与传递(客户端生成 UUID)
  • 服务端按幂等键去重与结果缓存
  • 并发冲突与结果一致性

幂等键模式:客户端在发起写操作时生成一个唯一的请求标识(Idempotency Key,通常是 UUID),携带在请求头(如 Idempotency-Key)中。服务端收到请求后,先按该键查缓存/存储:若已处理过则直接返回之前的结果,避免重复执行;若未处理则执行并记录(键 → 结果)。

通用框架设计:

  • 生成与传递:客户端为每次业务操作生成唯一键,网络重试时复用同一键。
  • 服务端存储:用一个幂等表(idempotency store)记录 (键, 请求哈希, 响应, 状态, 时间),键为主键,带唯一约束。
  • 并发处理:同一键并发到达时,用数据库唯一索引/分布式锁保证只有一个请求真正执行,其余等待或返回缓存结果。
  • 结果返回:已处理过返回存储的响应,包括状态码与 body,保证客户端重试得到一致结果。
  • 清理:幂等记录设置 TTL,防止无限增长。

关键点:幂等键必须由客户端生成并在重试间复用;服务端用"唯一键 + 原子插入"保证幂等;返回缓存结果保证一致性。典型实现如 Stripe 的 Idempotency-Key 头。

幂等键把"请求是否已处理"的判断从业务逻辑剥离到框架层,是防止重复下单/重复扣款等副作用的关键。核心是"唯一约束 + 结果缓存"。

// 幂等键存储(伪代码)
public class IdempotencyService {
    void handle(String key, Request req, Supplier<Response> action) {
        if (store.get(key) != null) return store.get(key);   // 已处理,返回缓存
        try {
            store.insert(key, "processing");                 // 唯一约束,冲突则等待
            Response resp = action.get();
            store.update(key, "done", resp);
            return resp;
        } catch (DuplicateKeyException e) {
            return store.get(key);                            // 并发重复,返回结果
        }
    }
}
#
★★★

2. Strangler Fig 的切流策略中路由层按比例切流、双写与影子流量如何降低风险,回滚条件与数据一致性窗口如何设计?

请解释 Strangler Fig 模式的切流策略,说明路由层按比例切流、双写与影子流量如何降低风险,以及回滚条件与数据一致性窗口如何设计?

  • 按比例切流与灰度发布
  • 双写(dual-write)与影子流量(shadow traffic)
  • 回滚条件与数据一致性窗口

Strangler Fig(绞杀者)模式:在保留旧系统的同时,逐步用新系统替换旧系统的功能,通过路由层控制流量在新旧系统间迁移。

切流策略:

  • 按比例切流:路由层(网关/负载均衡)按权重把业务流量从旧系统逐步切到新系统(如 1% → 5% → 50% → 100%),每档观察指标,异常则回退。
  • 双写(dual-write):写操作同时写入新旧两套系统,保证两边数据一致,才能支撑读取切换与对比。
  • 影子流量(shadow traffic):把真实请求复制一份发给新系统(影子),但新系统响应不返回给用户,用于验证新系统正确性与性能,不影响线上。

回滚条件:设定明确的指标阈值(错误率、延迟 P99、数据一致率),任一项超阈值即回滚切流比例,把流量切回旧系统。回滚要快,通常依赖路由层的即时切换。

数据一致性窗口:双写期间新旧系统数据存在同步延迟,读取切换时可能读到旧数据。设计上:优先切换写路径,再延迟切换读路径;用事件/消息补齐数据;对账机制定期校验新旧数据一致性,修复差异。一致性窗口越短,风险越低。

核心是"渐进式 + 可回滚 + 数据双写"。比例切流控风险,影子流量做验证,双写保证数据可切换,回滚靠阈值,一致性靠对账与事件补齐。

#
★★★

3. 分布式追踪中 traceId/spanId 如何跨服务传播,采样策略与 W3C trace context 的作用

请解释分布式追踪中 traceId/spanId 如何跨服务传播,说明采样策略与 W3C trace context 的作用?

  • traceId/spanId 的生成与传播(HTTP 头/消息头)
  • 采样策略(头采样/尾采样/概率采样)
  • W3C trace context(traceparent)标准化
  • traceId/spanId:一次完整请求对应一个 traceId(全局唯一,贯穿所有服务),每个处理单元是一个 span(带 spanId),span 之间有 parentSpanId 形成树形结构。span 记录开始/结束时间、标签、日志。
  • 跨服务传播:服务 A 生成 traceId 与 spanId,在处理完进入服务 B 前,把 traceId 和当前 span 的上下文(spanId)编码进 HTTP 头(如 traceparent/x-trace-id)或消息头,服务 B 收到后作为自己的 span 父节点继续生成子 span,从而把整条调用链串起来。
  • 采样策略:全量追踪开销大,通常采样。常见有头采样(在入口按 traceId 哈希或概率决定整条链路是否采样,保证链路完整)、尾采样(在出口按规则决定,可针对慢/错链路)、概率采样(固定比例)。采样需保证同一 traceId 的所有 span 一致采样,否则链路断裂。
  • W3C trace context:标准化的 traceparent 头(版本-trace-id-父span ID-采样标志),以及 tracestate 携带厂商附加信息。作用是让不同厂商的追踪系统(Jaeger/OpenTelemetry/Zipkin)互操作,统一传播格式。

traceId 贯穿全链路、spanId 定位单节点,靠 HTTP/消息头传播;采样在保证链路完整的前提下控制开销;W3C trace context 提供跨厂商的标准传播格式。

#
★★★

4. 配置中心与动态刷新中配置变更如何灰度发布与回滚,监听推送 vs 客户端轮询的实现差异

请解释配置中心如何实现动态刷新,说明配置变更的灰度发布与回滚机制,以及监听推送(push)与客户端轮询(poll)两种实现方式的差异?

  • 配置中心(Apollo/Nacos)的动态刷新
  • 灰度发布与回滚
  • 推送 vs 轮询的差异与取舍
  • 动态刷新:配置中心(如 Apollo/Nacos)集中管理配置,客户端通过长连接或监听感知配置变更,变更后实时刷新到本地生效,无需重启应用。
  • 灰度发布:配置变更先在小范围(某实例/某环境/按 IP 或用户)发布,验证无误后再全量;配合版本管理,任何环境可以回滚到之前版本。
  • 回滚:配置中心保留历史版本,发现异常可一键回滚到上一版,并广播给客户端重新拉取。

监听推送 vs 客户端轮询:

  • 推送(push):配置中心通过长连接(如 Nacos 的 gRPC 长连接、Apollo 的 long polling)主动通知客户端有变更,客户端再拉取最新配置。实时性好、网络开销小,但需要维持长连接,且有连接管理与推送失败重试的复杂度。
  • 轮询(poll):客户端定时(如每 30s)向配置中心拉取配置,简单可靠,但实时性差、有定时开销。长期轮询(long polling)是折中:客户端请求挂起,有变更才返回。
  • 实际多数采用"长轮询 + 变更通知 + 本地缓存",兼顾实时性与可靠性。

动态刷新的核心是"变更感知 + 本地热更新 + 版本回滚"。推送实时但复杂,轮询简单但延迟高,实际用长轮询/长连接折中。

#
★★

5. Saga 在订单场景的落地中下单 Saga 的步骤与补偿顺序如何设计(扣库存→扣款→失败回滚),超时与重试如何配置?

请解释 Saga 模式在订单场景的落地,说明下单 Saga 的步骤与补偿顺序如何设计(如扣库存→扣款→失败回滚),以及超时与重试如何配置?

  • Saga 的本地事务序列与补偿(compensation)
  • 补偿顺序设计(逆序撤销)
  • 超时与重试配置

Saga 模式:用一系列本地事务(每个服务一个事务)替代分布式事务,每个事务有对应的补偿(compensation)操作,失败时逆序执行补偿回滚。

下单 Saga 步骤设计(以扣库存→扣款→创建订单为例):

  1. 扣减库存(local tx)→ 补偿:回补库存
  2. 扣款(local tx)→ 补偿:退款
  3. 创建订单(local tx)→ 补偿:取消订单

补偿顺序:若第 3 步失败,则逆序执行第 2 步的补偿(退款)、第 1 步的补偿(回补库存)。要点:每个步骤必须是幂等的、补偿操作必须可执行(即使原步骤失败也要能补偿),补偿语义要"反向撤销"。

超时与重试配置:

  • 超时:每步设置超时,超时后按"已失败"处理并触发补偿;避免长时间等待。
  • 重试:对可重试的瞬时错误(网络抖动、超时)配置重试(指数退避 + 抖动),对不可重试错误(业务校验失败)直接进入补偿。
  • 幂等:每个步骤与补偿都要幂等,重试不会产生重复副作用。
  • 状态持久化:Saga 执行状态(当前步骤、已完成步骤)持久化,崩溃后可恢复并继续补偿。

Saga 的核心是"本地事务 + 逆序补偿"。补偿顺序总是与执行顺序相反,且每步幂等、可补偿;超时重试配合幂等保证最终一致。

#
★★

6. 分布式锁的租约与看门狗续期中锁过期后必须用 fencing token 防止旧持有者写入,Redlock 的争议点是什么?

请解释分布式锁的租约(lease)与看门狗(watchdog)续期机制,说明为什么锁过期后必须用 fencing token 防止旧持有者写入,以及 Redlock 的争议点?

  • 租约与看门狗续期
  • fencing token(隔离令牌)防止过期写入
  • Redlock 的争议(时钟漂移、网络分区)
  • 租约与看门狗:分布式锁通常设置租约(TTL),持锁者通过看门狗(watchdog)定期续期,防止持锁期间崩溃导致锁永远不释放。看门狗在持锁线程运行期间持续续期,线程结束则停止续期。
  • fencing token:锁过期后,旧持有者可能还在执行(只是没来得及续期),若它继续写共享资源,会与新持有者冲突。解决:每次获取锁时分配一个单调递增的 fencing token(版本号),写资源时携带该 token,服务端/存储校验 token 必须大于等于当前版本,否则拒绝。这样即使旧持有者锁已过期,也无法写入。
  • Redlock 的争议:Redlock 用多个独立 Redis 节点(N 个,多数派获取)实现分布式锁。争议点:Redis 无法保证底层时间一致性(时钟漂移),可能让锁在多个节点上同时有效;网络分区下无法保证正确性;作者 Martin Kleppmann 认为 Redlock 在存在 fencing/hybrid clock 时仍不安全,且 Redis 的锁不满足严格一致性。Antirez 则辩护其可用性。实际使用中,Redlock 可靠性依赖时钟与网络假设,严格场景需用 ZooKeeper/etcd 的线性一致性锁。

租约+看门狗解决"持锁崩溃不释放",fencing token 解决"过期锁旧持有者仍写入",Redlock 的争议本质是"在时钟漂移/网络分区下无法保证线性一致性"。

#
★★

7. Event Sourcing 的投影与重放中如何从事件流构建读模型,快照(snapshot)如何降低重放成本,事件版本化如何处理?

请解释 Event Sourcing 的投影与重放,说明如何从事件流构建读模型,快照(snapshot)如何降低重放成本,以及事件版本化如何处理?

  • 事件流作为唯一事实来源(source of truth)
  • 投影(projection)构建读模型
  • 快照降低重放成本与事件版本化
  • Event Sourcing:把状态变更记录为不可变的事件流(append-only),状态本身不直接存储,而是事件的累积结果。事件流是唯一事实来源。
  • 投影与重放:读模型(投影)通过从事件流起始(或从快照)依次重放(replay)事件,把事件应用到状态累加器,构建出当前状态/读模型。重放是"把事件序列还原为状态"的过程。
  • 快照(snapshot):从事件流开头重放越到后期成本越高,因此定期保存当前状态快照(含事件序号)。重放时从最近快照开始,只重放快照之后的事件,大幅降低重放成本。快照间隔按事件量/时间设定。
  • 事件版本化:事件结构会演进(如新增字段),需处理版本兼容。常见做法:事件带版本号(eventVersion),用事件反序列化器按版本解析;用"宽容读取"(schema evolution,新增字段给默认值);或做事件迁移(把旧版本事件重写为新版本)。保证旧事件可正确重放。

事件流是真相,投影从事件重放构建状态,快照是重放加速,版本化保证事件流可演进且可重放。

#
★★

8. 编排式(Orchestration)Saga 的控制器中如何集中维护各步骤状态并决定补偿顺序,与工作流引擎(Temporal)的关系?

请解释编排式(Orchestration)Saga 的控制器,说明它如何集中维护各步骤状态并决定补偿顺序,以及它与工作流引擎(如 Temporal)的关系?

  • 编排器(orchestrator)集中调度
  • 状态维护与补偿顺序决策
  • 与工作流引擎(Temporal)的关系
  • 编排式 Saga:由一个中心化的编排器(orchestrator)协调各步骤。编排器调用各服务(步骤),记录每个步骤的状态,失败时由编排器决定并执行补偿顺序。
  • 状态维护:编排器维护 Saga 的执行状态机(如:待执行→执行中→成功/失败→补偿中),记录每个步骤的执行结果。它会持久化状态,崩溃后可恢复。
  • 补偿顺序决策:编排器根据失败步骤,逆序回调已成功步骤的补偿操作。编排器集中了"哪步成功、哪步失败、如何补偿"的逻辑,逻辑清晰。
  • 与工作流引擎(Temporal)的关系:编排器本质上是一个工作流,Temporal 等工作流引擎提供了持久化执行、重试、超时、状态恢复等能力,是编排式 Saga 的理想实现载体。Temporal 用"确定性代码 + 事件持久化"保证工作流可重放恢复,编排器把 Saga 步骤写成工作流,靠引擎管理状态与补偿。也可用状态机库(如 Spring StateMachine)实现。

编排式 Saga 用中心控制器集中管理步骤状态与补偿决策,工作流引擎(Temporal)把这种编排做成可持久化、可恢复、可重试的工作流,降低自研编排器的复杂度。

#
★★

9. Outbox 模式的发布机制中轮询发布与 CDC(Debezium)的差异,事件表的分区/索引与清理,重复发布如何靠幂等消费兜底?

请解释 Outbox 模式的发布机制,说明轮询发布与 CDC(Debezium)的差异,事件表的分区索引与清理,以及重复发布如何靠幂等消费兜底?

  • Outbox 的本地事务双写(业务表 + outbox 表)
  • 轮询发布 vs CDC 发布
  • 事件表分区/索引/清理与幂等消费
  • Outbox 模式:业务事务中同时写业务数据和 outbox 事件表(同一数据库事务),保证两者原子提交;然后由独立进程把 outbox 事件发布到消息队列,避免"先写库后发消息"的分布式事务问题。
  • 轮询发布 vs CDC:
    • 轮询发布:独立进程定时(或按游标)查询 outbox 表未发布事件,发布后标记已发布。简单、可控,但实时性由轮询间隔决定,且可能重复读。
    • CDC(Change Data Capture,如 Debezium):监听数据库 binlog/WAL 变更,捕获 outbox 表的插入,实时推送事件到 MQ。实时性好、无侵入轮询,但引入 CDC 组件复杂度。
  • 事件表分区/索引与清理:outbox 表按时间/事件 ID 分区,按"未发布"状态建索引,定期清理已发布/过期事件(按时间删除),防止表无限增长。
  • 重复发布兜底:轮询或 CDC 都可能重复发布(如崩溃重试),因此消费者必须幂等消费——用事件唯一 ID(outbox 事件 ID)去重(去重表/唯一索引),或业务侧幂等,保证重复消费不产生副作用。

Outbox 用"同事务双写 + 独立发布"解决可靠发布,发布方式(轮询/CDC)权衡实时性与复杂度,表设计和幂等消费保证不丢不重。

#
★★

10. 两阶段提交与 Saga 的适用场景中强一致 vs 最终一致业务的取舍?

请对比两阶段提交(2PC)与 Saga 的适用场景,说明强一致 vs 最终一致业务的取舍?

  • 2PC 的强一致与阻塞问题
  • Saga 的最终一致与补偿
  • 选择依据(一致性要求、可用性、性能)
  • 两阶段提交(2PC):协调者(coordinator)先询问所有参与者(prepare 阶段),都同意后统一提交(commit 阶段)。保证强一致(原子性),但存在阻塞问题:prepare 后协调者崩溃会阻塞所有参与者,且锁持有时间长、性能差、可用性低,不适合跨服务/跨网络。
  • Saga:用本地事务序列 + 补偿,保证最终一致。不阻塞、可用性好、性能好,但中间状态对外可见,最终一致有短暂窗口,且补偿逻辑复杂。
  • 取舍:强一致业务(如跨库资金转账、金融对账)若必须在同一事务内保证原子性,且规模可控,考虑 2PC 或共享数据库;但现代分布式系统通常优先最终一致,用 Saga。选择依据:核心是"是否可接受最终一致"——可接受则用 Saga(更简单、高可用、高性能),必须强一致则 2PC 或同库事务。

2PC 强一致但阻塞脆弱,Saga 最终一致但灵活高效。实际业务多倾向 Saga + 补偿 + 幂等,把强一致需求收敛到单库/单事务内。

#
★★

11. Sidecar 模式中服务网格中数据面代理的部署与配置同步机制?

请解释 Sidecar 模式,说明服务网格中数据面代理(sidecar)的部署方式与配置同步机制?

  • Sidecar 与主容器同 Pod 部署
  • 数据面 vs 控制面
  • 配置下发与同步机制(xDS)
  • Sidecar 模式:把辅助功能(代理、日志、监控)以独立容器(sidecar)与主业务容器部署在同一 Pod/主机中,共享网络与文件系统,主容器通过 localhost 访问 sidecar。sidecar 与业务解耦,可独立升级。
  • 服务网格中的应用:istio/linkerd 通过 sidecar 注入(每个业务 Pod 注入一个 Envoy 代理),业务流量由 sidecar 拦截转发,实现 mTLS、路由、限流、可观测性,业务代码无需改动。数据面由各 sidecar 组成,控制面(Istiod)负责配置下发。
  • 配置同步机制:控制面(Istiod)通过 xDS 协议(LDS/RDS/CDS/EDS 等)向数据面 sidecar 下发配置(监听端口、路由规则、集群、端点)。sidecar(Envoy)订阅 xDS 流,增量更新,保证配置一致。控制面从注册中心/服务发现聚合端点,实时推送给各 sidecar。

Sidecar 把通用能力做成旁路代理,与业务同生命周期部署;服务网格用控制面通过 xDS 把配置同步到各数据面 sidecar,实现声明式网络治理。

#
★★

12. 多活与容灾中同城双活与两地三中心的复制拓扑、故障切换与数据丢失窗口

请解释多活与容灾方案,说明同城双活与两地三中心的复制拓扑、故障切换机制与数据丢失窗口?

  • 同城双活(RPO≈0、RTO 秒级)
  • 两地三中心的复制拓扑
  • 故障切换与数据丢失窗口(RPO/RTO)
  • 同城双活:两个数据中心在同一城市(低延迟互联),同时承载读写流量,互为备份。通过同步复制(如数据库 MS 同步、存储同步)保证两中心数据一致,RPO≈0(几乎不丢数据),故障切换时 RTO 秒级或分钟级。适合对可用性要求高的核心业务。
  • 两地三中心:一个"主数据中心"(同城双中心)+ 一个异地灾备中心。生产业务在 A、B 同城双中心双活,数据异步复制到异地 C 中心(灾备)。正常情况下 A/B 提供服务,C 只做备份;灾难时切到 C。异地复制是异步的,有数据丢失窗口(RPO 可能分钟级),RTO 较大(分钟到小时)。
  • 复制拓扑:同城用同步复制(强一致),异地用异步复制(最终一致)。故障切换:健康检查 + 仲裁/心跳探测,主故障切换流量到备,多活系统用全局负载均衡(GSLB)或 DNS 切换。
  • 数据丢失窗口:同步复制 RPO≈0,异步复制 RPO 有窗口(未复制的数据量),RTO 是切换时间。两者构成容灾的 RPO/RTO 指标。

多活是"双中心同时服务、同步复制",容灾是"异地异步备份",核心权衡是 RPO(数据丢失)与 RTO(恢复时间):同步复制 RPO 小但受距离延迟限制,异步复制 RPO 大但可覆盖远距离。

#
★★

13. 分布式任务调度中分片、抢占与幂等如何配合,为什么调度器本身不能成为单点

请解释分布式任务调度,说明分片、抢占与幂等如何配合,以及为什么调度器本身不能成为单点?

  • 任务分片(sharding)并行执行
  • 抢占(leader/failover)与容错
  • 幂等与调度器高可用
  • 分布式任务调度:把定时/批量任务分发给多个节点并行执行。核心组件:调度器(决定何时执行)与执行器(worker,实际执行)。
  • 分片(sharding):把大任务按数据维度(如按用户 ID/订单 ID 取模)切成多个分片,分发给不同 worker 并行处理,提高吞吐。分片策略要保证每个分片独立、可并行、结果可合并。
  • 抢占与容错:任务执行期间 worker 崩溃,任务需被重新调度(抢占/failover)到其他 worker 执行。通过 leader 选举 + 心跳 + 任务租约实现:worker 获取任务租约,崩溃后租约过期,调度器把任务重新分配。
  • 幂等:任务可能重复执行(failover 后重跑),因此任务处理必须幂等——按任务/分片 ID 去重,重复执行不产生重复副作用。
  • 调度器不能成为单点:若调度器单点,崩溃则整个系统的任务调度停摆。因此调度器本身要高可用,用 leader 选举(多个调度器节点,选一个主调度器,主故障自动切换),或把调度状态持久化到数据库/协调服务(etcd/ZooKeeper),保证任何节点崩溃都能接管。

分片提升并行度,抢占保证崩溃恢复,幂等保证重复安全,调度器高可用(leader 选举 + 状态持久化)避免单点。四者配合保证分布式任务可靠执行。

#

14. CQRS 的读写分离中命令侧写聚合、查询侧用读模型,投影(projection)如何异步更新读模型,与 Event Sourcing 的组合方式?

请解释 CQRS 的读写分离,说明命令侧写聚合、查询侧用读模型,投影(projection)如何异步更新读模型,以及它与 Event Sourcing 的组合方式?

  • CQRS 的命令/查询分离
  • 命令写聚合、查询读模型
  • 投影异步更新读模型与 ES 组合
  • CQRS(Command Query Responsibility Segregation):把写操作(Command)与读操作(Query)分离为不同的模型。写侧用领域模型(聚合),读侧用专门优化的读模型(投影表/视图)。
  • 命令侧写聚合:写操作通过命令(Command)修改聚合(Aggregate),校验业务规则,保证一致性;查询侧用读模型(如冗余列、宽表、物化视图)优化查询性能,两者互不干扰。
  • 投影(projection)异步更新读模型:写侧产生的事件/变更通过投影器(projection)异步更新读模型。投影器订阅事件流,把事件应用到读模型(如更新宽表、聚合统计),读模型最终一致,写侧与读侧通过事件解耦。
  • 与 Event Sourcing 组合:CQRS 常与 ES 结合——写侧用事件溯源(聚合状态由事件重放),命令产生的领域事件既是事实来源,也驱动投影构建读模型。写侧重一致性与审计,读侧重查询性能,二者解耦可独立扩展。

CQRS 的核心是"读写模型分离",投影把写侧事件异步同步到读模型实现最终一致;与 ES 组合时,事件流统一驱动写侧状态与读侧投影,扩展性最好但复杂度高。

#

15. BFF 模式的职责中移动端/Web 端需要各自后端聚合接口,BFF 如何做认证与裁剪,与 API Gateway 的边界?

请解释 BFF(Backend for Frontend)模式的职责,说明为什么移动端/Web 端需要各自后端聚合接口,BFF 如何做认证与裁剪,以及与 API Gateway 的边界?

  • BFF 为特定前端定制聚合接口
  • 认证与响应裁剪(payload 裁剪)
  • BFF 与 API Gateway 的边界
  • BFF(Backend for Frontend):为每种前端(移动端、Web、桌面)提供一个专属的后端聚合层,前端调用 BFF 获取该端需要的聚合数据,BFF 再调用下游微服务组装。因为不同前端的数据需求、网络、协议不同,用统一后端会导致接口冗余或返回过多字段。
  • 认证与裁剪:BFF 负责处理该前端的认证(登录态、token 校验、会话),把认证后的用户上下文传给下游服务;同时做响应裁剪(只返回该端需要的字段,减少移动端流量),以及接口聚合(一次请求聚合多个下游数据)。
  • 与 API Gateway 边界:API Gateway 是通用的流量入口,负责路由、限流、鉴权、负载均衡等横切关注点(面向所有客户端);BFF 是特定前端的业务聚合层,负责数据组装与裁剪。两者可叠加:Gateway 做通用网关能力,BFF 做前端定制聚合。BFF 偏业务、贴近前端,Gateway 偏基础设施、通用。

BFF 的核心是"为前端定制",解决"一个后端服务所有前端"的笨重问题;认证与裁剪是它的主要职责,与做通用网关能力的 API Gateway 在职责上互补而非替代。

#

16. 协同式(Choreography)Saga 的事件链中事件驱动如何解耦服务,故障定位为何更难,如何用事件追踪与幂等消费兜底?

请解释协同式(Choreography)Saga 的事件链,说明事件驱动如何解耦服务,故障定位为何更难,以及如何用事件追踪与幂等消费兜底?

  • 协同式 Saga 的事件驱动与无中心协调
  • 事件链解耦与故障定位难
  • 事件追踪 + 幂等消费兜底
  • 协同式 Saga:没有中心协调器,各服务通过发布/订阅事件协作。每个服务完成自己的本地事务后发布事件,下一个服务监听该事件执行下一步,形成事件链。例如:下单→ 发布"订单已创建"事件 → 库存服务监听后扣库存 → 发布"库存已扣"事件 → 支付服务监听后扣款。
  • 事件驱动解耦:服务之间不直接调用,只通过事件交互,服务间耦合低,可独立演进、独立部署。
  • 故障定位难:因为没有中心协调器,整个 Saga 的执行链路分散在多个服务的事件订阅中,一个事故可能由多个服务的事件消费共同造成,难以一眼定位"卡在哪一步、谁失败"。需要分布式追踪(traceId)与事件日志辅助定位。
  • 兜底:使用事件追踪(记录每个事件的发布/消费、traceId 贯穿)来还原链路;消费者幂等消费(事件唯一 ID 去重),保证事件重复投递不产生副作用;配合超时/重试与补偿事件(如发布"补偿库存"事件)实现回滚。

协同式 Saga 用事件链解耦服务,但代价是故障定位难、链路分散;靠事件追踪 + traceId + 幂等消费兜底,形成可观测、可恢复的最终一致。

#

17. 分布式锁的正确性中锁续期(watchdog)、fencing token 与可重入在分布式锁中的边界?

请解释分布式锁的正确性要求,说明锁续期(watchdog)、fencing token 与可重入在分布式锁中的边界?

  • 锁续期(watchdog)防止锁过期
  • fencing token 防止过期写入
  • 可重入的边界与安全
  • 锁续期(watchdog):分布式锁持有时长有限(TTL),若持锁线程执行时间超过 TTL 且未续期,锁会被自动释放,导致其他线程拿到锁。watchdog 定期续期,保证持锁线程运行期间锁一直有效,线程结束即停止续期。难点是区分"线程还在跑"与"线程已死"。
  • fencing token:锁过期后,即使 watchdog 续期,也可能因网络延迟/GC 停顿导致旧持有者仍在执行。解决:每次获取锁分配单调递增的 fencing token,写资源时携带,服务端校验 token 版本,拒绝过期 token。它是"锁过期后旧持有者不能写入"的终极保障。
  • 可重入:同一线程可重复获取同一把锁(重入计数)。分布式锁的可重入较难实现(需记录持有者身份与计数),且重入要谨慎:若重入计数与锁续期/释放逻辑不同步,可能引发锁泄漏。边界:可重入适合"同一线程内嵌套调用"需持锁的场景,但跨线程/跨进程重入不安全,且要配合持有者识别(如线程 ID/请求 ID)防止其他线程误释放。

分布式锁的正确性三角:watchdog 保活(防过期)、fencing token 隔离(防过期写入)、可重入(防嵌套死锁)。三者的边界要清晰,可重入尤其要结合持有者身份识别,避免误释放。

#

18. Leader 选举的租约机制中 etcd 或 ZooKeeper 的临时节点加会话超时如何实现故障转移,脑裂时如何保证单一领导?

请解释 Leader 选举的租约机制,说明 etcd 或 ZooKeeper 如何用临时节点加会话超时实现故障转移,以及脑裂时如何保证单一领导?

  • 临时节点 + 会话超时的租约
  • Leader 心跳续约与故障转移
  • 脑裂与多数派/仲裁保证单一领导
  • Leader 选举的精髓:让多个候选节点竞争成为唯一的 Leader,通过一致性协调服务(etcd/ZooKeeper)实现。
  • 临时节点 + 会话超时(ZooKeeper):候选节点创建"临时节点"(如 /leader),创建成功的节点成为 Leader;临时节点与会话绑定,会话超时(zk session timeout)则临时节点自动删除。Leader 通过心跳续约会话,若 Leader 崩溃/失联,会话超时,临时节点被删除,其他候选节点重新竞争创建,实现故障转移。
  • etcd 的租约:etcd 用 lease(租约)+ 心跳续约,Leader 通过 lease 持有 key,故障后 lease 过期释放,其他节点竞选。
  • 脑裂与单一领导:脑裂指网络分区导致多个节点都认为自己是 Leader。保证单一领导的机制是"多数派/仲裁":只有获得多数节点(quorum)确认的节点才能成为 Leader;写操作提交需多数派确认,因此即使分区,两个分区中只有一个能凑齐多数派成为有效 Leader,另一个分区无法提交,从而保证全局只有一个 Leader。etcd(Raft)与 ZooKeeper(ZAB)都依赖多数派仲裁。

临时节点 + 会话/租约机制保证"Leader 崩溃自动让位",多数派仲裁保证"脑裂时只有一个领导"。前者解决故障转移,后者解决单一领导。

#

19. Sidecar 与 Ambassador 中服务网格边车代理与 API 网关的部署位置与职责差异?

请对比 Sidecar 与 Ambassador 模式,说明服务网格边车代理与 API 网关的部署位置与职责差异?

  • Sidecar 与业务同部署(旁路)
  • Ambassador 作为入口代理(out-of-band)
  • 职责差异(东西向 vs 南北向)
  • Sidecar(边车):与业务服务部署在同一 Pod/进程,作为业务服务的旁路代理,拦截该服务的所有进出流量。用于服务间通信(东西向流量),实现 mTLS、负载均衡、熔断、可观测性。每个服务都有一个对应的 sidecar。
  • Ambassador(大使/代理):作为服务的"大使"向外接系统转发请求,通常部署在服务之外(out-of-band),作为服务的入口/出口代理。用于对外通信(南北向/外部集成),如代理对第三方 API 的访问。
  • 职责差异:Sidecar 管"服务与服务之间"(东西向),部署密度高(每个服务一个);Ambassador 管"服务与外部/入口"(南北向),部署在边界。API Gateway 是 Ambassador 的一种典型形态,做统一入口、路由、鉴权、限流。
  • 部署位置:Sidecar 与业务紧耦合(同生命周期),Ambassador/Gateway 独立部署(独立生命周期)。

Sidecar 是"内部旁路代理"(东西向、每服务一个),Ambassador 是"外部代理/网关"(南北向、独立部署)。职责不同导致部署位置与治理范围不同。

#

20. CQRS 与事件溯源中读写模型分离的扩展性收益与最终一致性的代价?

请解释 CQRS 与事件溯源(Event Sourcing)的组合,说明读写模型分离的扩展性收益与最终一致性的代价?

  • 读写模型分离的独立扩展
  • ES 提供完整审计与可重放
  • 最终一致性与复杂度代价
  • 组合方式:CQRS 把命令(写)与查询(读)分为不同模型;Event Sourcing 用事件流存储状态变更。两者结合:写侧用聚合 + 事件溯源,命令产生事件;读侧用投影从事件构建独立读模型。
  • 扩展性收益:读写模型可独立扩展(读侧可多副本、按需加投影,写侧单独扩容);查询侧可针对特定查询优化(宽表、物化视图、专用索引),避免写侧模型被查询拖累;事件流可重放重建任意读模型,支持审计与时间旅行。
  • 最终一致性的代价:写侧事件产生后,读模型通过投影异步更新,存在短暂不一致窗口(读到的可能是旧数据);需要处理投影失败、乱序、重复事件;需要额外的投影基础设施与事件存储;复杂度高,适用于读多写少、需要审计、高扩展性场景。

读写分离 + 事件溯源带来"独立扩展 + 完整审计 + 可重放",但代价是最终一致(读可能有延迟)与系统复杂度(事件存储、投影、幂等)。收益与代价需按业务权衡。

#

21. 密钥管理中 Secret 的集中存储与自动轮换如何避免硬编码,轮换期间新旧密钥如何共存

请解释密钥管理(Secret Management),说明 Secret 的集中存储与自动轮换如何避免硬编码,以及轮换期间新旧密钥如何共存?

  • 集中存储(Vault/KMS)避免硬编码
  • 自动轮换策略
  • 新旧密钥共存(双密钥窗口)
  • 集中存储:把密钥(密码、API key、证书)从代码/配置中抽离,集中存放到密钥管理系统(Vault、KMS、云 Secrets Manager),应用运行时通过 API 拉取,代码中不出现明文密钥,避免硬编码泄露。
  • 自动轮换:定时或按策略自动生成新密钥并替换旧密钥,降低密钥泄露风险。应用通过监听/拉取感知密钥变更,热更新到进程,无需重启。
  • 新旧密钥共存:轮换期间新旧密钥要共存一段时间("双密钥窗口"),因为已下发的旧密钥仍在被使用(如正在处理的请求、缓存的连接)。做法:发布明文列表用新密钥加密,同时保留旧密钥用于解密存量数据;验证/记录数据用 bucket 双验证(新旧都验证);解密支持新旧密钥,待存量彻底迁移后再移除旧密钥。逐密钥启动时间要错开,避免同时失效。

集中存储 + 自动轮换解决"硬编码与密钥泄露",新旧共存保证轮换期间不停服、不丢数据。核心是"双密钥窗口"的平滑过渡与最终清理。