From b11b27aee3f62e501c029f98ed9c0347aad0e6f9 Mon Sep 17 00:00:00 2001 From: coderbruis Date: Tue, 7 Jul 2026 23:25:39 +0800 Subject: [PATCH 01/21] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +- ...37\347\220\206\346\200\273\347\273\223.md" | 1865 +++++++++++++++++ 2 files changed, 1873 insertions(+), 1 deletion(-) create mode 100644 "note/kafka/kafka\346\234\200\346\226\260\347\211\210\346\234\254\346\240\270\345\277\203\346\246\202\345\277\265\344\270\216\346\240\270\345\277\203\345\216\237\347\220\206\346\200\273\347\273\223.md" diff --git a/README.md b/README.md index 7ca38a0..2dcbc08 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Java相关流行框架源码分析,学习以及总结,项目持续更新中 # 目录 - kafka源码分析 - - kafka版本:4.2 + - kafka版本:4.3 - [kafka 核心概念扫描](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/Kafka%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5%E6%89%AB%E7%9B%B2.md) - [kafka broker核心源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20broker%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) - [kafka broker核心源码分析——生产者篇](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20broker%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E2%80%94%E2%80%94%E7%94%9F%E4%BA%A7%E8%80%85%E7%AF%87.md) @@ -71,6 +71,13 @@ Java相关流行框架源码分析,学习以及总结,项目持续更新中 - [kafka Rebalance核心逻辑分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20rebalance%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91%E5%88%86%E6%9E%90.md) - [kafka ISR原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/Kafka%20ISR%20%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md) +重平衡 +![Kafka Cooperative Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_COOPERATIVE_rebalance.png) +![Kafka EAGER Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_EAGER_rebalance.png) + +ISR/HW/LEO关系图 +![Kafka_HW_ISR_LEO](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_HW_ISR_LEO.png) + - JDK源码学习 - JDK版本:1.8.0_77 - [深入学习String源码与底层(一)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0String%E6%BA%90%E7%A0%81%E4%B8%8E%E5%BA%95%E5%B1%82%EF%BC%88%E4%B8%80%EF%BC%89.md) diff --git "a/note/kafka/kafka\346\234\200\346\226\260\347\211\210\346\234\254\346\240\270\345\277\203\346\246\202\345\277\265\344\270\216\346\240\270\345\277\203\345\216\237\347\220\206\346\200\273\347\273\223.md" "b/note/kafka/kafka\346\234\200\346\226\260\347\211\210\346\234\254\346\240\270\345\277\203\346\246\202\345\277\265\344\270\216\346\240\270\345\277\203\345\216\237\347\220\206\346\200\273\347\273\223.md" new file mode 100644 index 0000000..8a4052f --- /dev/null +++ "b/note/kafka/kafka\346\234\200\346\226\260\347\211\210\346\234\254\346\240\270\345\277\203\346\246\202\345\277\265\344\270\216\346\240\270\345\277\203\345\216\237\347\220\206\346\200\273\347\273\223.md" @@ -0,0 +1,1865 @@ ++ 转载请标明出处 + + + +本章旨在快速扫盲\回顾 Apache Kafka 最新版本最核心、最重要的概念以及原理,不做具体框架源码级深入分析。 + +截至本文编写时间,Apache Kafka 官网下载页显示最新支持版本为 **Kafka 4.3.1**,发布日期为 **2026-06-25**。Kafka 4.x 之后的核心变化是 **ZooKeeper 架构退出主线,KRaft 成为 Kafka 元数据管理和集群控制的核心模式**。 + +参考来源: + ++ Apache Kafka Downloads: https://kafka.apache.org/community/downloads/ ++ Apache Kafka 4.3 Documentation: https://kafka.apache.org/43/ ++ Apache Kafka Documentation: https://kafka.apache.org/documentation/ + +重点覆盖: + ++ Kafka 是什么 ++ Topic、Partition、Replica、Broker ++ Producer、Consumer、Consumer Group ++ Offset、Log、Segment、Index ++ ISR、Leader、Follower、高水位 HW ++ Controller、KRaft、Metadata Quorum ++ 生产者发送流程、ACK、幂等、事务 ++ 消费者拉取流程、位移提交、Rebalance ++ 传统 Consumer Group 和新版 Consumer 协议 ++ Share Group / Queues for Kafka ++ 顺序性、可靠性、可用性、一致性 ++ Page Cache、顺序写、零拷贝 ++ Log Retention、Log Compaction ++ Tiered Storage ++ Kafka Connect、Kafka Streams ++ 安全机制、监控指标、常见问题排查 ++ 工程选型和实践建议 + +# Kafka +Kafka 是 **一个分布式事件流平台,用于高吞吐、低延迟、可持久化地发布、存储、订阅和处理事件数据**。 + +它主要解决: + +1. **系统解耦** +生产者只把消息写入 Kafka,不需要直接调用所有下游系统。 +2. **削峰填谷** +流量高峰先写入 Kafka,下游按自身能力消费。 +3. **数据持久化** +消息不是消费后立即删除,而是按保留策略存储一段时间。 +4. **实时数据流处理** +适合日志、埋点、监控、交易事件、CDC、实时计算。 +5. **多订阅方消费** +同一份数据可以被多个消费者组独立消费。 + +**传统同步调用** + +```plain +订单服务 + -> 库存服务 + -> 积分服务 + -> 风控服务 + -> 推荐服务 +``` + +调用链长,任何下游慢都会影响上游。 + +**使用 Kafka** + +```plain +订单服务 + -> 写入 order_created Topic + +库存服务消费 +积分服务消费 +风控服务消费 +推荐服务消费 +``` + +各系统通过事件解耦。 + +**需要注意** + ++ Kafka 不是传统意义上“发完即删”的简单消息队列。 ++ Kafka 的核心抽象是分布式日志。 ++ Kafka 强在高吞吐、可扩展、可回放、多消费者订阅。 ++ Kafka 不适合所有低延迟 RPC 场景,也不替代数据库事务。 ++ Kafka 4.x 新集群应优先理解 KRaft,而不是旧 ZooKeeper 架构。 + +一句话总结: + +**Kafka 是以分布式日志为核心的事件流平台,通过 Topic、Partition、Offset 和 Consumer Group 实现高吞吐、可持久化、可回放的数据流。** + +# Topic +Topic 是 **Kafka 中消息的逻辑分类,相当于一类事件流的名字**。 + +它主要解决: + +1. **按业务分类消息** +2. **隔离不同事件流** +3. **让生产者和消费者通过名称解耦** +4. **作为权限、保留策略、分区配置的管理单位** + +**例子** + +```plain +order_created +payment_success +user_login +stock_changed +app_log +``` + +**生产和消费** + +```plain +Producer -> order_created Topic +Consumer <- order_created Topic +``` + +**Topic 不是物理上的一个文件** + +Kafka 内部会把 Topic 拆成多个 Partition: + +```plain +order_created + -> partition-0 + -> partition-1 + -> partition-2 +``` + +**需要注意** + ++ Topic 命名要稳定,避免随意变更。 ++ Topic 数量过多会增加元数据和运维成本。 ++ Topic 的分区数、保留时间、清理策略要结合业务设计。 ++ 不同业务语义的数据不要随意混入同一个 Topic。 + +一句话总结: + +**Topic 是 Kafka 消息的逻辑分类,真正承载数据和并行能力的是 Topic 下的 Partition。** + +# Partition +Partition 是 **Topic 的物理分片,也是 Kafka 并行、顺序和扩展能力的基础单位**。 + +它主要解决: + +1. **单个 Topic 水平扩展** +2. **提升读写吞吐** +3. **让消费者并行消费** +4. **保证分区内消息顺序** + +**结构** + +```plain +Topic: order_created + +partition-0: + msg0, msg1, msg2 + +partition-1: + msg0, msg1, msg2 + +partition-2: + msg0, msg1, msg2 +``` + +**分区内有序** + +```plain +partition-0: + offset 0 + offset 1 + offset 2 +``` + +Kafka 只保证: + +```plain +同一个 Partition 内有序 +``` + +不保证: + +```plain +整个 Topic 全局有序 +``` + +**分区选择** + +```plain +有 key: + 根据 key hash 选择分区 + +无 key: + 按生产者分区策略分配 + +自定义: + 业务实现 Partitioner +``` + +**需要注意** + ++ 分区数决定消费者组内最大并行度。 ++ 分区数不是越多越好,过多会增加文件、网络和元数据成本。 ++ 扩容分区会影响 key 到分区的映射,可能破坏同 key 顺序。 ++ 需要同一业务实体有序时,应让同一 key 进入同一分区。 + +一句话总结: + +**Partition 是 Kafka 并行和顺序的核心单位,Kafka 保证分区内有序,不保证 Topic 全局有序。** + +# Broker +Broker 是 **Kafka 集群中的服务节点,负责接收生产请求、存储分区日志、处理消费请求和复制数据**。 + +它主要解决: + +1. **承载 Topic Partition 数据** +2. **处理客户端读写请求** +3. **参与副本复制** +4. **向集群汇报状态** +5. **作为 KRaft 节点参与元数据管理,取决于角色配置** + +**集群结构** + +```plain +Broker-1 + -> topicA partition-0 leader + -> topicB partition-1 follower + +Broker-2 + -> topicA partition-1 leader + -> topicA partition-0 follower + +Broker-3 + -> topicA partition-2 leader + -> topicB partition-0 follower +``` + +**Broker 角色** + +```plain +普通 Broker: + 处理数据读写 + +Controller: + 管理集群元数据和分区状态 + +Combined 节点: + 同时承担 Broker 和 Controller 角色 +``` + +**需要注意** + ++ Broker 宕机会触发副本 Leader 切换。 ++ 单个 Broker 磁盘、网络、CPU 都可能成为瓶颈。 ++ Broker 不是无状态服务,磁盘数据和 broker.id/node.id 很关键。 ++ 生产环境要合理规划磁盘、网络、机架和副本分布。 + +一句话总结: + +**Broker 是 Kafka 集群的数据服务节点,负责消息读写、日志存储、副本复制,并在 KRaft 架构中配合集群元数据管理。** + +# Replica +Replica 是 **Partition 的副本,用于提升 Kafka 数据可靠性和可用性**。 + +它主要解决: + +1. **Broker 宕机后的数据可用** +2. **降低单点故障风险** +3. **支持 Leader 故障切换** +4. **提升数据持久性** + +**副本结构** + +```plain +partition-0 + leader: Broker-1 + follower: Broker-2 + follower: Broker-3 +``` + +**Leader 副本** + +```plain +处理生产者写入 +处理消费者读取 +维护分区日志主流程 +``` + +**Follower 副本** + +```plain +从 Leader 拉取数据 +保持日志同步 +在 Leader 故障时可能被选为新 Leader +``` + +**副本因子** + +```plain +replication.factor = 3 +``` + +表示每个 Partition 有 3 个副本。 + +**需要注意** + ++ 副本越多,可靠性越好,但存储和复制成本越高。 ++ 生产常见副本因子是 3。 ++ Follower 默认不对普通消费者提供读服务。 ++ 副本分布要避免多个副本落在同一故障域。 + +一句话总结: + +**Replica 通过 Leader/Follower 副本复制提升 Kafka 的可靠性和可用性,生产环境通常使用 3 副本。** + +# ISR +ISR 是 **In-Sync Replicas,同步副本集合,表示当前跟得上 Leader 的副本列表**。 + +它主要解决: + +1. **判断哪些副本是健康同步副本** +2. **控制消息提交安全性** +3. **Leader 故障时选择可靠新 Leader** +4. **配合 acks 和 min.insync.replicas 保证写入可靠性** + +**结构** + +```plain +partition-0 + leader: Broker-1 + ISR: [Broker-1, Broker-2, Broker-3] +``` + +如果 Broker-3 落后太多: + +```plain +ISR: [Broker-1, Broker-2] +``` + +**写入确认** + +```plain +acks=all +min.insync.replicas=2 +``` + +含义: + +```plain +消息至少要被 ISR 中足够数量副本确认 +才认为写入成功 +``` + +**需要注意** + ++ ISR 不是固定副本列表,会动态变化。 ++ ISR 缩小通常表示副本复制延迟或故障。 ++ min.insync.replicas 设置过低会降低可靠性。 ++ 设置过高会降低可用性,副本不足时写入失败。 + +一句话总结: + +**ISR 表示当前与 Leader 保持同步的副本集合,是 Kafka 判断写入可靠性和故障切换安全性的核心机制。** + +# Offset +Offset 是 **消息在 Partition 内的递增位置编号**。 + +它主要解决: + +1. **标识消息位置** +2. **支持消费者断点续读** +3. **支持消息回放** +4. **让不同消费者组独立维护消费进度** + +**结构** + +```plain +partition-0: + offset 0 -> msgA + offset 1 -> msgB + offset 2 -> msgC +``` + +**消费者进度** + +```plain +consumer group A: + partition-0 committed offset = 2 + +consumer group B: + partition-0 committed offset = 0 +``` + +同一个 Topic 可以被多个消费者组独立消费。 + +**Offset 提交** + +```plain +消费消息 + -> 处理业务 + -> 提交 offset +``` + +**需要注意** + ++ Offset 只在单个 Partition 内有意义。 ++ 提交 offset 表示消费者认为之前的数据已经处理完成。 ++ 先提交再处理可能丢消息。 ++ 先处理再提交可能重复消费。 ++ Kafka 通常要求业务端做好幂等处理。 + +一句话总结: + +**Offset 是 Partition 内消息位置,消费者通过提交 Offset 记录消费进度,从而支持断点续读和消息回放。** + +# Log +Kafka Log 是 **Partition 在磁盘上的追加写日志,每条消息按 Offset 顺序追加到日志末尾**。 + +它主要解决: + +1. **消息持久化** +2. **顺序写入** +3. **按 Offset 查询** +4. **支持消息保留和回放** + +**日志结构** + +```plain +topic-partition/ + 00000000000000000000.log + 00000000000000000000.index + 00000000000000000000.timeindex + 00000000000000100000.log + 00000000000000100000.index + 00000000000000100000.timeindex +``` + +**追加写** + +```plain +Producer 写入 + -> Broker 追加到当前 active segment + -> 分配 offset + -> 等待确认 +``` + +**读取** + +```plain +Consumer 请求 offset + -> Broker 通过 index 定位 segment + -> 从 log 文件顺序读取 + -> 返回消息批次 +``` + +**需要注意** + ++ Kafka 不是随机更新消息,而是追加日志。 ++ 日志文件按 Segment 切分,便于删除、压缩和索引。 ++ 磁盘顺序写是 Kafka 高吞吐的重要原因。 ++ 日志保留时间到期后,旧消息会被清理,不能无限回放。 + +一句话总结: + +**Kafka 的核心存储模型是 Partition 追加日志,消息按 Offset 顺序写入磁盘,并通过 Segment 和索引支持高效读写。** + +# Segment 和 Index +Segment 是 **Kafka 把 Partition 日志切成多个文件段的机制**。 + +Index 是 **Kafka 用于快速定位 Offset 或时间戳对应日志位置的稀疏索引**。 + +它主要解决: + +1. **避免单个日志文件过大** +2. **方便按时间或大小清理旧数据** +3. **提升 Offset 定位效率** +4. **支持快速查找时间戳附近消息** + +**Segment 文件** + +```plain +00000000000000000000.log +00000000000000000000.index +00000000000000000000.timeindex +``` + +文件名前缀表示: + +```plain +该 Segment 起始 offset +``` + +**索引定位** + +```plain +Consumer 请求 offset=12345 + -> 找到起始 offset <= 12345 的 segment + -> 查 offset index 找近似位置 + -> 从 log 文件顺序扫描到目标消息 +``` + +**需要注意** + ++ Kafka 使用稀疏索引,不是每条消息都有索引。 ++ Segment 越大,文件数量少,但清理粒度更粗。 ++ Segment 越小,清理更灵活,但文件数量更多。 ++ 索引损坏通常可以根据 log 文件重建。 + +一句话总结: + +**Segment 把分区日志拆成可管理文件段,Index 用稀疏索引快速定位消息位置,是 Kafka 高效存储和清理的基础。** + +# Producer +Producer 是 **向 Kafka Topic 写入消息的客户端**。 + +它主要解决: + +1. **序列化业务数据** +2. **选择目标 Topic 和 Partition** +3. **批量发送消息** +4. **处理重试、幂等和事务** +5. **接收 Broker 写入确认** + +**发送流程** + +```plain +业务调用 send() + -> 序列化 key/value/header + -> 分区器选择 partition + -> 写入 RecordAccumulator + -> Sender 线程批量发送 + -> Broker 写入日志 + -> 返回 ack +``` + +**重要参数** + +```plain +acks: + 写入确认级别 + +batch.size: + 批次大小 + +linger.ms: + 等待聚合批次时间 + +compression.type: + 压缩方式 + +retries: + 重试次数 + +enable.idempotence: + 幂等生产 +``` + +**需要注意** + ++ batch.size 和 linger.ms 影响吞吐和延迟。 ++ 压缩可以降低网络和磁盘成本,但增加 CPU 开销。 ++ 重试可能导致乱序,幂等生产可以降低重复写入风险。 ++ 生产者发送成功不等于所有消费者已经消费。 + +一句话总结: + +**Producer 通过序列化、分区、批量、压缩、重试和 ACK 机制把业务事件高效可靠地写入 Kafka。** + +# ACK +ACK 是 **生产者写入消息时要求 Broker 返回确认的级别**。 + +它主要解决: + +1. **控制写入可靠性** +2. **平衡吞吐和数据安全** +3. **配合副本同步策略** + +**acks=0** + +```plain +Producer 不等待 Broker 确认 +吞吐高 +可能丢消息 +``` + +**acks=1** + +```plain +Leader 写入成功后返回 +性能较好 +Leader 宕机且 Follower 未同步时可能丢消息 +``` + +**acks=all** + +```plain +Leader 等待 ISR 副本满足要求后返回 +可靠性最高 +延迟更高 +``` + +**常见可靠配置** + +```plain +acks=all +min.insync.replicas=2 +replication.factor=3 +enable.idempotence=true +``` + +**需要注意** + ++ acks=all 还需要合理设置 min.insync.replicas。 ++ min.insync.replicas 过低会降低副本确认意义。 ++ 副本不足时,acks=all 可能导致写入失败。 ++ 高可靠配置会牺牲部分可用性和延迟。 + +一句话总结: + +**ACK 决定生产者等待到什么程度才认为写入成功,acks=all 配合 ISR 和 min.insync.replicas 是高可靠写入的核心。** + +# 幂等生产者 +幂等生产者是 **Kafka 通过 Producer ID、Producer Epoch 和 Sequence Number 避免生产者重试导致重复写入的机制**。 + +它主要解决: + +1. **网络超时后重试可能重复写入** +2. **Broker 已写入但 ACK 丢失** +3. **单分区内重试乱序和重复** + +**问题场景** + +```plain +Producer 发送 msg-1 + -> Broker 写入成功 + -> ACK 在网络中丢失 + -> Producer 重试 msg-1 + -> 可能重复写入 +``` + +**幂等机制** + +```plain +每个 Producer 有 PID +每个分区维护递增 sequence +Broker 检查 sequence 是否连续 +重复 sequence 被识别并丢弃 +``` + +**需要注意** + ++ 幂等生产者主要保证单 Producer 会话内、单分区的幂等写入。 ++ 它不等于业务幂等。 ++ 应用重启、业务重放、消费者重复处理仍要业务幂等。 ++ 跨分区、跨 Topic 的原子写入需要事务。 + +一句话总结: + +**幂等生产者通过 PID 和序列号避免发送重试造成的重复写入,是 Kafka 精确一次语义的重要基础。** + +# Kafka 事务 +Kafka 事务是 **让生产者把多条消息、多个分区写入和消费位移提交作为一个原子单元提交或回滚的机制**。 + +它主要解决: + +1. **跨分区原子写入** +2. **消费-处理-生产链路一致性** +3. **Exactly Once 语义基础** +4. **避免下游读到未提交事务消息** + +**典型流程** + +```plain +initTransactions +beginTransaction + -> consume input records + -> produce output records + -> sendOffsetsToTransaction +commitTransaction +``` + +失败时: + +```plain +abortTransaction +``` + +**读隔离级别** + +```plain +read_uncommitted: + 可以读到未提交事务消息 + +read_committed: + 只读已提交事务消息 +``` + +**需要注意** + ++ Kafka 事务解决 Kafka 内部读写链路的一致性,不自动保证外部数据库事务。 ++ 事务会增加协调成本和延迟。 ++ transactional.id 必须稳定且唯一。 ++ EOS 仍然要求业务端正确处理外部副作用。 + +一句话总结: + +**Kafka 事务把多分区写入和消费位移提交纳入同一原子单元,是 Kafka Exactly Once 处理链路的核心机制。** + +# Consumer +Consumer 是 **从 Kafka Topic 拉取并处理消息的客户端**。 + +它主要解决: + +1. **按 Offset 拉取消息** +2. **维护消费进度** +3. **参与消费者组分区分配** +4. **处理重试、提交和回放** + +**消费流程** + +```plain +Consumer 加入 Consumer Group + -> 获取分区分配 + -> poll 拉取消息 + -> 执行业务处理 + -> 提交 offset +``` + +**拉模式** + +Kafka 消费是客户端主动拉取: + +```plain +Consumer -> Broker: fetch request +Broker -> Consumer: records +``` + +优点: + +```plain +消费者按自身能力拉取 +容易做批量 +方便控制进度 +``` + +**需要注意** + ++ poll 要持续调用,否则可能被认为消费者失活。 ++ 消费处理慢会导致消费堆积。 ++ 自动提交 offset 简单但容易产生丢失或重复风险。 ++ 业务处理必须考虑重复消费。 + +一句话总结: + +**Consumer 通过 poll 主动拉取消息,并用 Offset 管理消费进度;消费可靠性核心在于处理和提交 Offset 的顺序。** + +# Consumer Group +Consumer Group 是 **Kafka 实现消费负载均衡和多订阅方隔离的机制**。 + +它主要解决: + +1. **同组消费者分摊分区消费** +2. **不同组独立消费同一 Topic** +3. **消费者故障后自动接管分区** +4. **支持水平扩展消费能力** + +**同组消费** + +```plain +Topic: order_created + partition-0 + partition-1 + partition-2 + +Group-A: + consumer-1 -> partition-0 + consumer-2 -> partition-1 + consumer-3 -> partition-2 +``` + +同一个消费者组内: + +```plain +一个 Partition 同一时刻通常只分配给一个 Consumer +``` + +不同消费者组: + +```plain +Group-A 可以消费一遍 +Group-B 也可以独立消费一遍 +``` + +**需要注意** + ++ 同一组消费者数量超过分区数时,多出来的消费者会空闲。 ++ 分区数决定同组最大并行消费数。 ++ 消费者上下线会触发 Rebalance。 ++ 不同业务系统通常应该使用不同 group.id。 + +一句话总结: + +**Consumer Group 让同一组消费者分摊分区消费,让不同组可以独立订阅同一份 Kafka 数据。** + +# Offset 提交 +Offset 提交是 **消费者把已处理到的位置记录到 Kafka 内部 Topic 中的机制**。 + +它主要解决: + +1. **消费者重启后从上次位置继续** +2. **消费者组故障恢复** +3. **消费进度可观测** +4. **支持手动回放和重置** + +**自动提交** + +```plain +enable.auto.commit=true +``` + +特点: + +```plain +使用简单 +可能处理前就提交 +异常时可能丢消息 +``` + +**手动提交** + +```plain +处理成功后 commitSync 或 commitAsync +``` + +常见流程: + +```plain +poll records + -> 处理业务 + -> 处理成功 + -> commit offset +``` + +**重复和丢失** + +```plain +先提交,再处理: + 处理失败会丢消息 + +先处理,再提交: + 提交失败会重复消费 +``` + +**需要注意** + ++ Kafka 默认更容易做到至少一次。 ++ 想避免业务重复,必须实现幂等。 ++ offset 提交的是下一条要消费的位置。 ++ 提交粒度过细影响性能,过粗增加重复范围。 + +一句话总结: + +**Offset 提交决定消费者故障恢复位置,可靠消费通常选择处理成功后再提交,并通过业务幂等接受重复消费。** + +# Rebalance +Rebalance 是 **消费者组成员或订阅分区变化时,重新分配 Partition 的过程**。 + +它主要解决: + +1. **消费者扩缩容** +2. **消费者故障接管** +3. **分区新增后的重新分配** +4. **消费负载均衡** + +**触发条件** + +```plain +消费者加入组 +消费者离开组 +消费者心跳超时 +Topic 分区变化 +订阅 Topic 变化 +``` + +**传统流程** + +```plain +暂停消费 + -> 组协调器确认成员 + -> 执行分区分配 + -> 通知消费者新分配 + -> 恢复消费 +``` + +**问题** + +```plain +消费暂停 +重复消费风险 +分区频繁迁移 +消费延迟抖动 +``` + +**优化方向** + +```plain +静态成员 +协作式再均衡 +新版消费者组协议 +合理 session.timeout.ms 和 max.poll.interval.ms +``` + +**需要注意** + ++ Rebalance 不是错误,但频繁 Rebalance 是问题。 ++ 处理时间超过 max.poll.interval.ms 可能被踢出消费者组。 ++ 消费者扩缩容会带来短暂消费抖动。 ++ 需要监控 Rebalance 次数和消费延迟。 + +一句话总结: + +**Rebalance 负责消费者组分区重新分配,是消费弹性和故障接管的基础,但频繁发生会造成延迟和重复消费风险。** + +# 新版 Consumer 协议 +新版 Consumer 协议是 **Kafka 近年来对消费者组协调和 Rebalance 流程的改进方向,目标是降低协调成本、减少客户端复杂度和提升 Rebalance 稳定性**。 + +它主要解决: + +1. **传统 Rebalance 停顿时间较长** +2. **客户端承担过多分配逻辑** +3. **成员变化时影响范围较大** +4. **大规模消费者组协调成本高** + +**传统模型** + +```plain +Consumer Leader 负责分区分配 +Group Coordinator 管理成员 +分配结果再同步给组成员 +``` + +**新版思路** + +```plain +Broker 侧承担更多协调职责 +协议更适合增量变化 +减少全组停顿 +降低客户端分配复杂度 +``` + +**需要注意** + ++ 不同 Kafka 客户端版本对新协议支持程度不同。 ++ 升级前要确认 broker、client、配置兼容性。 ++ 新协议优化 Rebalance,但不消除业务处理慢导致的消费堆积。 ++ 老版本消费者组仍然可能继续使用传统协议。 + +一句话总结: + +**新版 Consumer 协议把消费者组协调能力进一步服务端化,目标是减少大规模消费者组 Rebalance 的停顿和复杂度。** + +# Share Group +Share Group 是 **Kafka 4.x 引入的队列式消费能力方向,也常被称为 Queues for Kafka,用于让多个消费者共享处理同一分区中的不同消息**。 + +它主要解决: + +1. **传统消费者组并行度受分区数限制** +2. **队列场景下不希望过度增加分区** +3. **多个消费者共同处理同一分区消息** +4. **消息级确认和重投递需求** + +**传统 Consumer Group** + +```plain +一个 Partition 同一时刻分配给一个 Consumer +消费者数量超过分区数会空闲 +``` + +**Share Group** + +```plain +多个 Consumer 可以共享同一 Partition 的消息处理 +更接近队列语义 +支持消息级别的处理跟踪 +``` + +**适合场景** + +```plain +任务队列 +异步作业处理 +消费者数量动态变化 +不要求严格分区顺序的工作负载 +``` + +**需要注意** + ++ Share Group 更适合队列式任务,不是替代所有 Consumer Group。 ++ 如果业务强依赖分区内顺序,仍要谨慎使用。 ++ 客户端和 Broker 版本都要确认支持。 ++ 消息级确认也要求业务处理幂等。 + +一句话总结: + +**Share Group 让 Kafka 支持更接近队列的消费模型,突破传统消费者组并行度受分区数限制的问题。** + +# KRaft +KRaft 是 **Kafka Raft Metadata mode,用 Kafka 自身的 Raft 协议管理集群元数据,替代旧 ZooKeeper 架构**。 + +它主要解决: + +1. **去除 ZooKeeper 外部依赖** +2. **统一 Kafka 元数据管理** +3. **提升控制面扩展性和恢复速度** +4. **简化部署和运维** + +**旧架构** + +```plain +Broker + -> ZooKeeper 保存元数据 + -> Controller 监听和管理集群状态 +``` + +**KRaft 架构** + +```plain +Controller Quorum + -> 使用 Raft 管理元数据日志 + -> Broker 从 Controller 获取元数据 +``` + +**角色** + +```plain +broker: + 处理数据读写 + +controller: + 管理元数据、分区状态、Leader 选举 + +broker,controller: + 同一进程同时承担两种角色 +``` + +**需要注意** + ++ Kafka 4.x 新集群应按 KRaft 理解和部署。 ++ Controller Quorum 的节点数和可靠性非常关键。 ++ 元数据日志也需要持久化和备份意识。 ++ 从旧 ZooKeeper 集群迁移要严格遵循版本和迁移流程。 + +一句话总结: + +**KRaft 用 Kafka 自身的 Raft 元数据仲裁替代 ZooKeeper,是 Kafka 4.x 集群控制面的核心架构。** + +# Controller +Controller 是 **Kafka 集群控制面核心角色,负责管理元数据、分区 Leader、Broker 状态和集群变更**。 + +它主要解决: + +1. **分区 Leader 选举** +2. **Broker 上下线处理** +3. **Topic 和 Partition 元数据管理** +4. **副本状态变更** +5. **集群控制事件传播** + +**KRaft Controller Quorum** + +```plain +Controller-1 +Controller-2 +Controller-3 +``` + +通过 Raft 维护元数据日志: + +```plain +metadata log + -> topic 创建 + -> partition leader 变更 + -> broker 注册 + -> 配置变更 +``` + +**Broker 获取元数据** + +```plain +Broker 启动 + -> 注册到 Controller + -> 接收元数据变更 + -> 根据分区角色处理读写和复制 +``` + +**需要注意** + ++ Controller 不直接承载普通消息数据读写,主要负责控制面。 ++ Controller Quorum 不稳定会影响集群管理能力。 ++ Controller 与 Broker 的网络、磁盘、时间配置都要稳定。 ++ 小集群可以 combined 模式,大规模生产更建议角色隔离。 + +一句话总结: + +**Controller 是 Kafka 控制面核心,KRaft 模式下通过 Controller Quorum 和元数据日志管理整个集群状态。** + +# 高水位 HW +高水位 HW 是 **High Watermark,表示消费者可见的最高已提交 Offset 边界**。 + +它主要解决: + +1. **防止消费者读到未复制安全的数据** +2. **定义已提交消息范围** +3. **支持副本故障恢复** +4. **保证消费者读取的一致性** + +**简化示例** + +```plain +Leader log end offset = 10 +Follower-1 log end offset = 10 +Follower-2 log end offset = 8 + +HW = 8 +``` + +消费者只能读到: + +```plain +offset < HW +``` + +**为什么需要 HW** + +如果消费者读到 Leader 上还没复制到足够副本的数据: + +```plain +Leader 宕机 +新 Leader 没有这条消息 +消费者之前读到的数据消失 +``` + +HW 避免这种不一致。 + +**需要注意** + ++ HW 推进依赖副本复制进度。 ++ 副本落后会导致 HW 推进变慢。 ++ 消费延迟不只和消费者有关,也可能和副本同步有关。 ++ 事务场景还会涉及 LSO 等读可见边界。 + +一句话总结: + +**HW 定义消费者可见的已提交日志边界,确保消费者不会读到可能在故障切换中丢失的未安全复制数据。** + +# 顺序性 +Kafka 顺序性是 **Kafka 保证同一 Partition 内消息按 Offset 顺序追加和读取,但不保证 Topic 全局顺序**。 + +它主要解决: + +1. **同一业务实体事件有序** +2. **状态变更按顺序处理** +3. **避免乱序更新导致状态错误** + +**保证同 key 有序** + +```plain +key = orderId + +order_created +order_paid +order_shipped +``` + +通过 key hash 进入同一分区: + +```plain +partition-3: + order_created + order_paid + order_shipped +``` + +**无法天然保证** + +```plain +不同 Partition 之间的全局顺序 +多个 Producer 之间的严格全局顺序 +扩分区后的同 key 历史映射不变 +``` + +**需要注意** + ++ 需要有序的业务必须设计 key。 ++ 消费端并发处理同一分区消息也可能破坏业务顺序。 ++ 重试、异步处理、死信补偿都可能引入乱序。 ++ 全局有序通常意味着单分区,吞吐和可用性会受限。 + +一句话总结: + +**Kafka 只保证分区内顺序,业务要通过合理 key 设计把同一实体事件路由到同一分区。** + +# 可靠性 +Kafka 可靠性是 **消息从生产、存储、复制到消费处理过程中尽量不丢失、可恢复、可重试的能力**。 + +它主要依赖: + +```plain +replication.factor +acks +min.insync.replicas +ISR +幂等生产者 +事务 +手动提交 offset +消费者幂等 +``` + +**高可靠生产** + +```plain +replication.factor=3 +min.insync.replicas=2 +acks=all +enable.idempotence=true +``` + +**高可靠消费** + +```plain +关闭自动提交 +处理成功后提交 offset +业务幂等 +失败重试 +死信或补偿 +``` + +**需要注意** + ++ Kafka 能降低消息丢失概率,但不能替业务处理外部副作用兜底。 ++ 消费端提交 offset 和业务数据库事务之间仍可能不一致。 ++ 高可靠配置会增加延迟并降低部分故障场景下可用性。 ++ 消息至少一次通常比恰好一次更常见,业务幂等必须做。 + +一句话总结: + +**Kafka 可靠性来自副本、ACK、ISR、幂等、事务和正确的 Offset 提交,最终仍需要业务幂等和补偿闭环。** + +# Exactly Once +Exactly Once 是 **Kafka 在特定读写 Kafka 的处理链路中,通过幂等生产者和事务实现的一次性处理语义**。 + +它主要解决: + +1. **生产重试导致重复写入** +2. **消费后生产再提交 Offset 的一致性** +3. **流处理拓扑中重复输出** + +**典型场景** + +```plain +input-topic + -> Kafka Streams 处理 + -> output-topic +``` + +事务保证: + +```plain +输出消息 + + 输入 offset 提交 +作为一个事务提交 +``` + +**需要注意** + ++ Exactly Once 通常限定在 Kafka 到 Kafka 的处理链路内。 ++ 如果写外部数据库、调用 HTTP 接口,仍要外部系统支持幂等或事务。 ++ EOS 会带来额外协调开销。 ++ 不要把 Kafka EOS 理解成所有业务副作用绝对只发生一次。 + +一句话总结: + +**Kafka Exactly Once 主要保证 Kafka 内部消费-处理-生产链路的一致性,外部系统仍需要幂等和事务配合。** + +# Page Cache +Page Cache 是 **Linux 用内存缓存文件数据的机制,也是 Kafka 高吞吐的重要基础**。 + +它主要解决: + +1. **减少磁盘读写延迟** +2. **让顺序写更高效** +3. **利用操作系统缓存热点日志** +4. **降低 JVM 堆内缓存压力** + +**写入流程** + +```plain +Broker 追加消息 + -> 写入 OS Page Cache + -> 后台刷盘 +``` + +**读取流程** + +```plain +Consumer 拉取消息 + -> 如果日志在 Page Cache + -> 直接从内存读取 + -> 否则触发磁盘读取 +``` + +**为什么 Kafka 不把消息都放 JVM 堆** + +```plain +避免 GC 压力 +利用 OS Page Cache +支持大数据量日志缓存 +``` + +**需要注意** + ++ Kafka 写入返回不一定等于立刻 fsync 到磁盘。 ++ Page Cache 被挤压会影响消费读取性能。 ++ Broker 内存规划要给 Page Cache 留空间。 ++ 不要只看 JVM 堆,还要看系统可用内存和磁盘缓存。 + +一句话总结: + +**Kafka 高吞吐大量依赖 Linux Page Cache,合理的内存规划应给操作系统缓存留出足够空间。** + +# 顺序写和零拷贝 +顺序写和零拷贝是 **Kafka 高吞吐的重要底层优化思想**。 + +**顺序写** + +Kafka 追加日志: + +```plain +只在文件末尾追加 +避免大量随机写 +充分利用磁盘顺序写能力 +``` + +**零拷贝** + +消费发送数据时: + +```plain +磁盘文件 + -> Page Cache + -> Socket + -> 网卡 +``` + +减少: + +```plain +内核态到用户态拷贝 +用户态到内核态拷贝 +CPU 拷贝成本 +``` + +**典型机制** + +```plain +sendfile +transferTo +``` + +**需要注意** + ++ 零拷贝不是完全没有拷贝,而是减少不必要的数据拷贝。 ++ 压缩、加密、消息转换可能影响零拷贝路径。 ++ 顺序写不代表磁盘永远不是瓶颈。 ++ 网络带宽常常比磁盘更早成为 Kafka 瓶颈。 + +一句话总结: + +**Kafka 通过追加顺序写和零拷贝减少磁盘随机 IO 与 CPU 拷贝开销,从而获得高吞吐。** + +# Log Retention +Log Retention 是 **Kafka 按时间或大小保留消息日志,超过保留策略后清理旧 Segment 的机制**。 + +它主要解决: + +1. **控制磁盘空间** +2. **支持一段时间内消息回放** +3. **让 Kafka 成为持久事件日志** +4. **自动清理过期数据** + +**常见配置** + +```plain +retention.ms +retention.bytes +segment.ms +segment.bytes +``` + +**清理流程** + +```plain +Segment 达到保留条件 + -> 被标记为可删除 + -> 后台清理线程删除 +``` + +**需要注意** + ++ Retention 是按 Segment 粒度清理,不是逐条消息。 ++ 消费者太慢,消息可能在消费前被清理。 ++ 磁盘容量规划必须结合写入速率和保留时间。 ++ Retention 不等于归档,长期留存可考虑 Tiered Storage。 + +一句话总结: + +**Log Retention 通过时间和大小策略清理旧日志,是 Kafka 控制磁盘空间和支持有限回放窗口的核心机制。** + +# Log Compaction +Log Compaction 是 **Kafka 按 key 保留最新值、清理旧值的日志压缩机制**。 + +它主要解决: + +1. **保存最新状态** +2. **支持状态重建** +3. **减少相同 key 的历史冗余** +4. **适合变更日志和配置类数据** + +**普通日志** + +```plain +key=user1 value=A +key=user1 value=B +key=user1 value=C +``` + +压缩后至少保留: + +```plain +key=user1 value=C +``` + +**删除语义** + +```plain +key=user1 value=null +``` + +称为 tombstone,用于表示删除。 + +**适合场景** + +```plain +用户最新状态 +配置变更 +CDC upsert 事件 +状态存储 changelog +``` + +**需要注意** + ++ Compaction 不保证立刻清理。 ++ 同一个 key 的旧值在一段时间内仍可能存在。 ++ key 不能为空,否则无法按 key 压缩。 ++ 不适合需要完整历史审计的场景。 + +一句话总结: + +**Log Compaction 按 key 保留最新状态,适合状态重建和变更日志,不适合完整历史事件审计。** + +# Tiered Storage +Tiered Storage 是 **Kafka 把较旧日志从本地磁盘分层存储到远端存储的能力,用于降低本地磁盘压力并扩大保留窗口**。 + +它主要解决: + +1. **本地磁盘容量有限** +2. **长时间保留消息成本高** +3. **历史数据回放需求** +4. **冷热数据分层** + +**基本思路** + +```plain +热数据: + 保留在 Broker 本地磁盘 + +冷数据: + 上传到远端对象存储或远端存储系统 +``` + +消费旧数据时: + +```plain +Consumer 请求旧 offset + -> Broker 从远端读取 + -> 返回给 Consumer +``` + +**需要注意** + ++ Tiered Storage 会引入远端存储延迟和成本。 ++ 历史回放性能通常不如本地热数据。 ++ 远端存储可靠性和权限也要纳入运维。 ++ 要区分本地保留策略和远端保留策略。 + +一句话总结: + +**Tiered Storage 通过冷热分层把旧日志放到远端存储,扩大 Kafka 数据保留窗口并降低本地磁盘压力。** + +# Kafka Connect +Kafka Connect 是 **Kafka 的数据集成框架,用于把外部系统数据导入 Kafka,或把 Kafka 数据导出到外部系统**。 + +它主要解决: + +1. **数据库 CDC 接入** +2. **日志和文件采集** +3. **写入搜索、数仓、对象存储** +4. **减少重复开发导入导出程序** + +**Source Connector** + +```plain +外部系统 + -> Connector + -> Kafka Topic +``` + +例如: + +```plain +MySQL CDC -> Kafka +``` + +**Sink Connector** + +```plain +Kafka Topic + -> Connector + -> 外部系统 +``` + +例如: + +```plain +Kafka -> Elasticsearch +Kafka -> S3 +``` + +**需要注意** + ++ Connector 也要考虑 offset、重试、幂等和死信。 ++ Source 和 Sink 的一致性语义取决于外部系统能力。 ++ 大规模 Connect 集群要关注任务分配和 Rebalance。 ++ Connector 配置错误可能导致重复写入或数据延迟。 + +一句话总结: + +**Kafka Connect 用标准化 Connector 把 Kafka 与数据库、搜索、数仓、对象存储等系统连接起来,是数据集成的重要组件。** + +# Kafka Streams +Kafka Streams 是 **Kafka 官方 Java 流处理库,用于基于 Kafka Topic 构建实时处理应用**。 + +它主要解决: + +1. **流式转换** +2. **聚合和窗口** +3. **Join** +4. **状态ful 处理** +5. **Exactly Once 流处理** + +**基本模型** + +```plain +input topic + -> filter/map/groupBy/window/join + -> output topic +``` + +**状态存储** + +```plain +本地 State Store + -> changelog topic + -> 故障后从 changelog 恢复 +``` + +**常见场景** + +```plain +实时统计 +风控规则 +订单状态聚合 +用户行为窗口计算 +流表 Join +``` + +**需要注意** + ++ Kafka Streams 是库,不是独立集群计算框架。 ++ 应用实例数量和 Topic 分区数影响并行度。 ++ 状态存储需要磁盘和恢复时间规划。 ++ 复杂大规模计算可以评估 Flink、Spark Streaming 等框架。 + +一句话总结: + +**Kafka Streams 让应用直接基于 Kafka 构建实时流处理拓扑,适合轻量到中等复杂度的流式计算。** + +# 安全机制 +Kafka 安全机制包括 **认证、授权、加密和审计相关能力**。 + +它主要解决: + +1. **客户端身份确认** +2. **Topic 访问控制** +3. **网络传输加密** +4. **多租户隔离** +5. **操作审计** + +**常见能力** + +```plain +SSL/TLS: + 加密传输和证书认证 + +SASL: + 用户认证机制 + +ACL: + 资源访问控制 + +Principal: + 客户端身份 + +Authorizer: + 授权判断 +``` + +**ACL 示例语义** + +```plain +用户 userA + -> 允许写 Topic order_created + -> 允许读 Group inventory-service +``` + +**需要注意** + ++ 生产环境不要裸奔明文无认证。 ++ ACL 要按最小权限配置。 ++ 证书和密码要有轮换机制。 ++ 多租户 Kafka 要同时做配额、ACL 和 Topic 命名规范。 + +一句话总结: + +**Kafka 安全依赖 TLS、SASL、ACL 和配额等机制,核心目标是确认身份、限制权限和保护数据传输。** + +# 监控指标 +Kafka 监控是 **围绕 Broker、Topic、Partition、Producer、Consumer、Controller 和系统资源建立可观测性的过程**。 + +重点关注: + +```plain +Broker 存活 +Controller 状态 +Under Replicated Partitions +Offline Partitions +ISR 变化 +请求延迟 +生产吞吐 +消费吞吐 +Consumer Lag +磁盘使用率 +网络吞吐 +Page Cache 命中 +GC +``` + +**Consumer Lag** + +```plain +Lag = 最新 offset - 已提交 offset +``` + +表示: + +```plain +消费者落后多少消息 +``` + +**副本健康** + +```plain +Under Replicated Partitions > 0: + 有副本落后或不可用 + +Offline Partitions > 0: + 有分区不可服务 +``` + +**需要注意** + ++ 只看 Broker 是否存活不够。 ++ Lag 高可能是消费者慢,也可能是分区不均、下游慢、Rebalance 频繁。 ++ 磁盘满会直接影响 Broker 稳定性。 ++ Controller 和元数据仲裁健康在 KRaft 模式下非常重要。 + +一句话总结: + +**Kafka 监控要同时关注吞吐、延迟、Lag、副本健康、Controller、磁盘、网络和 JVM,不能只看进程存活。** + +# 常见问题排查 +Kafka 问题通常涉及客户端、Broker、网络、磁盘、Controller、消费者组和业务处理多个层面。 + +**消息堆积** + +排查方向: + +```plain +Consumer Lag 是否持续增长 +消费者实例数是否不足 +分区数是否限制并行度 +业务处理是否变慢 +下游数据库是否慢 +是否频繁 Rebalance +``` + +**生产延迟升高** + +排查方向: + +```plain +acks 配置是否变更 +ISR 是否缩小 +Broker 请求队列是否堆积 +磁盘 IO 是否变慢 +网络是否打满 +batch 和 linger 是否合理 +``` + +**频繁 Rebalance** + +排查方向: + +```plain +消费者是否频繁重启 +poll 间隔是否超过 max.poll.interval.ms +session.timeout.ms 是否太短 +GC 是否过长 +网络是否抖动 +``` + +**副本不同步** + +排查方向: + +```plain +Follower 网络是否慢 +Broker 磁盘 IO 是否慢 +复制线程是否繁忙 +Leader 写入压力是否过高 +跨机房复制是否延迟大 +``` + +**消息重复消费** + +排查方向: + +```plain +是否处理成功但 offset 提交失败 +是否 Rebalance 后重复拉取 +是否生产者重试重复写 +业务是否缺少幂等键 +``` + +**需要注意** + ++ 先区分生产端、Broker 端、消费端、下游业务端。 ++ Kafka 层成功不代表业务处理成功。 ++ 消息重复是常态风险,业务必须幂等。 ++ 排查要看时间线:配置变更、发布、流量峰值、Broker 事件。 + +一句话总结: + +**Kafka 排查要从生产、存储、复制、消费、下游和控制面逐层定位,Lag、ISR、Rebalance、磁盘和网络是最常见线索。** + +# 工程选型 +Kafka 适合高吞吐、可持久化、可回放的事件流场景,但不是所有异步通信都适合 Kafka。 + +**适合 Kafka** + +```plain +日志采集 +埋点数据 +CDC 数据流 +事件驱动架构 +实时计算输入 +多系统订阅同一事件 +高吞吐异步处理 +数据回放 +``` + +**不适合 Kafka** + +```plain +极低延迟 RPC +小规模简单任务队列 +强事务同步调用 +复杂延迟调度 +大量单条消息精细 ACK 的传统队列场景 +``` + +**选型关注** + +```plain +吞吐量 +消息保留时间 +顺序要求 +可靠性要求 +消费并行度 +运维能力 +客户端生态 +监控和告警 +``` + +**需要注意** + ++ Kafka 强项是事件流和日志,不是简单 RPC 替代品。 ++ 分区数、副本数、保留时间要在上线前规划。 ++ 高可靠要接受更高延迟和更复杂配置。 ++ 上线 Kafka 必须配套监控、容量规划和故障演练。 + +一句话总结: + +**Kafka 最适合高吞吐、可持久化、可回放、多订阅的事件流场景,选型时要重点评估顺序、可靠性、延迟、容量和运维成本。** + +# 总结 +Kafka 的核心不是简单消息队列,而是一个围绕分布式日志构建的事件流平台。 + +常见理解路径: + +```plain +基础模型: + Topic、Partition、Replica、Broker + +写入链路: + Producer、Partitioner、Batch、ACK、ISR、幂等、事务 + +存储模型: + Log、Segment、Index、Page Cache、顺序写、零拷贝 + +消费模型: + Consumer、Consumer Group、Offset、Rebalance、Share Group + +控制面: + KRaft、Controller、Metadata Quorum + +可靠性: + 副本、ISR、HW、acks、min.insync.replicas、事务 + +生态: + Kafka Connect、Kafka Streams、Tiered Storage + +运维: + Lag、ISR、Controller、磁盘、网络、GC、Rebalance +``` + +一句话总结: + +**Kafka 的本质是分布式、可复制、可持久化、可回放的日志系统;真正用好 Kafka,要理解分区日志、消费者组、副本同步、KRaft 控制面和端到端可靠性。** From 8a4c0aae19b709618efe9289f2ff17e38ea5d621 Mon Sep 17 00:00:00 2001 From: CoderBruis <37364336+coderbruis@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:27:09 +0800 Subject: [PATCH 02/21] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新kafka版本 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2dcbc08..9113c62 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ rocketmq - rocketmq + rocketmq

From c1c2302d3598e69ce42711d7868ad7498b259741 Mon Sep 17 00:00:00 2001 From: coderbruis Date: Tue, 7 Jul 2026 23:30:48 +0800 Subject: [PATCH 03/21] =?UTF-8?q?=E4=BC=98=E5=8C=96readme=E5=B8=83?= =?UTF-8?q?=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 251 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 142 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index 2dcbc08..7ba7704 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ ![JavaSourceCodeLearningImage](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/JavaSourceCodeLearningImage.png) +

Java Source Code Learning

+ +

+ 一份面向 Java 后端工程师的源码阅读地图:从 JDK / JUC 到 Spring、Netty、Kafka、RocketMQ,按核心链路拆解框架设计与底层实现。 +

+

JDK @@ -20,139 +26,166 @@ Netty - rocketmq + RocketMQ - rocketmq + Kafka

-Java相关流行框架源码分析,学习以及总结,项目持续更新中。 +## 项目亮点 -框架或者源码包括: +| 你能看到什么 | 重点能力 | +| --- | --- | +| JDK / JUC 源码 | 集合、并发、线程池、内存模型、CompletableFuture | +| Spring / SpringBoot 源码 | IOC 容器、事件机制、启动流程、自动装配、扩展点 | +| SpringAOP / Security / OAuth2 | 代理机制、过滤器链、安全认证授权主流程 | +| Netty 源码 | Reactor 模型、EventLoop、ChannelPipeline、网络通信链路 | +| Kafka / RocketMQ 源码 | Broker、Producer、Consumer、Rebalance、消息存储与复制 | -✅ JDK源码 +## 学习路线 -✅ JUC源码 +```text +JDK / JUC 基础源码 + ↓ +Spring 容器与扩展点 + ↓ +SpringBoot 启动与自动装配 + ↓ +Netty 网络通信模型 + ↓ +Kafka / RocketMQ 消息系统源码 +``` -✅ Spring源码 +## 为什么要读源码? -✅ SpringBoot源码 +源码学习不是为了记住每一行实现,而是为了把框架背后的设计选择看清楚。 -✅ SpringAOP源码 +当线上问题出现时,读过核心链路的人通常能更快定位边界、判断根因、验证假设;当自己做系统设计时,也更容易借鉴成熟框架在模块拆分、扩展点设计、并发控制、性能优化上的经验。 -✅ SpringSecurity源码 +这个项目会围绕主流 Java 后端框架的核心路径持续整理源码分析、学习笔记和关键图解。 -✅ SpringSecurity OAuth2源码 +## 精选图解 -✅ JDK源码 +### Kafka Rebalance -✅ Dubbo源码 +![Kafka Cooperative Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_COOPERATIVE_rebalance.png) -✅ Netty源码 +![Kafka EAGER Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_EAGER_rebalance.png) -✅ RocketMQ源码 +### Kafka ISR / HW / LEO -✅ kafka源码 +![Kafka_HW_ISR_LEO](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_HW_ISR_LEO.png) -> 为什么要分析、学习源码? +## 内容目录 - 学习框架源码不仅能帮助我们在实际问题出现时快速定位问题、理解根因并高效解决,还能深入掌握框架的整体架构设计思路与核心设计模式,从而提升自身的系统设计能力与架构思维。 -同时,通过学习优秀开源框架的底层实现,可以不断强化对复杂系统拆分、模块协作以及性能优化的理解,这对于个人技术能力的长期成长至关重要。因此,源码学习虽然过程相对枯燥,但这是提升架构设计能力的必经路径。只有持续积累,才能在实际系统设计与工程实践中做到真正的“游刃有余”。 +
+Kafka 源码分析 + +- Kafka 版本:4.3 +- [Kafka 核心概念扫盲](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/Kafka%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5%E6%89%AB%E7%9B%B2.md) +- [Kafka Broker 核心源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20broker%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) +- [Kafka Broker 源码分析:生产者篇](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20broker%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E2%80%94%E2%80%94%E7%94%9F%E4%BA%A7%E8%80%85%E7%AF%87.md) +- [Kafka 消费者核心源码分析(一)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%E6%B6%88%E8%B4%B9%E8%80%85%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%80%EF%BC%89.md) +- [Kafka Rebalance 核心逻辑分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20rebalance%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91%E5%88%86%E6%9E%90.md) +- [Kafka ISR 原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/Kafka%20ISR%20%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md) -# 目录 +
-- kafka源码分析 - - kafka版本:4.3 - - [kafka 核心概念扫描](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/Kafka%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5%E6%89%AB%E7%9B%B2.md) - - [kafka broker核心源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20broker%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) - - [kafka broker核心源码分析——生产者篇](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20broker%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E2%80%94%E2%80%94%E7%94%9F%E4%BA%A7%E8%80%85%E7%AF%87.md) - - [kafka消费者核心源码分析(一)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%E6%B6%88%E8%B4%B9%E8%80%85%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%80%EF%BC%89.md) - - [kafka Rebalance核心逻辑分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20rebalance%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91%E5%88%86%E6%9E%90.md) - - [kafka ISR原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/Kafka%20ISR%20%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md) +
+JDK / JUC 源码学习 -重平衡 -![Kafka Cooperative Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_COOPERATIVE_rebalance.png) -![Kafka EAGER Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_EAGER_rebalance.png) +- JDK 版本:1.8.0_77 +- [深入学习 String 源码与底层(一)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0String%E6%BA%90%E7%A0%81%E4%B8%8E%E5%BA%95%E5%B1%82%EF%BC%88%E4%B8%80%EF%BC%89.md) +- [深入学习 String 源码与底层(二)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0String%E6%BA%90%E7%A0%81%E4%B8%8E%E5%BA%95%E5%B1%82%EF%BC%88%E4%BA%8C%EF%BC%89.md) +- [深入解读 CompletableFuture 源码与原理](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E8%A7%A3%E8%AF%BBCompletableFuture%E6%BA%90%E7%A0%81%E4%B8%8E%E5%8E%9F%E7%90%86.md) +- [深入分析 ThreadLocal](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%88%86%E6%9E%90ThreadLocal.md) +- [深入学习 Java volatile 关键字](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Java%20volatile%E5%85%B3%E9%94%AE%E5%AD%97.md) +- [深入学习 Thread 底层原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Thread%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81.md) +- 深入学习 JDK1.7 / 1.8 HashMap 扩容原理 +- [开源项目里那些看不懂的位运算分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E9%87%8C%E9%82%A3%E4%BA%9B%E7%9C%8B%E4%B8%8D%E6%87%82%E7%9A%84%E4%BD%8D%E8%BF%90%E7%AE%97%E5%88%86%E6%9E%90.md) +- [ThreadPoolExecutor 源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90ThreadPoolExecutor%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md) -ISR/HW/LEO关系图 -![Kafka_HW_ISR_LEO](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_HW_ISR_LEO.png) +
+ +
+Spring 源码学习 + +- Spring 版本:5.2.1.RELEASE +- [深入 Spring 源码系列(一):在 IDEA 中构建 Spring 源码](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%E5%9C%A8IDEA%E4%B8%AD%E6%9E%84%E5%BB%BASpring%E6%BA%90%E7%A0%81.md) +- [深入 Spring 容器源码与时序图(上)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%E6%B7%B1%E5%85%A5Spring%E5%AE%B9%E5%99%A8%EF%BC%8C%E9%80%9A%E8%BF%87%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E5%92%8C%E6%97%B6%E5%BA%8F%E5%9B%BE%E6%9D%A5%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82Spring%E5%AE%B9%E5%99%A8%EF%BC%88%E4%B8%8A%EF%BC%89.md) +- [深入 Spring 容器源码与时序图(下)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%E6%B7%B1%E5%85%A5Spring%E5%AE%B9%E5%99%A8%EF%BC%8C%E9%80%9A%E8%BF%87%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E5%92%8C%E6%97%B6%E5%BA%8F%E5%9B%BE%E6%9D%A5%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82Spring%E5%AE%B9%E5%99%A8%EF%BC%88%E4%B8%8B%EF%BC%89.md) +- [深入 Spring 源码系列(补充篇):程序调用 Spring 源码](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E8%A1%A5%E5%85%85%E7%AF%87%EF%BC%89%E2%80%94%E2%80%94%E7%A8%8B%E5%BA%8F%E8%B0%83%E7%94%A8Spring%E6%BA%90%E7%A0%81.md) +- [从 Spring 源码中学习策略模式](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E4%BB%8ESpring%E6%BA%90%E7%A0%81%E4%B8%AD%E5%AD%A6%E4%B9%A0%E2%80%94%E2%80%94%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.md) + +
+ +
+SpringAOP 源码学习 + +- Spring 版本:5.2.1.RELEASE +- [深入学习 SpringAOP 源码(一):注册 AnnotationAwareAspectJAutoProxyCreator](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringAOP/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0SpringAOP%E6%BA%90%E7%A0%81%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%E6%B3%A8%E5%86%8CAnnotationAwareAspectJAutoProxyCreator.md) +- [深入学习 SpringAOP 源码(二):深入 AnnotationAwareAspectJAutoProxyCreator](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringAOP/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0SpringAOP%E6%BA%90%E7%A0%81%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%20%E6%B7%B1%E5%85%A5AnnotationAwareAspectJAutoProxyCreator.md) +- [深入学习 SpringAOP 源码(三):揭开 JDK 动态代理和 CGLIB 代理的神秘面纱](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringAOP/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0SpringAOP%E6%BA%90%E7%A0%81%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94%E6%8F%AD%E5%BC%80JDK%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E5%92%8CCGLIB%E4%BB%A3%E7%90%86%E7%9A%84%E7%A5%9E%E7%A7%98%E9%9D%A2%E7%BA%B1.md) + +
+ +
+SpringBoot 源码学习 + +- SpringBoot 版本:2.2.1.RELEASE +- [深入浅出 SpringBoot 源码:SpringFactoriesLoader](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94SpringFactoriesLoader.md) +- [深入浅出 SpringBoot 源码:监听器与事件机制](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94%E7%9B%91%E5%90%AC%E5%99%A8%E4%B8%8E%E4%BA%8B%E4%BB%B6%E6%9C%BA%E5%88%B6.md) +- [深入浅出 SpringBoot 源码:系统初始化器](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94%E7%B3%BB%E7%BB%9F%E5%88%9D%E5%A7%8B%E5%8C%96%E5%99%A8.md) +- [深入浅出 SpringBoot 源码:启动加载器](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94%E5%90%AF%E5%8A%A8%E5%8A%A0%E8%BD%BD%E5%99%A8.md) + +
+ +
+SpringSecurity / OAuth2 源码学习 + +- SpringSecurity 版本:5.1.0.RELEASE +- [深入浅出 SpringSecurity 和 OAuth2(一):初识 SpringSecurity](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%20%E5%88%9D%E8%AF%86SpringSecurity.md) +- [深入浅出 SpringSecurity 和 OAuth2(二):安全过滤器 FilterChainProxy](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%20%E5%AE%89%E5%85%A8%E8%BF%87%E6%BB%A4%E5%99%A8FilterChainProxy.md) +- [深入浅出 SpringSecurity 和 OAuth2(三):WebSecurity 建造核心逻辑](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94%20WebSecurity%E5%BB%BA%E9%80%A0%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91.md) +- [深入浅出 SpringSecurity 和 OAuth2(四):FilterChainProxy 过滤器链中的几个重要过滤器](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E5%9B%9B%EF%BC%89%E2%80%94%E2%80%94%20FilterChainProxy%E8%BF%87%E6%BB%A4%E5%99%A8%E9%93%BE%E4%B8%AD%E7%9A%84%E5%87%A0%E4%B8%AA%E9%87%8D%E8%A6%81%E7%9A%84%E8%BF%87%E6%BB%A4%E5%99%A8.md) + +
+ +
+Netty 底层源码解析 + +- Netty 版本:4.1.43.Final +- [Netty 概念扫盲](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E6%A6%82%E5%BF%B5%E6%89%AB%E7%9B%B2.md) +- [二进制运算以及源码、反码以及补码学习](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/%E4%BA%8C%E8%BF%9B%E5%88%B6.md) +- [Netty 源码包结构](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/Netty%E6%BA%90%E7%A0%81%E5%8C%85%E7%BB%93%E6%9E%84.md) +- [Netty 底层源码解析:EventLoopGroup](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/Netty%E4%B8%AD%E7%9A%84EventLoopGroup%E6%98%AF%E4%BB%80%E4%B9%88.md) +- [Netty 底层源码解析:初始 Netty 及其架构](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%88%9D%E5%A7%8BNetty%E5%8F%8A%E5%85%B6%E6%9E%B6%E6%9E%84.md) +- [Netty 底层源码解析:Netty 服务端启动分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-Netty%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%90%AF%E5%8A%A8%E5%88%86%E6%9E%90.md) +- [Netty 底层源码解析:NioEventLoop 原理分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-NioEventLoop%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md) +- [Netty 底层源码解析:ChannelPipeline 分析(上)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-ChannelPipeline%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%8A%EF%BC%89.md) +- [Netty 底层源码解析:ChannelPipeline 分析(下)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-ChannelPipeline%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%8B%EF%BC%89.md) +- [Netty 底层源码解析:NioServerSocketChannel 接受数据原理分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-NioServerSocketChannel%E6%8E%A5%E5%8F%97%E6%95%B0%E6%8D%AE%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md) +- Netty 底层源码解析:NioSocketChannel 接受、发送数据原理分析 +- Netty 底层源码解析:FastThreadLocal 原理分析 +- Netty 底层源码解析:内存分配原理分析 +- Netty 底层源码解析:RocketMQ 底层使用到的 Netty + +Netty 实战课相关代码位于 `Spring-Netty` 模块下的 `com/bruis/learnnetty/im` 包。 -- JDK源码学习 - - JDK版本:1.8.0_77 - - [深入学习String源码与底层(一)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0String%E6%BA%90%E7%A0%81%E4%B8%8E%E5%BA%95%E5%B1%82%EF%BC%88%E4%B8%80%EF%BC%89.md) - - [深入学习String源码与底层(二)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0String%E6%BA%90%E7%A0%81%E4%B8%8E%E5%BA%95%E5%B1%82%EF%BC%88%E4%BA%8C%EF%BC%89.md) - - [深入解读CompletableFuture源码与原理](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E8%A7%A3%E8%AF%BBCompletableFuture%E6%BA%90%E7%A0%81%E4%B8%8E%E5%8E%9F%E7%90%86.md) - - [深入分析ThreadLocal](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%88%86%E6%9E%90ThreadLocal.md) - - [深入学习Java volatile关键字](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Java%20volatile%E5%85%B3%E9%94%AE%E5%AD%97.md) - - [深入学习Thread底层原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Thread%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81.md) - - [深入学习JDK1.7、8 HashMap扩容原理]() - - [开源项目里那些看不懂的位运算分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E9%87%8C%E9%82%A3%E4%BA%9B%E7%9C%8B%E4%B8%8D%E6%87%82%E7%9A%84%E4%BD%8D%E8%BF%90%E7%AE%97%E5%88%86%E6%9E%90.md) - - [ThreadPoolExecutor源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90ThreadPoolExecutor%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md) - -- Spring源码学习 - - Spring版本:5.2.1.RELEASE - - - [深入Spring源码系列(一)——在IDEA中构建Spring源码](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%E5%9C%A8IDEA%E4%B8%AD%E6%9E%84%E5%BB%BASpring%E6%BA%90%E7%A0%81.md) - - [深入Spring源码系列(二)——深入Spring容器,通过源码阅读和时序图来彻底弄懂Spring容器(上)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%E6%B7%B1%E5%85%A5Spring%E5%AE%B9%E5%99%A8%EF%BC%8C%E9%80%9A%E8%BF%87%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E5%92%8C%E6%97%B6%E5%BA%8F%E5%9B%BE%E6%9D%A5%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82Spring%E5%AE%B9%E5%99%A8%EF%BC%88%E4%B8%8A%EF%BC%89.md) - - [深入Spring源码系列(二)——深入Spring容器,通过源码阅读和时序图来彻底弄懂Spring容器(下)](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%E6%B7%B1%E5%85%A5Spring%E5%AE%B9%E5%99%A8%EF%BC%8C%E9%80%9A%E8%BF%87%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E5%92%8C%E6%97%B6%E5%BA%8F%E5%9B%BE%E6%9D%A5%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82Spring%E5%AE%B9%E5%99%A8%EF%BC%88%E4%B8%8B%EF%BC%89.md) - - [深入Spring源码系列(补充篇)——程序调用Spring源码](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E6%B7%B1%E5%85%A5Spring%E6%BA%90%E7%A0%81%E7%B3%BB%E5%88%97%EF%BC%88%E8%A1%A5%E5%85%85%E7%AF%87%EF%BC%89%E2%80%94%E2%80%94%E7%A8%8B%E5%BA%8F%E8%B0%83%E7%94%A8Spring%E6%BA%90%E7%A0%81.md) - - [从Spring源码中学习——策略模式](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Spring/%E4%BB%8ESpring%E6%BA%90%E7%A0%81%E4%B8%AD%E5%AD%A6%E4%B9%A0%E2%80%94%E2%80%94%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F.md) - -- SpringAOP源码学习 - - Spring版本:5.2.1.RELEASE - - - [深入学习SpringAOP源码(一)——注册AnnotationAwareAspectJAutoProxyCreator](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringAOP/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0SpringAOP%E6%BA%90%E7%A0%81%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%E6%B3%A8%E5%86%8CAnnotationAwareAspectJAutoProxyCreator.md) - - [深入学习SpringAOP源码(二)—— 深入AnnotationAwareAspectJAutoProxyCreator](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringAOP/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0SpringAOP%E6%BA%90%E7%A0%81%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%20%E6%B7%B1%E5%85%A5AnnotationAwareAspectJAutoProxyCreator.md) - - [深入学习SpringAOP源码(三)——揭开JDK动态代理和CGLIB代理的神秘面纱](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringAOP/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0SpringAOP%E6%BA%90%E7%A0%81%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94%E6%8F%AD%E5%BC%80JDK%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E5%92%8CCGLIB%E4%BB%A3%E7%90%86%E7%9A%84%E7%A5%9E%E7%A7%98%E9%9D%A2%E7%BA%B1.md) - -- SpringBoot源码学习 - - SpringBoot版本:2.2.1.RELEASE - - - [深入浅出SpringBoot源码——SpringFactoriesLoader](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94SpringFactoriesLoader.md) - - [深入浅出SpringBoot源码——监听器与事件机制](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94%E7%9B%91%E5%90%AC%E5%99%A8%E4%B8%8E%E4%BA%8B%E4%BB%B6%E6%9C%BA%E5%88%B6.md) - - [深入浅出SpringBoot源码——系统初始化器](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94%E7%B3%BB%E7%BB%9F%E5%88%9D%E5%A7%8B%E5%8C%96%E5%99%A8.md) - - [深入浅出SpringBoot源码——启动加载器](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringBoot/%E6%B7%B1%E5%85%A5SpringBoot%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0%E4%B9%8B%E2%80%94%E2%80%94%E5%90%AF%E5%8A%A8%E5%8A%A0%E8%BD%BD%E5%99%A8.md) - -- SpringSecurity&OAuth2源码学习 - - SpringSecurity版本:5.1.0.RELEASE - - [深入浅出SpringSecurity和OAuth2(一)—— 初识SpringSecurity](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%B8%80%EF%BC%89%E2%80%94%E2%80%94%20%E5%88%9D%E8%AF%86SpringSecurity.md) - - [深入浅出SpringSecurity和OAuth2(二)—— 安全过滤器FilterChainProxy](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%BA%8C%EF%BC%89%E2%80%94%E2%80%94%20%E5%AE%89%E5%85%A8%E8%BF%87%E6%BB%A4%E5%99%A8FilterChainProxy.md) - - [深入浅出SpringSecurity和OAuth2(三)—— WebSecurity建造核心逻辑](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E4%B8%89%EF%BC%89%E2%80%94%E2%80%94%20WebSecurity%E5%BB%BA%E9%80%A0%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91.md) - - [深入浅出SpringSecurity和OAuth2(四)—— FilterChainProxy过滤器链中的几个重要的过滤器](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/SpringSecurity/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%B3%BB%E7%BB%9F%E5%AD%A6%E4%B9%A0SpringSecurity%E5%92%8COAuth2%EF%BC%88%E5%9B%9B%EF%BC%89%E2%80%94%E2%80%94%20FilterChainProxy%E8%BF%87%E6%BB%A4%E5%99%A8%E9%93%BE%E4%B8%AD%E7%9A%84%E5%87%A0%E4%B8%AA%E9%87%8D%E8%A6%81%E7%9A%84%E8%BF%87%E6%BB%A4%E5%99%A8.md) - -- Netty底层源码解析 - - Netty版本:4.1.43.Final - - [Netty概念扫盲](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E6%A6%82%E5%BF%B5%E6%89%AB%E7%9B%B2.md) - - [二进制运算以及源码、反码以及补码学习](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/%E4%BA%8C%E8%BF%9B%E5%88%B6.md) - - [Netty源码包结构](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/Netty%E6%BA%90%E7%A0%81%E5%8C%85%E7%BB%93%E6%9E%84.md) - - [Netty底层源码解析-EventLoopGroup](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/Netty%E4%B8%AD%E7%9A%84EventLoopGroup%E6%98%AF%E4%BB%80%E4%B9%88.md) - - [Netty底层源码解析-初始Netty及其架构](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%88%9D%E5%A7%8BNetty%E5%8F%8A%E5%85%B6%E6%9E%B6%E6%9E%84.md) - - [Netty底层源码解析-Netty服务端启动分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-Netty%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%90%AF%E5%8A%A8%E5%88%86%E6%9E%90.md) - - [Netty底层源码解析-NioEventLoop原理分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-NioEventLoop%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md) - - [Netty底层源码解析-ChannelPipeline分析(上)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-ChannelPipeline%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%8A%EF%BC%89.md) - - [Netty底层源码解析-ChannelPipeline分析(下)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-ChannelPipeline%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%8B%EF%BC%89.md) - - [Netty底层源码解析-NioServerSocketChannel接受数据原理分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-NioServerSocketChannel%E6%8E%A5%E5%8F%97%E6%95%B0%E6%8D%AE%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md) - - Netty底层源码解析-NioSocketChannel接受、发送数据原理分析 - - Netty底层源码解析-FastThreadLocal原理分析 - - Netty底层源码解析-内存分配原理分析 - - Netty底层源码解析-RocketMQ底层使用到的Netty - -Netty实战课相关点位于:Spring-Netty,com/bruis/learnnetty/im包下,有需要的读者可前往查看。 - - -- RocketMQ底层源码解析 - - RocketMQ版本:4.9.0 - - RocketMQ底层源码解析-RocketMQ环境搭建 - - RocketMQ底层源码解析-本地调试RocketMQ源码 - - RocketMQ底层源码解析-NameServer分析 - - 持续更新中... - - - -# 支持 - - 原创不易,各位帅哥美女star支持下... +
+
+RocketMQ 底层源码解析 + +- RocketMQ 版本:4.9.0 +- RocketMQ 底层源码解析:RocketMQ 环境搭建 +- RocketMQ 底层源码解析:本地调试 RocketMQ 源码 +- RocketMQ 底层源码解析:NameServer 分析 +- 持续更新中... + +
+ +## 支持 +如果这个项目对你有帮助,欢迎 Star。源码学习是长期工程,我会持续补充更多核心链路分析、图解和实践示例。 From f64c6b7eab6b8086d80c54443f95e0f85d5839b2 Mon Sep 17 00:00:00 2001 From: coderbruis Date: Tue, 7 Jul 2026 23:32:54 +0800 Subject: [PATCH 04/21] merged --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7ba7704..ceb6041 100644 --- a/README.md +++ b/README.md @@ -29,17 +29,17 @@ RocketMQ - Kafka + Kafka

## 项目亮点 -| 你能看到什么 | 重点能力 | -| --- | --- | -| JDK / JUC 源码 | 集合、并发、线程池、内存模型、CompletableFuture | -| Spring / SpringBoot 源码 | IOC 容器、事件机制、启动流程、自动装配、扩展点 | -| SpringAOP / Security / OAuth2 | 代理机制、过滤器链、安全认证授权主流程 | +| 你能看到什么 | 重点能力 | +| --- |--------------------------------------------| +| JDK / JUC 源码 | 集合、并发、线程池、内存模型、CompletableFuture | +| Spring / SpringBoot 源码 | IOC 容器、事件机制、启动流程、自动装配、扩展点 | +| SpringAOP / Security / OAuth2 | 代理机制、过滤器链、安全认证授权主流程 | | Netty 源码 | Reactor 模型、EventLoop、ChannelPipeline、网络通信链路 | | Kafka / RocketMQ 源码 | Broker、Producer、Consumer、Rebalance、消息存储与复制 | From 4c51768d5e0968484c61f7a041506bef41838a7e Mon Sep 17 00:00:00 2001 From: CoderBruis <37364336+coderbruis@users.noreply.github.com> Date: Wed, 8 Jul 2026 06:32:36 +0800 Subject: [PATCH 05/21] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index ceb6041..174d040 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -![JavaSourceCodeLearningImage](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/images/JavaSourceCodeLearningImage.png) -

Java Source Code Learning

From b41b9afef32065e2f2ddc2052cc22d399920438f Mon Sep 17 00:00:00 2001 From: coderbruis Date: Thu, 9 Jul 2026 22:39:05 +0800 Subject: [PATCH 06/21] =?UTF-8?q?=E8=A1=A5=E5=85=85kafka=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 174d040..01817ba 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,9 @@ Kafka / RocketMQ 消息系统源码 ## 精选图解 +### Kafka架构图 +![kafka_architecture](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_architecture.png) + ### Kafka Rebalance ![Kafka Cooperative Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_COOPERATIVE_rebalance.png) From de5de4482c23c14a6d41269159433be351358d9b Mon Sep 17 00:00:00 2001 From: coderbruis Date: Fri, 10 Jul 2026 20:21:27 +0800 Subject: [PATCH 07/21] =?UTF-8?q?=E6=9B=B4=E6=96=B0HashMap=E5=8E=9F?= =?UTF-8?q?=E7=90=86=E5=88=86=E6=9E=90=E6=96=87=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 32 +- ...20\347\240\201\345\210\206\346\236\220.md" | 630 ++++++++++++++++++ 2 files changed, 646 insertions(+), 16 deletions(-) create mode 100644 "note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md" diff --git a/README.md b/README.md index 01817ba..5fffc49 100644 --- a/README.md +++ b/README.md @@ -63,21 +63,6 @@ Kafka / RocketMQ 消息系统源码 这个项目会围绕主流 Java 后端框架的核心路径持续整理源码分析、学习笔记和关键图解。 -## 精选图解 - -### Kafka架构图 -![kafka_architecture](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_architecture.png) - -### Kafka Rebalance - -![Kafka Cooperative Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_COOPERATIVE_rebalance.png) - -![Kafka EAGER Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_EAGER_rebalance.png) - -### Kafka ISR / HW / LEO - -![Kafka_HW_ISR_LEO](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_HW_ISR_LEO.png) - ## 内容目录

@@ -91,6 +76,18 @@ Kafka / RocketMQ 消息系统源码 - [Kafka Rebalance 核心逻辑分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20rebalance%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91%E5%88%86%E6%9E%90.md) - [Kafka ISR 原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/Kafka%20ISR%20%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md) +Kafka架构图 +![kafka_architecture](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_architecture.png) + +Kafka Rebalance流程图 +![Kafka Cooperative Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_COOPERATIVE_rebalance.png) + +![Kafka EAGER Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_EAGER_rebalance.png) + +Kafka ISR / HW / LEO关系图 +![Kafka_HW_ISR_LEO](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_HW_ISR_LEO.png) + +
@@ -103,10 +100,13 @@ Kafka / RocketMQ 消息系统源码 - [深入分析 ThreadLocal](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%88%86%E6%9E%90ThreadLocal.md) - [深入学习 Java volatile 关键字](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Java%20volatile%E5%85%B3%E9%94%AE%E5%AD%97.md) - [深入学习 Thread 底层原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Thread%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81.md) -- 深入学习 JDK1.7 / 1.8 HashMap 扩容原理 +- [深入学习HashMap 底层源码与原理]() - [开源项目里那些看不懂的位运算分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E9%87%8C%E9%82%A3%E4%BA%9B%E7%9C%8B%E4%B8%8D%E6%87%82%E7%9A%84%E4%BD%8D%E8%BF%90%E7%AE%97%E5%88%86%E6%9E%90.md) - [ThreadPoolExecutor 源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90ThreadPoolExecutor%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md) +HashMap原理图 +![HashMap原理图](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/HashMap.png) +
diff --git "a/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md" new file mode 100644 index 0000000..61386e6 --- /dev/null +++ "b/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md" @@ -0,0 +1,630 @@ +当前HashMap版本:JDK1.8 + +# HashMap的重要成员变量以及内部类 +> **默认容量:DEFAULT_INITIAL_CAPACITY** +> + +```java +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 +``` + +当HashMap没有设置大小时,调用HashMap的put方法时,会进行初始值,并使用DEFAULT_INITIAL_CAPACITY设置默认大小。调用位置在: + +```java +final Node[] resize() { + ... + else { // zero initial threshold signifies using defaults + // HashMap默认大小,16 + newCap = DEFAULT_INITIAL_CAPACITY; + // 扩容阈值,12 + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + ... +} +``` + + + +> **扩容阈值:DEFAULT_LOAD_FACTOR** +> + +```java +static final float DEFAULT_LOAD_FACTOR = 0.75f; +``` + +在resize()中可以看到,当HashMap首次添加元素,调用put时,会计算第一次扩容阈值12,也就是说HashMap中元素=12即触发扩容。 + + + +扩容实际发生在putVal()中,源码如下: + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + ... + if (++size > threshold) + resize(); + ... +} +``` + +当HashMap中添加了新元素,size递增之后判断是否大于扩容阈值。 + + + +> **树化相关配置:TREEIFY_THRESHOLD、UNTREEIFY_THRESHOLD、MIN_TREEIFY_CAPACITY** +> + +```java +// 桶内链表长度达到 8,考虑红黑树化 +static final int TREEIFY_THRESHOLD = 8; +// 数组容量至少 64,才真正允许红黑树化 +static final int UNTREEIFY_THRESHOLD = 6; +// 红黑树节点减少到 6 个或更少,考虑退化回链表 +static final int MIN_TREEIFY_CAPACITY = 64; +``` + +链表长到 8 时,如果数组容量小于 64,先扩容;如果数组容量已经至少 64,才转红黑树。树节点少到 6 时,再退化回链表。 + + + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + ... + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + ... +} + +final void treeifyBin(Node[] tab, int hash) { + ... + if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) + // 先扩容 + resize(); + else if ((e = tab[index = (n - 1) & hash]) != null) { + // 红黑树化 + } + ... +} +``` + +可以看到,当HashMap中元素小于MIN_TREEIFY_CAPACITY时,是先进行的扩容,而非直接红黑树化。 + + + +> **HashMap内部类Node** +> + +这个类就是HashMap桶中存储的基本单元类,HashMap其实可以理解为一个数组,数组里每个位置叫一个桶bucket。 + +```java +static class Node implements Map.Entry { + final int hash; + final K key; + V value; + Node next; +} +``` + +在HashMap中通过一个Node数组来存储Node节点。 + +```java +transient Node[] table; +``` + +如果多个 key 经过 hash 计算后落到同一个桶,就会通过 `next` 串起来,形成链表: + +```plain +table[3] + | + v +Node(key1, value1) + | + next + v +Node(key2, value2) + | + next + v +Node(key3, value3) +``` + +HashMap 的每个桶位保存一个头节点引用;发生 hash 冲突时,新节点通过 next 挂在这个桶的链表后面;链表过长时可能转成红黑树。 + +还有一个细节需要注意,Node不仅存了key、value最核心的键值对信息,还存储了这个Node的hash值,这个hash值的作用是:**用于快速比较、查找、扩容迁移和树化查找,避免反复计算 hashCode,也保证节点定位稳定。** + +hash核心作用代码: + +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + ... + Node e; K k; + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + ... +} +``` + +putVal() 里比较 key 是否已经存在,这里先比较hash值,如果hash值不一样,则key一定不相等,可以直接跳过,避免频繁调用 equals()。 + + + +查找时也会用到hash: + +```java +final Node getNode(int hash, Object key) { + ... + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + ... +} +``` + +**总结:Node.hash 是 key 的缓存 hash 值,用于快速比较、查找、扩容迁移和树化查找,避免反复计算 hashCode,也保证节点定位稳定。** + + + + + + + +# HashMap核心方法源码分析 +## putVal() +```java +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + + // tab:HashMap 底层数组 + // p:当前桶的第一个节点 + // n:数组长度 + // i:key 对应的桶下标 + Node[] tab; Node p; int n, i; + + // 如果 table 还没初始化,或者长度为 0,则先 resize 初始化数组 + // new HashMap<>() 第一次 put 时,会在这里创建默认长度 16 的数组 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + + // 计算桶下标:(n - 1) & hash + // 因为 n 是 2 的幂,所以等价于 hash % n,但效率更高 + // 如果这个桶为空,直接放入新 Node + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + + else { + // e:最终找到的旧节点;如果为 null,说明是新增 key + // k:临时保存已有节点的 key + Node e; K k; + + // 先检查桶中第一个节点是否就是目标 key + // 先比 hash,再比 key 引用或 equals + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + + // 如果桶已经是红黑树结构,走红黑树插入/查找逻辑 + else if (p instanceof TreeNode) + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + + else { + // 桶是普通链表,遍历链表 + for (int binCount = 0; ; ++binCount) { + + // 如果下一个节点为空,说明没有找到相同 key + // 把新节点追加到链表尾部 + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + + // 链表长度达到树化阈值 8 时,尝试树化 + // 注意:treeifyBin 内部还会判断 table 长度是否 >= 64 + // 如果小于 64,优先扩容,不会真正树化 + if (binCount >= TREEIFY_THRESHOLD - 1) + treeifyBin(tab, hash); + + break; + } + + // 找到 hash 和 key 都相同的旧节点,停止遍历 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + + // 继续向后遍历链表 + p = e; + } + } + + // e != null 表示找到了旧 key,不是新增,而是更新 value + if (e != null) { + V oldValue = e.value; + + // onlyIfAbsent 为 false:直接覆盖旧值 + // onlyIfAbsent 为 true:只有旧值为 null 时才覆盖 + // put() 传 false,putIfAbsent() 传 true + if (!onlyIfAbsent || oldValue == null) + e.value = value; + + // LinkedHashMap 扩展点:访问节点后回调 + // HashMap 中是空实现 + afterNodeAccess(e); + + // 返回旧值 + return oldValue; + } + } + + // 结构性修改次数 +1 + // 用于 fail-fast,比如迭代时检测并发修改 + ++modCount; + + // 新增节点后 size +1 + // 如果 size 超过扩容阈值 threshold,则扩容 + if (++size > threshold) + resize(); + + // LinkedHashMap 扩展点:插入节点后回调 + // HashMap 中是空实现 + afterNodeInsertion(evict); + + // 新增 key 时返回 null + return null; +} +``` + +从源码中可以看到几个细节。 + +### 1)**JDK8 的 HashMap 链表插入用的是尾插法**。 +```java +if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + ... + break; +} +``` + +```plain +table[i] -> A -> B +``` + +插入新元素之后 + +```plain +table[i] -> A -> B -> C +``` + +对比 JDK7,JDK7 HashMap 扩容迁移时使用头插法,可能在并发扩容下形成环链表,导致死循环。JDK8 改了扩容迁移逻辑,并且普通链表插入也是尾插,能保持链表相对顺序。 + +JDK8 HashMap 仍然不是线程安全的。尾插法解决不了所有并发问题。并发 put 仍可能出现数据覆盖、丢数据、size 不准、扩容状态异常等问题。 + + + +### 2)JDK8 HashMap线程不安全原因分析 +JDK8 HashMap 不安全,不是因为还会像 JDK7 那样容易成环,而是因为 put、size++、resize、table 发布、链表/红黑树修改都没有加锁或 CAS,多线程并发读写会发生覆盖、丢数据、计数错误和可见性问题。 + + + +> **桶为空时,多线程操作桶,会直接覆盖table[i]** +> + +```plain +if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); +``` + +此处最核心原因是操作同一个桶位置,没有加锁,也没有进行CAS,线程不安全。 + + + +> **链表尾插时,p.next 可能互相覆盖** +> + +```java +if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + ... + break; +} +``` + +多线程尾插法容易导致p.next正确结果被覆盖。 + + + +> **++size不是原子操作** +> + +```java +if (++size > threshold) + resize(); +``` + +此处++size不是原子操作,会导致最终size结果不准确。 + + + + + +### 3)JDK7 HashMap线程不安全原因分析 +JDK7 并发扩容时,头插法会反转链表,两个线程交叉修改同一批 Entry 的 next 指针,就可能把 A.next 指向 B,同时又把 B.next 指回 A,形成死循环。 + +```plain +T1 线程处理原始链表: + +A -> B -> null + + +T2 线程头插迁移后,把指针改成: + +B -> A -> null + + +T1 线程继续按旧进度迁移,但读到了 T2 线程改过的 B.next: + +B.next = A + + +最后成了循环链表,变成下图: + +A -> B +^ | +|____| +``` + +## getNode() +getNode()方法源码如下 + +```java +final Node getNode(int hash, Object key) { + // tab:底层数组 + // first:桶里的第一个节点 + // e:遍历链表时的当前节点 + // n:数组长度 + // k:临时保存节点 key + Node[] tab; Node first, e; int n; K k; + + // table 不为空、长度大于 0,并且目标桶不为空,才继续查找 + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + + // 先检查桶里的第一个节点 + // 先比 hash,再比 key 引用或 equals + if (first.hash == hash && + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + + // 第一个节点不是目标 key,并且后面还有节点 + if ((e = first.next) != null) { + + // 如果桶已经树化,走红黑树查找 + if (first instanceof TreeNode) + return ((TreeNode)first).getTreeNode(hash, key); + + // 普通链表,依次向后遍历 + do { + // 找到 hash 和 key 都匹配的节点,直接返回 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + + // 继续访问下一个节点 + } while ((e = e.next) != null); + } + } + + // table 为空、桶为空,或者遍历完没找到 + return null; +} +``` + + 核心流程: + +1.table 为空,直接返回 null。 + +2.根据 hash 定位桶下标。 + +3.先查桶里的第一个节点。 + +4.如果是红黑树,走树查找。 + +5.否则遍历链表。 + +6.找不到返回 null。 + +## hash() +hash()方法是HashMap中的hash扰动函数,作用是:把 key 的 hashCode() 再处理一下,让高位信息也参与到低位计算,减少哈希冲突。 + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + + 最核心的是这一段:(h = key.hashCode()) ^ (h >>> 16) + +> **为什么要这么做?** +> + +在HashMap中计算桶下标都需要通过:(n - 1) & hash 来计算。又因为n是2的幂,所以这个计算主要以来的是hash的**低位**,高位一直没有利用到。HashMap初始容量为16,则n-1=15,15的二进制位:0000 1111,那么:(n - 1) & hash由于与操作的特性,这实际上只看hash的低4位。**这会导致:如果很多 key 的低位相同,即使高位不同,也会落到同一个桶里。** + +所以 HashMap 做了这个扰动:h ^ (h >>> 16)。把高 16 位右移到低 16 位,再和原 hash 异或,让高位信息参与低位计算。 + +举例: + +```java +>>>是无符号右移:整体向右移动,左边补 0,右边被移出去的低位丢弃。 + +原始 hash: + +高 16 位 低 16 位 +AAAA AAAA BBBB BBBB + +h >>> 16: + +0000 0000 AAAA AAAA + +异或后: + +AAAA AAAA (BBBB BBBB ^ AAAA AAAA) +``` + +## resize() +resize也是HashMap的核心方法之一,源码如下: + +```java +final Node[] resize() { + // 旧数组 + Node[] oldTab = table; + + // 旧容量,table 为空则为 0 + int oldCap = (oldTab == null) ? 0 : oldTab.length; + // 旧扩容阈值 + int oldThr = threshold; + // 新容量、新阈值 + int newCap, newThr = 0; + + // 情况一:旧数组已经存在,说明是正常扩容 + if (oldCap > 0) { + // 已经达到最大容量,不能再扩容 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + // 容量扩大 2 倍 + // 阈值也扩大 2 倍 + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; + } + + // 情况二:数组还没创建,但 threshold > 0 + // 说明构造 HashMap 时指定了初始容量 + // 例如 new HashMap<>(32) + // 此时 threshold 暂时存的是 tableSizeFor(initialCapacity) + else if (oldThr > 0) + newCap = oldThr; + + // 情况三:无参构造 new HashMap<>() + // 第一次 put 时走这里,使用默认容量 16,默认阈值 12 + else { + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + + // 如果上面没有算出新阈值,则按 newCap * loadFactor 计算 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + + // 更新扩容阈值 + threshold = newThr; + // 创建新数组 + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap]; + // table 指向新数组 + table = newTab; + + // 如果旧数组不为空,需要迁移旧数据 + if (oldTab != null) { + + // 遍历旧数组每个桶 + for (int j = 0; j < oldCap; ++j) { + Node e; + // 如果当前桶不为空 + if ((e = oldTab[j]) != null) { + + // 旧桶置空,帮助 GC + oldTab[j] = null; + // 情况一:桶里只有一个节点,直接重新计算下标放入新数组 + if (e.next == null) + newTab[e.hash & (newCap - 1)] = e; + // 情况二:桶里是红黑树,走红黑树拆分逻辑 + else if (e instanceof TreeNode) + ((TreeNode)e).split(this, newTab, j, oldCap); + // 情况三:桶里是链表 + else { + // lo 链:扩容后仍然留在原下标 j + Node loHead = null, loTail = null; + // hi 链:扩容后移动到 j + oldCap + Node hiHead = null, hiTail = null; + Node next; + + // 遍历旧链表,把节点拆成 lo 和 hi 两条链 + do { + // 先保存下一个节点 + next = e.next; + // 判断扩容后位置是否不变 + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + // 扩容后位置变为 原下标 + oldCap + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + + } while ((e = next) != null); + + // lo 链放回原下标 j + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + // hi 链放到新下标 j + oldCap + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + + // 返回新数组 + return newTab; +} +``` + + + + + +# 总结 +HashMap中最核心的概念如下 + +```java +数组 + 链表 + 红黑树 +默认容量 16 +负载因子 0.75 +容量始终是 2 的幂 +链表长度 >= 8 且 table 容量 >= 64 时树化 +树节点过少时退化回链表 +允许 null key / null value +非线程安全 +``` + + + +JDK8和JDK7相比: + +```java +JDK7: +数组 + 链表 + +JDK8: +数组 + 链表 + 红黑树 +``` + From a1dedcd58aa5cc4c6e0fcfeb18f477f9f61ed30b Mon Sep 17 00:00:00 2001 From: coderbruis Date: Fri, 10 Jul 2026 20:33:38 +0800 Subject: [PATCH 08/21] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- ...20\347\240\201\345\210\206\346\236\220.md" | 6 +- ...25\345\261\202\345\216\237\347\220\206.md" | 4 + ...73\350\276\221\345\210\206\346\236\220.md" | 136 ++++++++++++++---- 4 files changed, 120 insertions(+), 32 deletions(-) rename "note/kafka/kafka rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md" => "note/kafka/kafka Rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md" (81%) diff --git a/README.md b/README.md index 5fffc49..b426ccb 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,10 @@ Kafka / RocketMQ 消息系统源码 ![kafka_architecture](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_architecture.png) Kafka Rebalance流程图 -![Kafka Cooperative Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_COOPERATIVE_rebalance.png) - ![Kafka EAGER Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_EAGER_rebalance.png) +![Kafka Cooperative Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_COOPERATIVE_rebalance.png) + Kafka ISR / HW / LEO关系图 ![Kafka_HW_ISR_LEO](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_HW_ISR_LEO.png) @@ -100,7 +100,7 @@ Kafka / RocketMQ 消息系统源码 - [深入分析 ThreadLocal](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%88%86%E6%9E%90ThreadLocal.md) - [深入学习 Java volatile 关键字](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Java%20volatile%E5%85%B3%E9%94%AE%E5%AD%97.md) - [深入学习 Thread 底层原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0Thread%E5%BA%95%E5%B1%82%E6%BA%90%E7%A0%81.md) -- [深入学习HashMap 底层源码与原理]() +- [深入学习HashMap 底层源码与原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/HashMap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) - [开源项目里那些看不懂的位运算分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E9%87%8C%E9%82%A3%E4%BA%9B%E7%9C%8B%E4%B8%8D%E6%87%82%E7%9A%84%E4%BD%8D%E8%BF%90%E7%AE%97%E5%88%86%E6%9E%90.md) - [ThreadPoolExecutor 源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90ThreadPoolExecutor%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md) diff --git "a/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md" index 61386e6..a586cb2 100644 --- "a/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md" +++ "b/note/JDK/HashMap\346\272\220\347\240\201\345\210\206\346\236\220.md" @@ -1,4 +1,8 @@ -当前HashMap版本:JDK1.8 ++ 当前HashMap版本:JDK1.8 ++ 转载请标明出处 + +# HashMap底层原理图 +![HashMap原理图](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/HashMap.png) # HashMap的重要成员变量以及内部类 > **默认容量:DEFAULT_INITIAL_CAPACITY** diff --git "a/note/kafka/Kafka ISR \345\272\225\345\261\202\345\216\237\347\220\206.md" "b/note/kafka/Kafka ISR \345\272\225\345\261\202\345\216\237\347\220\206.md" index 02e5bd2..93a9b36 100644 --- "a/note/kafka/Kafka ISR \345\272\225\345\261\202\345\216\237\347\220\206.md" +++ "b/note/kafka/Kafka ISR \345\272\225\345\261\202\345\216\237\347\220\206.md" @@ -1,6 +1,10 @@ + 当前分析版本是kafka最新版本(版本随时变化,最新分析代码请关注仓库:https://github.com/coderbruis/kafka source_code_analysis分支,底层原理持续更新) + 转载请标明出处 +# Kafka HW,ISR,LEO关系图 + +![Kafka_HW_ISR_LEO](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_HW_ISR_LEO.png) + # Kafka ISR是什么?解决什么问题? ## 是什么? Kafka ISR是In-Sync Replicas,意思是“与leader保持同步的副本集合”。在Kafka中会有leader副本和follower副本,下面举例: diff --git "a/note/kafka/kafka rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md" "b/note/kafka/kafka Rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md" similarity index 81% rename from "note/kafka/kafka rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md" rename to "note/kafka/kafka Rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md" index 409bc93..bf3bde3 100644 --- "a/note/kafka/kafka rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md" +++ "b/note/kafka/kafka Rebalance\346\240\270\345\277\203\351\200\273\350\276\221\345\210\206\346\236\220.md" @@ -1,6 +1,12 @@ + 当前分析版本是kafka最新版本(版本随时变化,最新分析代码请关注仓库:[https://github.com/coderbruis/kafka](https://github.com/coderbruis/kafka) **source_code_analysis分支**,底层原理持续更新) + 转载请标明出处 +# Kafka Rebalance流程图 + +![Kafka EAGER Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_EAGER_rebalance.png) + +![Kafka Cooperative Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_COOPERATIVE_rebalance.png) + # Kafka Rebalance 核心流程 `KafkaConsumer.poll()` 是消费者触发 rebalance 的主要入口。 @@ -629,20 +635,47 @@ if (protocol == RebalanceProtocol.COOPERATIVE && # EAGER 和 COOPERATIVE 的差异 ## EAGER -EAGER rebalance 的特点是简单直接。 +EAGER核心特点:全量停、全量分、再恢复 + +### EAGER Rebalance流程 + +第一步:触发 Rebalance +1. 成员变化:join / leave / session timeout / max.poll.interval 超时等。 +2. 订阅或元数据变化:订阅 topic 变化、正则订阅匹配变化、partition 数变化等。 + +第二步:Coordinator 进入 Rebalance 状态 +1. Coordinator 将 group 状态切到 PreparingRebalance(可能不是第一次进入rebalance,所以这里状态已经是PreparingRebalance了,正常第一次是在JoinGroup进入Coordinator里,会将状态变更为PreparingRebalance。) +2. 现有成员会通过 heartbeat 或 poll 流程感知需要重新加入 group。 +3. 新成员或需要重分配的成员准备发送 JoinGroupRequest。 + +第三步:EAGER 全员撤销旧 assignment,然后 JoinGroup +1. 每个 consumer 在 JoinGroup 前执行 onJoinPrepare。 +2. EAGER 协议下 revoke 当前持有的所有 partitions。 +3. 调用 onPartitionsRevoked(allAssignedPartitions)。 +4. 清空本地 assignment,停止这些 partitions 的消费。全组成员进入消费暂停状态(STW)。 +5. 向 coordinator 发送 JoinGroupRequest,携带 subscription 和支持的 assignor。 + +第四步:JoinGroup 完成成员协商 +1. Coordinator 收集本轮成员的 JoinGroupRequest。 +2. 选择 leader consumer。 +3. 选择 assignment strategy / protocol,leader consumer生成新的assignment。 +4. 生成新的 generationId,同时将group状态变更为COMPLETING_REBALANCE。 +5. JoinGroupResponse 返回给成员。 +6. Leader 会拿到所有成员的 subscription metadata。 + +第五步:分配 + SyncGroup +1. Leader consumer 执行分配算法,比如 Range / Sticky。 +2. Leader 通过 SyncGroupRequest 把全组 assignment 发给 coordinator。 +3. Follower 也发送 SyncGroupRequest,但通常不带 assignment。 +4. Coordinator 保存本轮 assignment,group状态变更为STABLE。 +5. Coordinator 通过各自的 SyncGroupResponse 返回每个 consumer 自己的 assignment。 + +第六步:恢复消费 +1. Consumer 收到自己的 assignment。 +2. 更新本地 assignment。 +3. 调用 onPartitionsAssigned(newAssignedPartitions)。 +4. 从对应 offset 开始拉取消息,恢复消费。 -```plain -onJoinPrepare: - revoke all partitions - clear local assignment - -leader assign: - assign all partitions again - -onJoinComplete: - assign new partitions - trigger onPartitionsAssigned -``` 优点: @@ -655,23 +688,70 @@ onJoinComplete: + 即使某些 partition 仍然分给同一个 consumer,也会先 revoke 再 assign。 ## COOPERATIVE -COOPERATIVE rebalance 的特点是渐进迁移。 - -```plain -onJoinPrepare: - keep still-owned partitions - revoke only obviously invalid partitions +COOPERATIVE rebalance 的特点是渐进迁移,不会撤销所有分区导致全部分区STW。 + +### COOPERATIVE Rebalance流程 + +第一轮:标记迁移,旧 owner revoke(onwer表示持有parition的consumer) +1. 成员变化、订阅变化或元数据变化触发 rebalance。 +2. consumer 发送 JoinGroup 给 coordinator。 + JoinGroup metadata 里包含: + ○ 当前 subscription + ○ 当前本地已分配分区,也就是 ownedPartitions +3. coordinator 收集所有成员的 JoinGroup。 + coordinator 负责: + ○ 选 leader + ○ 选 assignor/protocol + ○ 把所有成员 subscription metadata 返回给 leader + coordinator收集完所有的JoinGroup请求之后,会进入新的generationId(递增)。 +4. leader consumer 执行 assignor。 + CooperativeStickyAssignor 会: + ○ 先计算目标 assignment + ○ 如果某个分区要从旧 owner 转给新 owner + ○ 但旧 owner 在本轮 JoinGroup 里仍上报该分区为 owned + ○ 那么本轮不会把该分区分给新 owner + ○ 同时旧 owner 的本轮 assignment 不再包含该分区 +5. leader Consumer 通过 SyncGroup 把全组 assignment 提交给 coordinator。其他consumer也会发送一个空的SyncGroup给coorinator +6. coordinator 保存 assignment,并通过 SyncGroupResponse 返回每个 consumer 自己的 assignment。 +7. consumer 处理 SyncGroupResponse。 + 本地计算: +```text + owned = 当前本地 assignment + assigned = 本轮收到的 assignment + + revoked = owned - assigned + added = assigned - owned +``` + +需要注意,第一轮主要是做撤销,但是也可能直接添加新的分区分配结果。因为如果这一轮有某个分区本来就没有旧onwer(旧的持有这个parition的consumer),这一轮就可以直接分配给新onwer。 +8. 如果 revoked 非空: + ○ 调用 onPartitionsRevoked(revoked) + ○ 调用 requestRejoin() + ○ 后续把本地 assignment 更新为 assigned +9. 如果 added 非空: + ○ 调用 onPartitionsAssigned(added) + 注意:正在从旧 owner 转移给新 owner 的分区,第一轮不会出现在新 owner 的 added 里。 + 第二轮:旧owner撤销分区,新owner获得第一轮撤销的分区 +1. 因为第一轮有 consumer 调用了 requestRejoin(),group 再次 rebalance。 +2. consumer 再次发送 JoinGroup。 + 此时旧 owner 的本地 assignment 已经更新,所以它上报的 ownedPartitions 不再包含刚刚 revoked 的分区。 + 被撤销的分区不会通过JoinGroup发送给Coordinator,revokeList和addLIst都是通过集合计算来得出的。 + coordinator收集完所有的JoinGroup请求之后,会进入新的generationId(递增)。 +3. coordinator 再次收集成员,选 leader,选协议。 + 然后将所有成员 subscription metadata 返回给 leader consumer。 +4. leader 再次执行 assignor。 + 这次 assignor 发现: + ○ 该分区已经没有旧 owner 上报 owned + ○ 可以安全分配给新 owner +5. leader 通过 SyncGroup 提交新的全组 assignment。 +6. coordinator 通过 SyncGroupResponse 返回各成员自己的 assignment。 +7. 新 owner 处理 assignment。 + 本地计算: + added = assigned - owned +8. 新 owner 对新增分区调用: + onPartitionsAssigned(added) -leader assign: - do not immediately reassign partitions still owned by others -onJoinComplete: - revoke partitions that need migration - request another rejoin if needed - -next rebalance: - assign released partitions to new owners -``` 优点: From 8ab30cd0f7d23e527f5d77fbf8c6b2d93fd17a4f Mon Sep 17 00:00:00 2001 From: coderbruis Date: Sat, 11 Jul 2026 10:50:31 +0800 Subject: [PATCH 09/21] =?UTF-8?q?=E6=96=B0=E5=A2=9EAQS=E5=8E=9F=E7=90=86?= =?UTF-8?q?=E5=88=86=E6=9E=90=E6=96=87=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 + ...20\347\240\201\345\210\206\346\236\220.md" | 1060 +++++++++++++++++ 2 files changed, 1063 insertions(+) create mode 100644 "note/JDK/AQS\346\272\220\347\240\201\345\210\206\346\236\220.md" diff --git a/README.md b/README.md index b426ccb..158573c 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,9 @@ Kafka / RocketMQ 消息系统源码 HashMap原理图 ![HashMap原理图](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/HashMap.png) +AQS架构图 +![AQS架构图](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/AQS.png) +
diff --git "a/note/JDK/AQS\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/note/JDK/AQS\346\272\220\347\240\201\345\210\206\346\236\220.md" new file mode 100644 index 0000000..8fb00bc --- /dev/null +++ "b/note/JDK/AQS\346\272\220\347\240\201\345\210\206\346\236\220.md" @@ -0,0 +1,1060 @@ +AQS版本:JDK 1.8 + +# AQS灵魂三问 +## 是什么? +AQS 全称是 AbstractQueuedSynchronizer,中文通常翻译为:**抽象队列同步器**。 + +AQS 本身不是一把可以直接使用的锁,而是一个用于构建锁和同步器的基础框架。 + +JDK 中很多并发工具都基于 AQS 实现: + +```java +ReentrantLock +ReentrantReadWriteLock +Semaphore +CountDownLatch +ThreadPoolExecutor.Worker +``` + +## 解决什么问题? +AQS 主要解决两类通用问题: + +1. 如何通过一个同步状态 state 判断资源是否可获取 +2. 获取失败后,如何让线程排队、阻塞,并在合适时机被唤醒 + + + +AQS 用于统一解决线程竞争、等待队列、阻塞与唤醒问题。 + +## 起什么作用? +AQS是 ReentrantLock、Semaphore 等同步器的基础框架。 + +AQS 将同步器拆成了两部分: + +1. 子类负责: + 1. 定义 state 的业务含义。 + 2. 定义获取资源和释放资源的规则。 +2. AQS负责: + 1. CAS 修改状态。 + 2. 维护等待队列。 + 3. 阻塞线程。 + 4. 唤醒线程。 + 5. 处理中断、超时和取消。 + +例如: + +1. ReentrantLock:state 表示锁的重入次数 +2. CountDownLatch:state 表示剩余计数 +3. Semaphore:state 表示剩余许可证数量 + +**** + +**AQS 的核心内容可以浓缩为:** + +1. 一个 volatile int state +2. 一个双向同步等待队列 +3. CAS 原子操作 +4. LockSupport.park/unpark 阻塞与唤醒 + +# AQS整体架构 +AQS 支持两种资源获取模式: + +1. 独占模式 Exclusive:同一时刻只允许一个线程成功获取资源。 +2. 共享模式 Shared:同一时刻允许多个线程成功获取资源。 + + + +典型应用: + +1. 独占模式:ReentrantLock、ReentrantReadWriteLock 写锁。 +2. 共享模式:Semaphore、CountDownLatch、ReentrantReadWriteLock 读锁。 + + + +两种模式共用同一个同步队列: + +```plain +head tail + | | + v v +[哨兵节点] <-> [独占节点 T1] <-> [共享节点 T2] <-> [独占节点 T3] +``` + + + +AQS 不理解“锁”“许可证”“计数器”等业务含义。它只负责: + +1. tryAcquire/tryAcquireShared 成功:线程继续执行。 +2. tryAcquire/tryAcquireShared 失败:线程进入队列并阻塞。 +3. tryRelease/tryReleaseShared 成功:唤醒后继线程。 + + + + +![](https://cdn.nlark.com/yuque/0/2026/png/12890164/1783689319993-5d064d1b-9d0e-4f6f-b208-5fbaedbc0952.png) + + + +# AQS核心结论 +AQS 本质上是一个用于构建锁和同步器的基础框架,核心由两部分组成:一个表示同步状态的 state,以及一个保存等待线程的 FIFO 双向队列。 + + + +线程获取资源时,首先通过 CAS 尝试修改 state: + ++ 获取成功:线程继续执行。 ++ 获取失败:线程封装成 Node 加入等待队列,并通过 LockSupport.park() 阻塞。 ++ 资源释放:修改 state,再通过 LockSupport.unpark() 唤醒后继节点重新竞争。 + + + +AQS 自身不定义资源如何获取和释放,而是通过模板方法交给子类实现: + ++ tryAcquire() ++ tryRelease() ++ tryAcquireShared() ++ tryReleaseShared() + + + +因此,AQS 的核心可以概括为: + ++ state 表示资源或锁状态。 ++ CAS 保证状态修改的原子性。 ++ 等待队列管理获取资源失败的线程。 ++ park/unpark 实现线程阻塞与唤醒。 ++ 模板方法定义具体同步规则。 ++ 支持独占模式和共享模式。 ++ ConditionObject 提供条件等待队列。 + + + +AQS 将“资源竞争、线程排队、阻塞与唤醒”统一封装,子类只需要实现资源的获取和释放规则。 + +# AQS 的重要成员变量和内部类 +## state:同步状态 +state 是 AQS 中最核心的变量: + +```java +private volatile int state; +``` + +AQS 使用一个 volatile int 保存同步状态,并通过以下方法读取和修改: + +```java +protected final int getState() +protected final void setState(int newState) +protected final boolean compareAndSetState(int expect, int update) +``` + +其中: + +```plain +getState():读取当前同步状态 +setState():直接设置同步状态 +compareAndSetState():通过 CAS 原子更新同步状态 +``` + +state 的含义由具体同步器决定。 + + + +ReentrantLock 中的 state表示 + ++ state = 0:锁未被任何线程持有 ++ state = 1:当前线程第一次获取锁 ++ state = 2:当前线程重入一次 ++ state = 3:当前线程重入两次 + + + +Semaphore 中的 state表示当前剩余许可证数量。 + +例如: + +```plain +初始许可证数量为 3 + +state = 3:还可以有 3 个线程获取 +state = 2:还可以有 2 个线程获取 +state = 0:没有剩余许可证 +``` + + + +CountDownLatch中的state表示尚未完成的计数。 + +例如: + +```plain +new CountDownLatch(3) + +初始 state = 3 +每次 countDown(),state 减 1 +state = 0 时,所有 await() 线程可以继续执行 +``` + +**** + +**总结:AQS 只提供一个线程安全的状态字段,不规定状态的业务含义。** + + + +## head 和 tail:同步队列首尾节点 +```java +private transient volatile Node head; +private transient volatile Node tail; +``` + +AQS 使用一个基于 CLH 思想改造的双向链表维护获取资源失败的线程。 + +```plain +head tail + | | + v v +[哨兵节点] <-> [节点 T1] <-> [节点 T2] <-> [节点 T3] +``` + +其中: + +```plain +head:队列头节点,通常是已经获取过资源的哨兵节点 + +tail:队列尾节点,新节点通过 CAS 追加到 tail +``` + +head和tail都使用 volatile 修饰,保证不同线程之间的可见性。 + + + +AQS 创建时不会立刻创建队列。只有第一次发生竞争、线程需要入队时,才会初始化一个空的哨兵节点: + +```plain +head == tail == new Node() +``` + +这样可以避免在从未发生竞争的同步器上浪费节点对象。 + +## Node:同步队列节点 +AQS 内部通过Node表示一个等待线程。 + +源码结构如下: + +```java +static final class Node { + ... + static final Node SHARED = new Node(); + static final Node EXCLUSIVE = null; + + static final int CANCELLED = 1; + static final int SIGNAL = -1; + static final int CONDITION = -2; + static final int PROPAGATE = -3; + + volatile int waitStatus; + // 指向同步队列中的前驱节点。 + volatile Node prev; + volatile Node next; + // 保存当前节点对应的等待线程。 + // 当节点需要被唤醒时,AQS 会执行:LockSupport.unpark() + volatile Thread thread; + Node nextWaiter; + ... +} +``` + +### prev +prev表示指向同步队列中的前驱节点。 + +```java +volatile Node prev; +``` + +指向同步队列中的前驱节点。 + +AQS 判断当前节点能否尝试获取资源时,最重要的条件是:**当前节点的前驱节点是否是 head。** + +只有队列中的第一个有效等待节点,才有资格再次调用tryAcquire()或tryAcquireShared()竞争资源。 + + + +AQS中判断源码如下: + +独占模式acquire: + +```java +final boolean acquireQueued(final Node node, int arg) { + ... + final Node p = node.predecessor(); + if (p == head && tryAcquire(arg)) { + ... + } + ... +} +``` + +共享模式acquire: + +```java +private void doAcquireShared(int arg) { + ... + final Node p = node.predecessor(); + if (p == head) { + ... + } + ... +} +``` + + + +**所以“当前节点的前驱节点是否是 head”是非常重要的一个条件。** + + + +### next(设计思想非常的细节) +指向同步队列中的后继节点。 + +```java +volatile Node next; +``` + +释放资源时,AQS 通常通过 head.next 找到需要唤醒的线程。 + +但next只是一个优化路径,不是绝对可靠的队列判断依据。 + +原因是节点入队过程如下: + +1. node.prev = oldTail +2. CAS 把 tail 从 oldTail 修改为 node +3. oldTail.next = node + +```java +// 入队操作,并返回当前node的前序节点 +private Node enq(final Node node) { + for (;;) { + Node t = tail; + if (t == null) { + if (compareAndSetHead(new Node())) + tail = head; + } else { + // 1. oldTail <- node + node.prev = t; + // 2. oldTail <- node(tail) 将node赋值给tail,表明当前node成为了tail + if (compareAndSetTail(t, node)) { + // oldTail -> node(tail) + t.next = node; + // 返回oldTail + return t; + } + } + } +} +``` + + + +在线程完成第 2 步、尚未完成第 3 步时: + +```plain +tail 已经指向新节点 +但 oldTail.next 仍然是 null +``` + +因此在部分场景下,如果判断到next=null,则AQS 会从tail沿着prev反向扫描,寻找有效后继节点。 + +```java +private void unparkSuccessor(Node node) { + ... + Node s = node.next; + if (s == null || s.waitStatus > 0) { + s = null; + // 通过next找不到要unpark的节点,则从tail开始向前遍历 + for (Node t = tail; t != null && t != node; t = t.prev) + if (t.waitStatus <= 0) + s = t; + } + ... +} +``` + + + +**总结:**因为有 next 指针,所以 release 时可以直接取 head,再通过 head.next 找到要唤醒的后继节点,通常是 O(1)。而如果没有next指针,则需要从tail尾结点向前通过prev遍历,时间复杂度为O(n)。所以在入队维护next指针的时候,因为: + +1)node.prev = t; + +2)compareAndSetTail(t, node) + +3)t.next = node; + +导致可能t.next会存在null的场景,所以通过next指针遍历会拿不到数据。所以如果next为null,就会退化成通过prev指针去获取目标节点。 + + + +### nextWaiter +nextWaiter 有两个用途。 + +#### 用途一:标识节点模式 +```java +nextWaiter == Node.SHARED:共享节点 +nextWaiter == Node.EXCLUSIVE:独占节点 +``` + +其中 Node.EXCLUSIVE 实际是 null。 + +#### 用途二:连接 Condition 条件队列 +Condition 条件队列是单向链表,节点之间通过 `nextWaiter` 连接: + +```plain +firstWaiter + | + v +[T1 CONDITION] -> [T2 CONDITION] -> [T3 CONDITION] + ^ + | + lastWaiter +``` + +所以: + +```plain +prev/next:用于 AQS 同步队列 +nextWaiter:用于 Condition 条件队列,或者标记共享模式 +``` + + + +### waitStatus 状态详解 +Node 中最难理解的字段是: + +```java +volatile int waitStatus; +``` + +它表示节点当前的等待状态。 + +JDK 8 中主要有以下几种状态: + ++ CANCELLED = 1 ++ SIGNAL = -1 ++ CONDITION = -2 ++ PROPAGATE = -3 ++ 默认状态 = 0 + +**** + +#### 0:默认状态 +新创建的同步队列节点,`waitStatus` 默认为 0。 + +表示节点当前没有特殊状态。 + + + +#### CANCELLED = 1:节点已取消 +```java +static final int CANCELLED = 1; +``` + +出现以下情况时,节点可能被取消: + ++ 线程等待超时 ++ 线程在可中断等待中被中断 ++ 获取资源过程中发生异常 + + + +取消后的节点不会再次参与资源竞争。 + +AQS 会在后续遍历中跳过 waitStatus > 0 的节点。 + +需要注意:CANCELLED 是唯一的正数状态。 + +因此源码中经常通过下面的方式判断节点是否已取消:waitStatus > 0 + + + +#### SIGNAL = -1:后继节点需要被唤醒 +```java +static final int SIGNAL = -1; +``` + +**这是同步队列中最重要的状态。** + + + +假设队列结构如下:pred -> node + +当 pred.waitStatus == SIGNAL 时,表示: + +```plain +node 准备阻塞。 +当 pred 释放资源、成为无效头节点或被取消时,pred 负责唤醒 node。 +``` + +一个非常容易混淆的点是: + +**SIGNAL 状态保存在前驱节点上,但它表达的是后继节点需要被唤醒。** + +例如: + +```plain +head(waitStatus = SIGNAL) -> T1 +``` + +表示 T1 可以安全阻塞,head 对应的资源持有者释放资源时,需要唤醒 T1。 + + + +为什么不把 SIGNAL 放在当前节点上? + +因为释放资源时,释放线程主要操作当前 `head`,由前驱节点记录唤醒责任,可以减少对后继节点状态的竞争修改。 + + + +#### CONDITION = -2:节点正在 Condition 条件队列中等待 +```java +static final int CONDITION = -2; +``` + +线程调用 Condition.await() 后,会进入 Condition 条件队列,此时节点状态为 CONDITION。 + +```plain +Condition 条件队列: + +[T1 CONDITION] -> [T2 CONDITION] -> [T3 CONDITION] +``` + + + +当其他线程调用 signal() 后,节点会: + +```plain +waitStatus:CONDITION -> 0 +从 Condition 条件队列转移到 AQS 同步队列 +``` + +只有重新进入同步队列并再次获取锁后,`await()` 才会返回。 + + + +#### PROPAGATE = -3:共享模式继续传播 +```java +static final int PROPAGATE = -3; +``` + +PROPAGATE 只用于共享模式。 + +它用于记录:即使当前释放动作没有直接找到需要唤醒的节点,后续共享节点仍需要继续检查并传播唤醒。 + + + +该状态主要用于解决共享模式下并发获取、释放交错时可能出现的传播遗漏问题。 + +可以先把它理解为:共享唤醒传播标记。 + + + +## waitStatus 的正负设计 +AQS 中对 `waitStatus` 的判断非常精简: + +```plain +waitStatus > 0:节点已取消,需要跳过 +waitStatus < 0:节点处于有效的信号、条件或传播状态 +waitStatus = 0:普通初始状态 +``` + +这种设计让很多分支只需要判断正负,而不必逐个比较状态常量。 + + + +# AQS 同步队列为什么是 CLH 变体 +AQS 的同步队列通常被称为 CLH 队列,但它并不是原始的 CLH 自旋锁队列。 + +原始 CLH 队列的核心思想是:每个线程关注自己的前驱节点状态。 + + + +AQS 保留了这个思想,但做了改造: + +```plain +原始 CLH:线程主要自旋等待 +AQS:线程尝试几次后,通过 LockSupport.park 阻塞 + +原始 CLH:通常只需要前驱引用 +AQS:增加 prev 和 next,便于取消、唤醒和队列维护 +``` + + + +AQS 节点主要观察前驱节点: + +```plain +前驱是 head:尝试获取资源 +前驱为 SIGNAL:当前线程可以安全 park +前驱已取消:跳过取消节点,重新连接有效前驱 +``` + +因此,AQS 同步队列可以理解为:基于 CLH 思想改造的 FIFO 双向阻塞队列 + + + +# AQS 留给子类实现的五个方法 +AQS 将排队和阻塞机制实现好,但资源获取规则需要子类定义。 + +核心扩展方法如下: + +```java +protected boolean tryAcquire(int arg) +protected boolean tryRelease(int arg) +protected int tryAcquireShared(int arg) +protected boolean tryReleaseShared(int arg) +protected boolean isHeldExclusively() +``` + +默认实现都会抛出UnsupportedOperationException,子类根据需要选择实现独占模式、共享模式或两者。 + + + +# AQS多线程竞争入队流程 +## 独占锁场景 +下面按**独占锁**场景流程,比如ReentrantLock.lock()。 + +**背景:当前有T0、T1、T2三个线程竞争锁,默认T0已获得锁。** + +### 1. T0 已经持有锁 +```plain +state = 1 +owner = T0 + +同步队列:空 +head = null +tail = null +``` + +**** + +### 2. T1 来竞争,tryAcquire 失败,封装成 Node 入队 +AQS 执行:addWaiter(Node.EXCLUSIVE) + + + +队列第一次初始化,会先创建哨兵 head: + +```plain +head(dummy) <-> T1 + ^ + tail +``` + +此时节点状态: + +```plain +head.waitStatus = 0 +T1.waitStatus = 0 +``` + +### 3. T1 入队后马上自旋再次竞争,失败,准备阻塞 +T1 在 acquireQueued 里判断: + +```plain +p == head && tryAcquire(arg) +``` + +此时 `p == head`,但锁还被 T0 持有,所以失败。 + +然后进入: + +```plain +shouldParkAfterFailedAcquire(head, T1) +``` + +T1 会把自己的前驱 head 设置成 **SIGNAL状态**: + +```plain +head(SIGNAL) <-> T1(0) + ^ + tail +``` + + + +然后T1发现前序节点是SIGNAL状态,然后T1调用park()进入阻塞状态。 + + + +### 4. T2 也来竞争,失败后入队 +```plain +head(SIGNAL) <-> T1(0) <-> T2(0) + ^ + tail +``` + +T2 的前驱是 T1。T2 获取失败后,马上自旋再次竞争锁,失败之后会把前驱 T1 设置成 `SIGNAL`: + +```plain +head(SIGNAL) <-> T1(SIGNAL) <-> T2(0) + ^ + tail +``` + +然后T2发现前序节点是SIGNAL状态,然后T2调用park()进入阻塞状态。 + +### 5. T0 释放锁,AQS 从 head 唤醒后继 +T0 调用: + +```plain +unlock() + -> release(1) + -> tryRelease(1) + -> unparkSuccessor(head) +``` + +队列: + +```plain +head(SIGNAL) <-> T1(SIGNAL) <-> T2(0) +``` + +AQS 找: + +```plain +head.next == T1 +``` + +然后: + +```plain +LockSupport.unpark(T1.thread) +``` + +结果: + +```plain +T1 被唤醒 +T2 仍阻塞 +``` + +### 6. T1 被唤醒后重新竞争锁,成功 +T1 醒来后回到循环: + +```plain +p == head && tryAcquire(arg) +``` + +此时锁已经空了,所以 T1 成功获取锁。 + +然后: + +```plain +setHead(T1) +``` + +队列变成: + +```plain +head = T1 +T1(thread=null, prev=null, SIGNAL) <-> T2(0) + ^ + tail +``` + +旧的 dummy head 会断开,帮助 GC: + +```plain +T1 成为新的 head +``` + +注意: + +```plain +T1 成为 head 后,它本身不再代表一个等待线程; +它代表当前成功获取过锁的节点。 +``` + +### 7. T1 释放锁,再唤醒 T2 +T1 执行: + +```plain +unlock() + -> release(1) + -> unparkSuccessor(head) +``` + +此时: + +```plain +head(T1, SIGNAL) <-> T2(0) +``` + +AQS 唤醒: + +```plain +head.next == T2 +``` + +T2 醒来,重新执行: + +```plain +p == head && tryAcquire(arg) +``` + +成功后: + +```plain +setHead(T2) +``` + +队列变成: + +```plain +head = T2 +tail = T2 +``` + +如果后面没有等待节点: + +```plain +T2.waitStatus 通常是 0 +``` + +# AQS核心方法 +## acquire() +当某个线程发起锁获取,比如调用ReentrantLock.lock()方法时,调用链会走到AbstractQueuedSynchronizer.acquire()方法,源码如下: + +```java +public final void acquire(int arg) { +if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +``` + +## nonfairTryAcquire() +tryAcquire()逻辑在AQS子类里,当前就是在ReentrantLock中,核心逻辑: + +```java +final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + // 获取AQS状态 + int c = getState(); + // 没有线程持有这个共享状态 + if (c == 0) { + // CAS 变更state状态 + if (compareAndSetState(0, acquires)) { + // 标识当前线程持有state + setExclusiveOwnerThread(current); + return true; + } + } + // 如果是持有锁的线程 + else if (current == getExclusiveOwnerThread()) { + // state加一,重复持有锁 + int nextc = c + acquires; + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } + return false; +} +``` + +nonfairTryAcquire()方法比较简单,主要就是通过CAS变更state状态,然后将持有锁的线程标识为owner,后续竞争锁则判断该线程是否持有,否则加锁失败返回false。 + + + +## addWaiter() +```java + private Node addWaiter(Node mode) { + Node node = new Node(Thread.currentThread(), mode); + // 尝试一次快速入队 + Node pred = tail; + // 如果pred=null,则表示队列还未初始化,快速入队逻辑不负责初始化 + if (pred != null) { + node.prev = pred; + // 并发竞争失败,会走到兜底入队逻辑 + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + // 兜底入队逻辑 + enq(node); + return node; + } +``` + +addWaiter() 先用一次 CAS 快速入队;如果队列未初始化或发生并发竞争,就交给 enq() 自旋,确保当前线程节点最终进入 AQS 同步队列。 + + + +enq() 方法负责同步队列的延迟初始化,以及节点竞争入队。当队列未初始化时,通过 CAS 创建哨兵头节点;当队列已经初始化时,通过 CAS 将当前节点设置为新的尾节点。如果 CAS 失败,则不断自旋重试,直到节点成功入队。 + +```java + private Node enq(final Node node) { + for (;;) { + Node t = tail; + // 队列初始化 + if (t == null) { // Must initialize + if (compareAndSetHead(new Node())) + tail = head; + } else { + node.prev = t; + // CAS往队列尾部添加节点 + if (compareAndSetTail(t, node)) { + t.next = node; + // !返回的是入队节点的前序节点 + return t; + } + } + } + } +``` + +## acquireQueued() + + +```java +/** + * 已入队节点以独占模式获取同步状态。 + * + * @return 等待过程中是否发生过中断 + */ +final boolean acquireQueued(final Node node, int arg) { + boolean failed = true; + try { + boolean interrupted = false; + + // 自旋,直到成功获取同步状态 + for (;;) { + final Node p = node.predecessor(); + + // 只有头节点的直接后继才有资格尝试获取同步状态 + if (p == head && tryAcquire(arg)) { + // 获取成功,当前节点成为新的头节点 + setHead(node); + p.next = null; // 断开旧头节点,帮助 GC + failed = false; + return interrupted; + } + + // 获取失败,判断是否需要阻塞;被唤醒后检查中断状态 + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) { + interrupted = true; + } + } + } finally { + // 出现异常等获取失败的情况时,取消当前节点 + if (failed) { + cancelAcquire(node); + } + } +} +``` + +shouldParkAfterFailedAcquire()方法核心就是将入参node的前驱节点状态置为:SIGNAL,然后方法返回false,继续执行后面的parkAndCheckInterrupt(),将node节点置为阻塞状态。 + + + +这里就是对应着有新线程竞争锁失败之后,先加入队列,之后自旋尝试再次竞争锁,失败了则将前序节点置为SIGNAL,然后将自己阻塞等待被唤醒。 + + + +下面再分析下释放锁的流程,释放锁核心会调用AQS的release()方法。 + +```java +public final boolean release(int arg) { + // 子类负责更新锁状态,并判断锁是否已完全释放 + if (tryRelease(arg)) { + Node h = head; + + // 头节点存在且后继节点需要唤醒 + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + + return true; + } + + // 重入次数尚未归零,本次只减少持锁次数 + return false; +} +``` + + + +tryRelease()方法在ReentrantLock中,源码如下: + +```java +protected final boolean tryRelease(int releases) { + // 减少重入计数 + int c = getState() - releases; + + // 只有锁的持有线程才能释放锁 + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + + boolean free = false; + + // 重入计数归零,锁才算完全释放 + if (c == 0) { + free = true; + setExclusiveOwnerThread(null); + } + + // 更新剩余重入次数 + setState(c); + return free; +} +``` + + + +AQS的unparkSuccessor()源码如下: + +```java +/** + * 唤醒等待队列中有效的后继节点。 + */ +private void unparkSuccessor(Node node) { + int ws = node.waitStatus; + + // 清除当前节点的待唤醒标记 + if (ws < 0) + compareAndSetWaitStatus(node, ws, 0); + + Node s = node.next; + + // next 无效时,从队尾反向查找最靠前的有效等待节点 + if (s == null || s.waitStatus > 0) { + s = null; + for (Node t = tail; t != null && t != node; t = t.prev) { + // waitStatus > 0 表示节点已经取消 + if (t.waitStatus <= 0) + s = t; + } + } + + // 唤醒目标线程,让其重新参与锁竞争 + if (s != null) + LockSupport.unpark(s.thread); +} +``` + + + +释放锁流程总结: + +1. 当前线程调用 unlock(),最终进入 AQS 的 release(1)。 +2. release() 调用 tryRelease(1),减少 state 表示的重入次数。 +3. 如果当前线程不是锁的持有者,抛出 IllegalMonitorStateException。 +4. 如果 state 仍大于 0,说明当前线程还持有重入锁,不唤醒其他线程。 +5. 如果 state 减少到 0,清空锁的持有线程,表示锁已完全释放。 +6. release() 检查等待队列,通过 unparkSuccessor() 找到有效的后继节点。 +7. 调用 LockSupport.unpark() 唤醒对应线程,使其重新尝试获取锁。 + + + +需要注意:unpark() 只是让等待线程具备继续运行的条件,并不代表它立刻获得锁。线程被唤醒后,仍然需要参与锁竞争。 + From bfa742da0cf74f902fb2d8e3046181f4a109e416 Mon Sep 17 00:00:00 2001 From: coderbruis Date: Sat, 11 Jul 2026 10:52:38 +0800 Subject: [PATCH 10/21] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 158573c..05b1c54 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ Kafka / RocketMQ 消息系统源码 - [深入学习HashMap 底层源码与原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/HashMap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) - [开源项目里那些看不懂的位运算分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE%E9%87%8C%E9%82%A3%E4%BA%9B%E7%9C%8B%E4%B8%8D%E6%87%82%E7%9A%84%E4%BD%8D%E8%BF%90%E7%AE%97%E5%88%86%E6%9E%90.md) - [ThreadPoolExecutor 源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/%E6%B7%B1%E5%85%A5%E8%A7%A3%E6%9E%90ThreadPoolExecutor%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md) +- [AQS 源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/JDK/AQS%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) HashMap原理图 ![HashMap原理图](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/HashMap.png) From 623f4101ac7c0817163af21aac5836e8655c35c0 Mon Sep 17 00:00:00 2001 From: coderbruis Date: Sun, 12 Jul 2026 08:54:05 +0800 Subject: [PATCH 11/21] =?UTF-8?q?=E9=87=8D=E5=B9=B3=E8=A1=A1=E4=B8=A5?= =?UTF-8?q?=E6=A0=BC=E5=8C=BA=E5=88=86=E4=B8=8D=E5=90=8C=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E7=89=88=E6=9C=AC:=20Classic=20Consumer=20Group=20Protocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05b1c54..41a5b54 100644 --- a/README.md +++ b/README.md @@ -73,13 +73,13 @@ Kafka / RocketMQ 消息系统源码 - [Kafka Broker 核心源码分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20broker%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) - [Kafka Broker 源码分析:生产者篇](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20broker%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E2%80%94%E2%80%94%E7%94%9F%E4%BA%A7%E8%80%85%E7%AF%87.md) - [Kafka 消费者核心源码分析(一)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%E6%B6%88%E8%B4%B9%E8%80%85%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%80%EF%BC%89.md) -- [Kafka Rebalance 核心逻辑分析](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20rebalance%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91%E5%88%86%E6%9E%90.md) +- [Kafka Rebalance 核心逻辑分析(Classic Consumer Group Protocol)](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/kafka%20Rebalance%E6%A0%B8%E5%BF%83%E9%80%BB%E8%BE%91%E5%88%86%E6%9E%90.md) - [Kafka ISR 原理](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/kafka/Kafka%20ISR%20%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86.md) Kafka架构图 ![kafka_architecture](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_architecture.png) -Kafka Rebalance流程图 +Kafka Rebalance流程图(Classic Consumer Group Protocol 经典消费者组协议) ![Kafka EAGER Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_EAGER_rebalance.png) ![Kafka Cooperative Rebalance](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/kafka_COOPERATIVE_rebalance.png) From 39b2a718a77226f0962adf897f8bb366c03c531e Mon Sep 17 00:00:00 2001 From: CoderBruis <37364336+coderbruis@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:17:49 +0800 Subject: [PATCH 12/21] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=94=99=E5=88=AB?= =?UTF-8?q?=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tor\345\272\225\345\261\202\345\216\237\347\220\206.md" | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git "a/note/JDK/\346\267\261\345\205\245\350\247\243\346\236\220ThreadPoolExecutor\345\272\225\345\261\202\345\216\237\347\220\206.md" "b/note/JDK/\346\267\261\345\205\245\350\247\243\346\236\220ThreadPoolExecutor\345\272\225\345\261\202\345\216\237\347\220\206.md" index ba9b143..8f55d40 100644 --- "a/note/JDK/\346\267\261\345\205\245\350\247\243\346\236\220ThreadPoolExecutor\345\272\225\345\261\202\345\216\237\347\220\206.md" +++ "b/note/JDK/\346\267\261\345\205\245\350\247\243\346\236\220ThreadPoolExecutor\345\272\225\345\261\202\345\216\237\347\220\206.md" @@ -11,7 +11,7 @@ private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); -在不同操作系统下,Java 中的 Integer 变量都是32位,ThreadPoolExecutor 使用前3位(31~29)表示线程池状态,用后29位(28~0)表示活跃线程数。 +在不同操作系统下,Java 中的 Integer 变量都是32位,ThreadPoolExecutor 使用前3位(31 ~ 29)表示线程池状态,用后29位(28 ~ 0)表示活跃线程数。 ## COUNT_BITS COUNT_BITS的作用:用来划分 ctl 这个 int 变量的高低位边界。 @@ -22,7 +22,7 @@ private static final int COUNT_BITS = Integer.SIZE - 3; // 29 -int的最大位值是32位,32 - 3 = 29。通过这29来控制高3位存线程池状态**(注意这里不是线程,是线程池)**,低29位表示线程池中worker数量。通过位移来实现高效操作。 +int的最大位值是32位,32 - 3 = 29。通过这29来控制高3位存线程池状态(注意这里不是线程,是线程池),低29位表示线程池中worker数量。通过位移来实现高效操作。 ## CAPACITY CAPACITY的作用:用来表示 workerCount 线程数量部分的最大容量,同时也作为低 29 位的**掩码**。 @@ -40,11 +40,10 @@ private static final int CAPACITY = (1 << COUNT_BITS) - 1; 2)对左移结果 - 1 -(1 << COUNT_BITS) - 1:就是00100000 00000000 00000000 00000000 - 1,对于二进制,如果高位减1,会借位到对应位置-1。举例如下: +(1 << COUNT_BITS) - 1:就是00100000 00000000 00000000 00000000 - 1,对于二进制,如果高位减1,会借位到对应位置减1。举例如下: + 01000 - 1 = 01000 - 01111 = 00111 + 00100000 00000000 00000000 00000000 - 1 = 00100000 00000000 00000000 00000000 - 00111111 11111111 11111111 11111111 = 00011111 11111111 11111111 11111111 -+ 口诀:对于这种数:00100000 00000000 00000000 00000000,它是“某一位是 1,右边全是 0”。 ### 【扩展】掩码 掩码 Mask,就是一串二进制位,用来通过位运算“筛选”出你想要的部分。在ThreadPoolExecutor里: From 17d5b480db8c127ed08fb9227f7ab0d9277b247b Mon Sep 17 00:00:00 2001 From: coderbruis Date: Wed, 15 Jul 2026 20:25:47 +0800 Subject: [PATCH 13/21] =?UTF-8?q?=E6=96=B0=E5=A2=9ENetty=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 41a5b54..5f8a663 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,7 @@ Kafka / RocketMQ 消息系统源码
Netty 底层源码解析 -- Netty 版本:4.1.43.Final +- Netty 版本:4.2 - [Netty 概念扫盲](https://github.com/coderbruis/JavaSourceCodeLearning/blob/master/note/Netty/Netty%E6%A6%82%E5%BF%B5%E6%89%AB%E7%9B%B2.md) - [二进制运算以及源码、反码以及补码学习](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/%E4%BA%8C%E8%BF%9B%E5%88%B6.md) - [Netty 源码包结构](https://github.com/coderbruis/JavaSourceLearning/blob/master/note/Netty/Netty%E6%BA%90%E7%A0%81%E5%8C%85%E7%BB%93%E6%9E%84.md) @@ -178,6 +178,12 @@ Kafka / RocketMQ 消息系统源码 Netty 实战课相关代码位于 `Spring-Netty` 模块下的 `com/bruis/learnnetty/im` 包。 +Netty架构图 +![Netty整体架构图](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/Netty4.2.x.png) + +Netty主从Reactor架构图 +![Netty主从Reactor架构图](https://github.com/coderbruis/JavaSourceCodeLearning/releases/download/images-v1/Netty4.2.x.Reactor.png) +
From 59e16a86a1e61d46b1d8548d11e542f88bfdb79e Mon Sep 17 00:00:00 2001 From: coderbruis Date: Thu, 16 Jul 2026 13:58:31 +0800 Subject: [PATCH 14/21] =?UTF-8?q?=E6=9B=B4=E6=96=B0ThreadLocal=E5=86=85?= =?UTF-8?q?=E5=AD=98=E6=B3=84=E6=BC=8F=E4=BC=AA=E4=BB=A3=E7=A0=81=E5=88=86?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...245\345\210\206\346\236\220ThreadLocal.md" | 142 +++++++++++++++++- 1 file changed, 138 insertions(+), 4 deletions(-) diff --git "a/note/JDK/\346\267\261\345\205\245\345\210\206\346\236\220ThreadLocal.md" "b/note/JDK/\346\267\261\345\205\245\345\210\206\346\236\220ThreadLocal.md" index ff6c108..44f8c01 100644 --- "a/note/JDK/\346\267\261\345\205\245\345\210\206\346\236\220ThreadLocal.md" +++ "b/note/JDK/\346\267\261\345\205\245\345\210\206\346\236\220ThreadLocal.md" @@ -150,7 +150,7 @@ Entry是ThreadLocalMap数组中的核心元素,它继承了WeakReference。核 Thread -> ThreadLocalMap -> Entry -> ThreadLocal ``` -在线程池场景下,线程可能长期存活。只要线程不结束,ThreadLocalMap 就还在,Entry 也还在,那么 ThreadLocal 对象就永远无法被 GC。即使业务代码已经不再持有这个 ThreadLocal 变量了,它也会被 Entry 强行引用住。这会导致:**ThreadLocal 对象无法回收,对应的 value 也无法回收。** +在线程池场景下,线程可能长期存活。只要线程不结束,ThreadLocalMap 就还在,Entry 也还在,那么 ThreadLocal 对象就永远无法被 GC。即使业务代码已经不再持有这个 ThreadLocal 变量了,它也会被 Entry 强行引用住。这会导致:**ThreadLocal 对象无法回收,对应的 value 也无法回收。** 这个后果就是线程长期持有已经没用的 ThreadLocal 和 value,导致内存释放不了,严重时内存泄漏、数据串用、甚至 OOM。 @@ -516,7 +516,9 @@ new Thread(() -> { }).start(); ``` - 而 InheritableThreadLocal 可以让子线程继承父线程的值: +```plain +而 InheritableThreadLocal 可以让子线程继承父线程的值: +``` ```java InheritableThreadLocal local = new InheritableThreadLocal<>(); @@ -576,7 +578,7 @@ private ThreadLocalMap(ThreadLocalMap parentMap) { -但是现在基本都没有直接通过new Thread()的方式创建线程了,基本都是通过线程池来管理线程。而在常规业务线程池里,InheritableThreadLocal 基本不适合作为上下文传递方案。它的继承时机是**“创建线程时”**,而线程池的线程通常早就创建好了,任务提交时不会重新继承父线程上下文。 +但是现在基本都没有直接通过new Thread()的方式创建线程了,基本都是通过线程池来管理线程。而在常规业务线程池里,InheritableThreadLocal 基本不适合作为上下文传递方案。它的继承时机是**“创建线程时”**,而线程池的线程通常早就创建好了,任务提交时不会重新继承父线程上下文。 线程池上下文传递方案,用的最多的就是阿里的TransmittableThreadLocal,简称 TTL。 @@ -620,7 +622,139 @@ class TtlRunnable implements Runnable { } ``` - + 总结:TransmittableThreadLocal在线程池里传值,是通过包装任务,在任务提交时捕获父线程的 TTL 快照,在工作线程执行前恢复这份快照,执行结束后再还原工作线程原上下文来实现的。 +# ThreadLocal内存泄漏代码分析 +下面是一段ThreadLocal内存泄漏的伪代码,通过这段伪代码加深ThreadLocal底层原理的理解。 + +```java +static final ThreadLocal USER_CONTEXT = new ThreadLocal<>(); + +void handleRequest(Request request) { + UserInfo userInfo = getUserInfo(request); + USER_CONTEXT.set(userInfo); + + doBusiness(); + + // 忘记执行 USER_CONTEXT.remove() +} +``` + +上述这段代码是用户登录之后,获取用户信息并存到ThreadLocal中,但是方法结束后并未执行:USER_CONTEXT.remove()移除ThreadLocal中的用户信息,这会造成内存泄漏,下面通过引用链来分析下内存泄漏的原因。 + +方法在USER_CONTEXT.set(userInfo)之后,引用关系如下图: + +```java +GC Roots +│ +├── ClassLoader +│ │ 强引用 +│ ▼ +│ Class对象 +│ │ 静态字段强引用 +│ ▼ +│ USER_CONTEXT +│ │ 强引用 +│ ▼ +│ ThreadLocal对象 ◀-------------------┐ +│ │ +└── 工作线程 Thread │ + │ │ + ├── 线程栈 │ + │ │ │ + │ ▼ │ + │ handleRequest()栈帧 │ + │ │ │ + │ ▼ │ + │ 局部变量 userInfo │ + │ │ 强引用 │ + │ ▼ │ + │ UserInfo对象 ◀──────────┐ │ + │ │ │ + └── threadLocals │ │ + │ 强引用 │ │ + ▼ │ │ + ThreadLocalMap │ │ + │ 强引用 │ │ + ▼ │ │ + Entry │ │ + ├── value强引用 ─────┘ │ + │ │ + └---- key弱引用 -----------┘ +``` + +这里有个细节,调用了USER_CONTEXT.set(userInfo)之后,ThreadLocal对象被USER_CONTEXT强引用引用,同时还被Entry这个弱引用给引用了。 + + + +当handleRequest()方法调用完之后,userInfo局部变量消失,此时引用关系图如下 + +```java +GC Roots +│ +├── ClassLoader +│ │ 强引用 +│ ▼ +│ Class对象 +│ │ 静态字段强引用 +│ ▼ +│ USER_CONTEXT +│ │ 强引用 +│ ▼ +│ ThreadLocal对象 ◀--------------------┐ +│ │ +└── 工作线程 Thread │ + │ │ + ├── 线程栈 │ + │ │ │ + │ └── handleRequest栈帧已消失 │ + │ │ + └── threadLocals │ + │ 强引用 │ + ▼ │ + ThreadLocalMap │ + │ 强引用 │ + ▼ │ + Entry │ + ├---- key弱引用 -----------┘ + │ + └── value强引用 + │ + ▼ + UserInfo对象 +``` + +此时发生变化的是,局部变量对应的引用已经断开。 + +```java +局部变量userInfo ──X──> UserInfo对象 +``` + + + +因此,即使局部变量消失,`UserInfo` 仍然无法被 GC 回收。只有执行 `USER_CONTEXT.remove()` 清理对应 Entry,才能断开这条引用链。 + + + +所以正确的写法如下 + +```java +private static final ThreadLocal USER_CONTEXT = + new ThreadLocal<>(); + +public void handleRequest(Request request) { + try { + UserInfo userInfo = getUserInfo(request); + USER_CONTEXT.set(userInfo); + + // 处理具体业务 + doBusiness(); + } finally { + // 无论正常结束还是发生异常,都必须清理 + USER_CONTEXT.remove(); + } +} +``` + From 7cf7a1b788b793999baa86c4734b2230f48aa3d5 Mon Sep 17 00:00:00 2001 From: coderbruis Date: Fri, 17 Jul 2026 22:11:02 +0800 Subject: [PATCH 15/21] =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=A9=BA=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...15\350\257\225\346\234\272\345\210\266.md" | 0 ...23\345\255\230\346\234\272\345\210\266.md" | 0 ...20\347\240\201\346\220\255\345\273\272.md" | 52 ------------------- ...bo\351\235\242\350\257\225\351\242\230.md" | 22 -------- ...05\350\257\273\351\241\272\345\272\217.md" | 11 ---- 5 files changed, 85 deletions(-) delete mode 100644 "note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\224\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\351\207\215\350\257\225\346\234\272\345\210\266.md" delete mode 100644 "note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\345\233\233\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\347\274\223\345\255\230\346\234\272\345\210\266.md" delete mode 100644 "note/Dubbo/Dubbo\346\272\220\347\240\201\346\220\255\345\273\272.md" delete mode 100644 "note/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" delete mode 100644 "note/Dubbo/dubbo\345\272\225\345\261\202\345\216\237\347\220\206\351\230\205\350\257\273\351\241\272\345\272\217.md" diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\224\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\351\207\215\350\257\225\346\234\272\345\210\266.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\344\272\224\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\351\207\215\350\257\225\346\234\272\345\210\266.md" deleted file mode 100644 index e69de29..0000000 diff --git "a/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\345\233\233\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\347\274\223\345\255\230\346\234\272\345\210\266.md" "b/note/Dubbo/Dubbo\345\272\225\345\261\202\346\272\220\347\240\201\345\255\246\344\271\240\357\274\210\345\233\233\357\274\211\342\200\224\342\200\224 Dubbo\347\232\204\346\263\250\345\206\214\344\270\255\345\277\203\347\274\223\345\255\230\346\234\272\345\210\266.md" deleted file mode 100644 index e69de29..0000000 diff --git "a/note/Dubbo/Dubbo\346\272\220\347\240\201\346\220\255\345\273\272.md" "b/note/Dubbo/Dubbo\346\272\220\347\240\201\346\220\255\345\273\272.md" deleted file mode 100644 index 70b098d..0000000 --- "a/note/Dubbo/Dubbo\346\272\220\347\240\201\346\220\255\345\273\272.md" +++ /dev/null @@ -1,52 +0,0 @@ -## 前言 - -想要深入学习Dubbo,最好的方式就是阅读并调用Dubbo源码,接下来先来动手搭建一个Dubbo源码环境。 - -## 正文 - -### 1. 下载源码 - -步骤: - -1. 先从dubbo源码官网github中fork一份到自己的github仓库中。 - - ``` - git clone git@github.com:xxxxxxxx/dubbo.git - ``` - -2. 使用命令:git branch v2.7.8。 切换到分支2.7.8。 - - ``` - git checkout -b dubbo-2.7.8 dubbo-2.7.8 - ``` - -3. 导入方式(IDEA导入方式) - - 可以通过IDEA ——> File ——> Open ——> pom.xml ——> open as project - - 然后让IDEA下载相关的依赖,等下载完成即可。 - -4. mvn命令导入 - - ``` - mvn clean install -Dmaven.test.skip=true - ``` - - 然后执行下面的命令转换成 IDEA 项目: - - ``` - mvn idea:idea - ``` - - 如果执行报错了,则执行: - ``` - mvn idea:workspace - ``` - -### 2. 分支切换 - -本人Fork了官方Dubbo源码到本地仓库,并且新建了一个分支名为:SourceCode-of-Translation - -该分支主要用于进行源码注释,每个核心功能代码都有详细注释,欢迎大家Fork到本地,然后进行注释查看。 - -> 地址为:[SourceCode-of-Translation](https://github.com/coderbruis/dubbo) 下载到本地后,只需要切换到SourceCode-of-Translation分支即可。 diff --git "a/note/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" "b/note/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index a8a3d5b..0000000 --- "a/note/Dubbo/Dubbo\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,22 +0,0 @@ -## 前言 - -Dubbo面试题 - -## 正文 - -RPC非常重要,很多人面试的时候都挂在了这个地方!你要是还不懂RPC是什么?他的基本原理是什么?你一定要把下边的内容记起来!好好研究一下!特别是文中给出的一张关于RPC的基本流程图,重点中的重点,Dubbo RPC的基本执行流程就是他,RPC框架的基本原理也是他,别说我没告诉你!看了下边的内容你要掌握的内容如下,当然还有很多: - -1. RPC的由来,是怎么一步一步演进出来的; -2. RPC的基本架构是什么; -3. RPC的基本实现原理, 重点; -4. REST和SOAP、PRC的区别; -5. 整个调用的过程经历了哪几部?和SpringMVC流程区别? - - -### 1. 为什么要有RPC - -随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。 - -- [PRC原理图](https://github.com/coderbruis/JavaSourceLearning/blob/master/images/PRC/rpc.jpg) - - diff --git "a/note/Dubbo/dubbo\345\272\225\345\261\202\345\216\237\347\220\206\351\230\205\350\257\273\351\241\272\345\272\217.md" "b/note/Dubbo/dubbo\345\272\225\345\261\202\345\216\237\347\220\206\351\230\205\350\257\273\351\241\272\345\272\217.md" deleted file mode 100644 index 18929b7..0000000 --- "a/note/Dubbo/dubbo\345\272\225\345\261\202\345\216\237\347\220\206\351\230\205\350\257\273\351\241\272\345\272\217.md" +++ /dev/null @@ -1,11 +0,0 @@ -## 前言 - -在真正深入Dubbo底层源码前,先梳理好阅读路线,这样才能够事半功倍。 - -## 正文 - -### 1. 配置加载流程? - -[配置加载流程](http://dubbo.apache.org/zh-cn/docs/user/configuration/configuration-load-process.html) - -### 2. \ No newline at end of file From a7f505d5bbcb7f19b523626c40a2e71a7a129e67 Mon Sep 17 00:00:00 2001 From: coderbruis Date: Sat, 18 Jul 2026 22:03:26 +0800 Subject: [PATCH 16/21] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9E=B6=E6=9E=84?= =?UTF-8?q?=E6=96=B9=E6=A1=88=E4=BC=AA=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- architecture/README.md | 46 +++++ architecture/pom.xml | 78 ++++++++ .../arch/policy/ActiveSnapshotRegistry.java | 61 +++++++ .../arch/policy/DefaultSnapshotValidator.java | 15 ++ .../policy/FileSystemSnapshotDirectory.java | 45 +++++ .../java/com/arch/policy/MessagePosition.java | 20 +++ .../java/com/arch/policy/PolicyChange.java | 30 ++++ .../java/com/arch/policy/PolicyRecord.java | 27 +++ .../arch/policy/PolicySearchApplication.java | 15 ++ .../java/com/arch/policy/PolicySnapshot.java | 170 ++++++++++++++++++ .../arch/policy/PolicySnapshotService.java | 80 +++++++++ .../java/com/arch/policy/SnapshotBuilder.java | 56 ++++++ .../java/com/arch/policy/SnapshotPorts.java | 28 +++ .../aggregation/AsyncSearchCoordinator.java | 76 ++++++++ .../aggregation/LocalSearchWaiters.java | 32 ++++ .../arch/policy/aggregation/SearchState.java | 3 + .../policy/aggregation/SearchStateStore.java | 13 ++ .../aggregation/SupplierCallbackService.java | 38 ++++ .../aggregation/SupplierTaskDispatcher.java | 9 + .../com/arch/policy/api/CallbackResponse.java | 10 ++ .../arch/policy/api/PolicySearchRequest.java | 22 +++ .../arch/policy/api/PolicySearchResponse.java | 22 +++ .../policy/api/PolicySearchRpcService.java | 7 + .../policy/api/SupplierCallbackRequest.java | 25 +++ .../api/SupplierCallbackRpcService.java | 5 + .../application/PolicyIncrementalUpdater.java | 16 ++ .../application/PolicyStartupRunner.java | 19 ++ .../config/PolicySearchConfiguration.java | 109 +++++++++++ .../demo/DemoSupplierTaskDispatcher.java | 47 +++++ .../kafka/KafkaPolicyChangeListener.java | 45 +++++ .../policy/kafka/PolicyChangeMessage.java | 23 +++ .../policy/redis/RedisSearchStateStore.java | 84 +++++++++ .../redis/SearchFinishedSubscriber.java | 15 ++ .../policy/rpc/DubboPolicySearchService.java | 34 ++++ .../rpc/DubboSupplierCallbackService.java | 23 +++ .../src/main/resources/application.yml | 32 ++++ .../policy/AsyncSearchCoordinatorTest.java | 99 ++++++++++ .../arch/policy/SnapshotLifecycleTest.java | 119 ++++++++++++ 38 files changed, 1598 insertions(+) create mode 100644 architecture/README.md create mode 100644 architecture/pom.xml create mode 100644 architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java create mode 100644 architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java create mode 100644 architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java create mode 100644 architecture/src/main/java/com/arch/policy/MessagePosition.java create mode 100644 architecture/src/main/java/com/arch/policy/PolicyChange.java create mode 100644 architecture/src/main/java/com/arch/policy/PolicyRecord.java create mode 100644 architecture/src/main/java/com/arch/policy/PolicySearchApplication.java create mode 100644 architecture/src/main/java/com/arch/policy/PolicySnapshot.java create mode 100644 architecture/src/main/java/com/arch/policy/PolicySnapshotService.java create mode 100644 architecture/src/main/java/com/arch/policy/SnapshotBuilder.java create mode 100644 architecture/src/main/java/com/arch/policy/SnapshotPorts.java create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/SearchState.java create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java create mode 100644 architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java create mode 100644 architecture/src/main/java/com/arch/policy/api/CallbackResponse.java create mode 100644 architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java create mode 100644 architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java create mode 100644 architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java create mode 100644 architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java create mode 100644 architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java create mode 100644 architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java create mode 100644 architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java create mode 100644 architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java create mode 100644 architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java create mode 100644 architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java create mode 100644 architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java create mode 100644 architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java create mode 100644 architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java create mode 100644 architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java create mode 100644 architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java create mode 100644 architecture/src/main/resources/application.yml create mode 100644 architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java create mode 100644 architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java diff --git a/architecture/README.md b/architecture/README.md new file mode 100644 index 0000000..ed7e14c --- /dev/null +++ b/architecture/README.md @@ -0,0 +1,46 @@ +# Policy snapshot lifecycle + +This module implements version-isolated policy snapshots. Each snapshot owns a RocksDB detail +store, a bitmap index, and the incremental-message position at which both were built. + +## Integration flow + +1. Implement `FullPolicyLoader` to stream the full-data cut into `PolicySnapshot.upsert` and + return the cut's message position. +2. Implement `IncrementalReplayer` to apply upserts/deletes after that position. The builder + repeats replay until it reaches a stable latest position. +3. Supply `DefaultSnapshotValidator` (or a stricter domain validator), a + `FileSystemSnapshotDirectory`, and construct `SnapshotBuilder`. +4. Call `PolicySnapshotService.start(version)` during startup. Keep the application's readiness + probe bound to `service.isReady()`; failures remain unready and retry. +5. Call `refresh(newVersion)` at runtime. A failed candidate is discarded. A valid candidate is + atomically activated while the old snapshot continues serving existing leases. +6. Every search must use `try (SnapshotLease lease = registry.acquire())`. Releasing the last old + lease closes RocksDB and deletes that retired version's directory. + +The module intentionally leaves message-broker and full-data-source clients behind interfaces so +the snapshot consistency rules are independent of Kafka, HTTP, database, or framework choices. + +## Package layout + +- `api`: serializable Dubbo contract and request/response DTOs. +- `rpc`: Dubbo search provider and supplier callback provider. +- `aggregation`: Redis-backed fan-out/fan-in search coordination and local waiters. +- `redis`: atomic Lua state transitions and Pub/Sub early wake-up. +- `demo`: asynchronous downstream supplier simulation. +- `kafka`: JSON policy-change consumer and message DTO. +- `application`: startup and incremental-update use cases. +- root `policy` package: versioned RocksDB/Bitmap snapshot domain and lifecycle. + +Kafka messages use a globally monotonic `position` so duplicate/out-of-order delivery is ignored. +If the topic has multiple partitions, the producer must supply this global sequence; otherwise the +position model should be replaced with a per-partition offset map. + +## Async supplier search + +`asyncSearch` initializes pending suppliers, state and result TTL atomically in Redis, registers a +local waiter, double-checks Redis, then dispatches all supplier tasks. Supplier callbacks append +result chunks and remove a supplier from the pending set only on its final callback. The Lua script +sets `COMPLETED` and publishes `search-finished` when the last supplier finishes. Pub/Sub only wakes +the local waiter early; Redis remains the source of truth and is checked every 200 ms. Timeout is +also a Lua state transition and returns all partial results already recorded. diff --git a/architecture/pom.xml b/architecture/pom.xml new file mode 100644 index 0000000..4f7f396 --- /dev/null +++ b/architecture/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + com.arch + architecture + 1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + 5.10.2 + 2.7.18 + 3.2.15 + + + + + org.rocksdb + rocksdbjni + 8.11.3 + + + org.roaringbitmap + RoaringBitmap + 0.9.47 + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-data-redis + ${spring-boot.version} + + + org.springframework.kafka + spring-kafka + 2.9.13 + + + com.fasterxml.jackson.core + jackson-databind + 2.13.5 + + + org.apache.dubbo + dubbo-spring-boot-starter + ${dubbo.version} + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + diff --git a/architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java b/architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java new file mode 100644 index 0000000..f086e2c --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java @@ -0,0 +1,61 @@ +package com.arch.policy; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** Atomic activation plus draining of queries that still hold the old version. */ +public final class ActiveSnapshotRegistry implements AutoCloseable { + private Entry active; + + public synchronized boolean isReady() { return active != null; } + + public synchronized SnapshotLease acquire() { + if (active == null) throw new IllegalStateException("policy snapshot is not ready"); + active.references++; + return new SnapshotLease(active); + } + + public synchronized void activate(PolicySnapshot snapshot) { + Entry previous = active; + active = new Entry(snapshot); + if (previous != null) retire(previous); + } + + @Override public synchronized void close() { + Entry previous = active; + active = null; + if (previous != null) retire(previous); + } + + private void release(Entry entry) { + synchronized (this) { + entry.references--; + closeWhenDrained(entry); + } + } + + private void retire(Entry entry) { + entry.retired = true; + closeWhenDrained(entry); + } + + private void closeWhenDrained(Entry entry) { + if (entry.retired && entry.references == 0) entry.snapshot.closeAndDelete(); + } + + private static final class Entry { + private final PolicySnapshot snapshot; + private int references; + private boolean retired; + private Entry(PolicySnapshot snapshot) { this.snapshot = snapshot; } + } + + public final class SnapshotLease implements AutoCloseable { + private final Entry entry; + private final AtomicBoolean released = new AtomicBoolean(); + private SnapshotLease(Entry entry) { this.entry = entry; } + public PolicySnapshot snapshot() { return entry.snapshot; } + @Override public void close() { + if (released.compareAndSet(false, true)) release(entry); + } + } +} diff --git a/architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java b/architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java new file mode 100644 index 0000000..2c8c848 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java @@ -0,0 +1,15 @@ +package com.arch.policy; + +import static com.arch.policy.SnapshotPorts.SnapshotValidator; + +/** Baseline invariants; domain-specific checks can be supplied through SnapshotValidator. */ +public final class DefaultSnapshotValidator implements SnapshotValidator { + @Override public void validate(PolicySnapshot candidate, MessagePosition expectedPosition) throws Exception { + if (!candidate.getPosition().equals(expectedPosition)) { + throw new IllegalStateException("message position mismatch"); + } + if (candidate.policyCount() != candidate.indexedPolicyCount()) { + throw new IllegalStateException("RocksDB and bitmap policy counts differ"); + } + } +} diff --git a/architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java b/architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java new file mode 100644 index 0000000..64c0bb1 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java @@ -0,0 +1,45 @@ +package com.arch.policy; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Stream; + +import static com.arch.policy.SnapshotPorts.SnapshotDirectory; + +/** Keeps every version in an isolated directory and removes abandoned candidates. */ +public final class FileSystemSnapshotDirectory implements SnapshotDirectory { + private final Path root; + + public FileSystemSnapshotDirectory(Path root) { this.root = root; } + + @Override public Path create(String version) throws IOException { + Path directory = root.resolve(safeVersion(version)); + delete(directory); + return Files.createDirectories(directory); + } + + @Override public void delete(Path directory) throws IOException { + if (!Files.exists(directory)) return; + try (Stream paths = Files.walk(directory)) { + paths.sorted(Comparator.reverseOrder()).forEach(path -> { + try { Files.deleteIfExists(path); } + catch (IOException failure) { throw new DeleteFailure(failure); } + }); + } catch (DeleteFailure failure) { + throw (IOException) failure.getCause(); + } + } + + private static String safeVersion(String version) { + if (version == null || !version.matches("[A-Za-z0-9._-]+")) { + throw new IllegalArgumentException("invalid snapshot version: " + version); + } + return version; + } + + private static final class DeleteFailure extends RuntimeException { + private DeleteFailure(IOException cause) { super(cause); } + } +} diff --git a/architecture/src/main/java/com/arch/policy/MessagePosition.java b/architecture/src/main/java/com/arch/policy/MessagePosition.java new file mode 100644 index 0000000..50794e1 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/MessagePosition.java @@ -0,0 +1,20 @@ +package com.arch.policy; + +public final class MessagePosition implements Comparable { + public static final MessagePosition BEGINNING = new MessagePosition(0); + private final long value; + + public MessagePosition(long value) { + if (value < 0) throw new IllegalArgumentException("position must be non-negative"); + this.value = value; + } + + public long getValue() { return value; } + + @Override public int compareTo(MessagePosition other) { return Long.compare(value, other.value); } + @Override public boolean equals(Object other) { + return other instanceof MessagePosition && value == ((MessagePosition) other).value; + } + @Override public int hashCode() { return Long.valueOf(value).hashCode(); } + @Override public String toString() { return Long.toString(value); } +} diff --git a/architecture/src/main/java/com/arch/policy/PolicyChange.java b/architecture/src/main/java/com/arch/policy/PolicyChange.java new file mode 100644 index 0000000..5dfcecc --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/PolicyChange.java @@ -0,0 +1,30 @@ +package com.arch.policy; + +public final class PolicyChange { + public enum Type { UPSERT, DELETE } + + private final Type type; + private final int policyId; + private final PolicyRecord policy; + private final MessagePosition position; + + private PolicyChange(Type type, int policyId, PolicyRecord policy, MessagePosition position) { + this.type = type; + this.policyId = policyId; + this.policy = policy; + this.position = position; + } + + public static PolicyChange upsert(PolicyRecord policy, MessagePosition position) { + return new PolicyChange(Type.UPSERT, policy.getId(), policy, position); + } + + public static PolicyChange delete(int policyId, MessagePosition position) { + return new PolicyChange(Type.DELETE, policyId, null, position); + } + + public Type getType() { return type; } + public int getPolicyId() { return policyId; } + public PolicyRecord getPolicy() { return policy; } + public MessagePosition getPosition() { return position; } +} diff --git a/architecture/src/main/java/com/arch/policy/PolicyRecord.java b/architecture/src/main/java/com/arch/policy/PolicyRecord.java new file mode 100644 index 0000000..42371e3 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/PolicyRecord.java @@ -0,0 +1,27 @@ +package com.arch.policy; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public final class PolicyRecord { + private final int id; + private final byte[] detail; + private final Set indexTerms; + + public PolicyRecord(int id, byte[] detail, Set indexTerms) { + if (id < 0) { + throw new IllegalArgumentException("policy id must be non-negative"); + } + this.id = id; + this.detail = Arrays.copyOf(detail, detail.length); + this.indexTerms = Collections.unmodifiableSet(new HashSet(indexTerms)); + } + + public int getId() { return id; } + + public byte[] getDetail() { return Arrays.copyOf(detail, detail.length); } + + public Set getIndexTerms() { return indexTerms; } +} diff --git a/architecture/src/main/java/com/arch/policy/PolicySearchApplication.java b/architecture/src/main/java/com/arch/policy/PolicySearchApplication.java new file mode 100644 index 0000000..9bdfd8b --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/PolicySearchApplication.java @@ -0,0 +1,15 @@ +package com.arch.policy; + +import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.kafka.annotation.EnableKafka; + +@EnableKafka +@EnableDubbo +@SpringBootApplication +public class PolicySearchApplication { + public static void main(String[] args) { + SpringApplication.run(PolicySearchApplication.class, args); + } +} diff --git a/architecture/src/main/java/com/arch/policy/PolicySnapshot.java b/architecture/src/main/java/com/arch/policy/PolicySnapshot.java new file mode 100644 index 0000000..d4e7e53 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/PolicySnapshot.java @@ -0,0 +1,170 @@ +package com.arch.policy; + +import org.roaringbitmap.RoaringBitmap; +import org.rocksdb.Options; +import org.rocksdb.RocksDB; +import org.rocksdb.RocksDBException; +import org.rocksdb.RocksIterator; + +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +/** A version-isolated RocksDB detail store and its matching bitmap index. */ +public final class PolicySnapshot implements AutoCloseable { + static { RocksDB.loadLibrary(); } + + private final String version; + private final Path directory; + private final Options options; + private final RocksDB database; + private final Map bitmapIndex = new HashMap(); + private final RoaringBitmap allPolicyIds = new RoaringBitmap(); + private final Map> termsByPolicy = new HashMap>(); + private MessagePosition position = MessagePosition.BEGINNING; + private boolean closed; + + public PolicySnapshot(String version, Path directory) throws RocksDBException { + this.version = version; + this.directory = directory; + this.options = new Options().setCreateIfMissing(true); + this.database = RocksDB.open(options, directory.toString()); + } + + public synchronized void upsert(PolicyRecord policy) throws RocksDBException { + ensureOpen(); + removeFromIndex(policy.getId()); + database.put(key(policy.getId()), policy.getDetail()); + allPolicyIds.add(policy.getId()); + termsByPolicy.put(policy.getId(), policy.getIndexTerms()); + for (String term : policy.getIndexTerms()) { + RoaringBitmap bitmap = bitmapIndex.get(term); + if (bitmap == null) { + bitmap = new RoaringBitmap(); + bitmapIndex.put(term, bitmap); + } + bitmap.add(policy.getId()); + } + } + + public synchronized void delete(int policyId) throws RocksDBException { + ensureOpen(); + database.delete(key(policyId)); + allPolicyIds.remove(policyId); + removeFromIndex(policyId); + } + + public synchronized byte[] findDetail(int policyId) throws RocksDBException { + ensureOpen(); + return database.get(key(policyId)); + } + + public synchronized RoaringBitmap findPolicyIds(String term) { + ensureOpen(); + RoaringBitmap bitmap = bitmapIndex.get(term); + return bitmap == null ? new RoaringBitmap() : bitmap.clone(); + } + + public synchronized RoaringBitmap allPolicyIds() { + ensureOpen(); + return allPolicyIds.clone(); + } + + /** Applies an ordered event and advances its position in the same monitor. */ + public synchronized boolean apply(PolicyChange change) throws RocksDBException { + ensureOpen(); + if (change.getPosition().compareTo(position) <= 0) return false; + if (change.getType() == PolicyChange.Type.DELETE) delete(change.getPolicyId()); + else upsert(change.getPolicy()); + advanceTo(change.getPosition()); + return true; + } + + public synchronized long policyCount() throws RocksDBException { + ensureOpen(); + long count = 0; + try (RocksIterator iterator = database.newIterator()) { + for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) count++; + iterator.status(); + } + return count; + } + + public synchronized Map bitmapCardinalities() { + ensureOpen(); + Map result = new HashMap(); + for (Map.Entry entry : bitmapIndex.entrySet()) { + result.put(entry.getKey(), entry.getValue().getCardinality()); + } + return Collections.unmodifiableMap(result); + } + + public synchronized int indexedPolicyCount() { + ensureOpen(); + RoaringBitmap all = new RoaringBitmap(); + for (RoaringBitmap bitmap : bitmapIndex.values()) all.or(bitmap); + return all.getCardinality(); + } + + public String getVersion() { return version; } + public Path getDirectory() { return directory; } + public synchronized MessagePosition getPosition() { return position; } + public synchronized void advanceTo(MessagePosition newPosition) { + ensureOpen(); + if (newPosition.compareTo(position) < 0) throw new IllegalArgumentException("position cannot move backwards"); + position = newPosition; + } + + @Override public synchronized void close() { + if (closed) return; + closed = true; + database.close(); + options.close(); + bitmapIndex.clear(); + allPolicyIds.clear(); + termsByPolicy.clear(); + } + + public void closeAndDelete() { + close(); + try { + if (!Files.exists(directory)) return; + try (Stream paths = Files.walk(directory)) { + paths.sorted(java.util.Comparator.reverseOrder()).forEach(path -> { + try { Files.deleteIfExists(path); } + catch (IOException failure) { throw new DeleteFailure(failure); } + }); + } + } catch (IOException failure) { + throw new IllegalStateException("cannot delete snapshot " + directory, failure); + } catch (DeleteFailure failure) { + throw new IllegalStateException("cannot delete snapshot " + directory, failure.getCause()); + } + } + + private static final class DeleteFailure extends RuntimeException { + private DeleteFailure(IOException cause) { super(cause); } + } + + private void removeFromIndex(int policyId) { + Set oldTerms = termsByPolicy.remove(policyId); + if (oldTerms == null) return; + for (String term : oldTerms) { + RoaringBitmap bitmap = bitmapIndex.get(term); + bitmap.remove(policyId); + if (bitmap.isEmpty()) bitmapIndex.remove(term); + } + } + + private void ensureOpen() { + if (closed) throw new IllegalStateException("snapshot is closed: " + version); + } + + private static byte[] key(int id) { return ByteBuffer.allocate(4).putInt(id).array(); } +} diff --git a/architecture/src/main/java/com/arch/policy/PolicySnapshotService.java b/architecture/src/main/java/com/arch/policy/PolicySnapshotService.java new file mode 100644 index 0000000..d90fac8 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/PolicySnapshotService.java @@ -0,0 +1,80 @@ +package com.arch.policy; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** Coordinates startup retry and side-by-side runtime refresh. */ +public final class PolicySnapshotService implements AutoCloseable { + private final SnapshotBuilder builder; + private final ActiveSnapshotRegistry registry; + private final ScheduledExecutorService executor; + private final long retryDelayMillis; + private final AtomicBoolean building = new AtomicBoolean(); + private volatile boolean stopped; + + public PolicySnapshotService(SnapshotBuilder builder, ActiveSnapshotRegistry registry, + ScheduledExecutorService executor, long retryDelayMillis) { + this.builder = builder; + this.registry = registry; + this.executor = executor; + this.retryDelayMillis = retryDelayMillis; + } + + /** Keeps readiness false and retries until the first valid snapshot is activated. */ + public void start(String version) { submitBuild(version, true); } + + /** Returns false when another candidate is already being built. */ + public boolean refresh(String version) { return submitBuild(version, false); } + + public boolean isReady() { return registry.isReady(); } + public boolean isBuilding() { return building.get(); } + + /** Used by application startup: Spring startup does not finish before a snapshot is ready. */ + public void initializeBlocking(String version) throws InterruptedException { + if (!building.compareAndSet(false, true)) throw new IllegalStateException("snapshot build already running"); + try { + while (!stopped && !registry.isReady()) { + try { + registry.activate(builder.build(version)); + } catch (Exception failure) { + Thread.sleep(retryDelayMillis); + } + } + } finally { + building.set(false); + } + } + + private boolean submitBuild(final String version, final boolean retryOnFailure) { + if (stopped || !building.compareAndSet(false, true)) return false; + executor.execute(new Runnable() { + @Override public void run() { buildAndActivate(version, retryOnFailure); } + }); + return true; + } + + private void buildAndActivate(final String version, final boolean retryOnFailure) { + try { + PolicySnapshot candidate = builder.build(version); + if (stopped) candidate.closeAndDelete(); + else registry.activate(candidate); + } catch (Exception ignored) { + if (retryOnFailure && !stopped) { + executor.schedule(new Runnable() { + @Override public void run() { buildAndActivate(version, true); } + }, retryDelayMillis, TimeUnit.MILLISECONDS); + return; + } + } finally { + // Startup retry owns the build slot until it succeeds or the service stops. + if (!retryOnFailure || registry.isReady() || stopped) building.set(false); + } + } + + @Override public void close() { + stopped = true; + registry.close(); + executor.shutdownNow(); + } +} diff --git a/architecture/src/main/java/com/arch/policy/SnapshotBuilder.java b/architecture/src/main/java/com/arch/policy/SnapshotBuilder.java new file mode 100644 index 0000000..fbd2ccf --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/SnapshotBuilder.java @@ -0,0 +1,56 @@ +package com.arch.policy; + +import org.rocksdb.RocksDBException; + +import java.nio.file.Path; + +import static com.arch.policy.SnapshotPorts.*; + +/** Builds a candidate without exposing it to queries. */ +public final class SnapshotBuilder { + private final FullPolicyLoader fullLoader; + private final IncrementalReplayer replayer; + private final SnapshotValidator validator; + private final SnapshotDirectory directories; + + public SnapshotBuilder(FullPolicyLoader fullLoader, IncrementalReplayer replayer, + SnapshotValidator validator, SnapshotDirectory directories) { + this.fullLoader = fullLoader; + this.replayer = replayer; + this.validator = validator; + this.directories = directories; + } + + public PolicySnapshot build(String version) throws Exception { + Path directory = directories.create(version); + PolicySnapshot candidate = null; + try { + candidate = open(version, directory); + MessagePosition position = fullLoader.loadInto(candidate); + candidate.advanceTo(position); + catchUp(candidate); + validator.validate(candidate, candidate.getPosition()); + return candidate; + } catch (Exception failure) { + if (candidate != null) candidate.close(); + directories.delete(directory); + throw failure; + } + } + + private void catchUp(PolicySnapshot candidate) throws Exception { + while (true) { + MessagePosition target = replayer.latestPosition(); + if (candidate.getPosition().compareTo(target) >= 0) return; + MessagePosition replayed = replayer.replayInto(candidate, candidate.getPosition(), target); + if (replayed.compareTo(candidate.getPosition()) <= 0 || replayed.compareTo(target) > 0) { + throw new IllegalStateException("incremental replay made invalid progress"); + } + candidate.advanceTo(replayed); + } + } + + private PolicySnapshot open(String version, Path directory) throws RocksDBException { + return new PolicySnapshot(version, directory); + } +} diff --git a/architecture/src/main/java/com/arch/policy/SnapshotPorts.java b/architecture/src/main/java/com/arch/policy/SnapshotPorts.java new file mode 100644 index 0000000..9e3c4d4 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/SnapshotPorts.java @@ -0,0 +1,28 @@ +package com.arch.policy; + +import java.nio.file.Path; + +public final class SnapshotPorts { + private SnapshotPorts() {} + + public interface FullPolicyLoader { + /** Returns the message position corresponding to the full-data cut. */ + MessagePosition loadInto(PolicySnapshot target) throws Exception; + } + + public interface IncrementalReplayer { + /** Replays (position, latest] and returns the actual replayed position. */ + MessagePosition replayInto(PolicySnapshot target, MessagePosition position, + MessagePosition latest) throws Exception; + MessagePosition latestPosition() throws Exception; + } + + public interface SnapshotValidator { + void validate(PolicySnapshot candidate, MessagePosition expectedPosition) throws Exception; + } + + public interface SnapshotDirectory { + Path create(String version) throws Exception; + void delete(Path directory) throws Exception; + } +} diff --git a/architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java b/architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java new file mode 100644 index 0000000..a58d313 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java @@ -0,0 +1,76 @@ +package com.arch.policy.aggregation; + +import com.arch.policy.api.PolicySearchRequest; +import com.arch.policy.api.PolicySearchResponse; +import com.arch.policy.api.SupplierCallbackRequest; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public final class AsyncSearchCoordinator { + private static final long MAX_WAIT_SLICE_MILLIS = 200; + private final SearchStateStore stateStore; + private final SupplierTaskDispatcher dispatcher; + private final LocalSearchWaiters localWaiters; + private final ObjectMapper objectMapper; + private final long redisTtlSeconds; + + public AsyncSearchCoordinator(SearchStateStore stateStore, SupplierTaskDispatcher dispatcher, + LocalSearchWaiters localWaiters, ObjectMapper objectMapper, + long redisTtlSeconds) { + this.stateStore = stateStore; + this.dispatcher = dispatcher; + this.localWaiters = localWaiters; + this.objectMapper = objectMapper; + this.redisTtlSeconds = redisTtlSeconds; + } + + public PolicySearchResponse search(PolicySearchRequest request) throws Exception { + validate(request); + String searchKey = UUID.randomUUID().toString(); + Set suppliers = new LinkedHashSet(request.getSupplierIds()); + stateStore.initialize(searchKey, suppliers, redisTtlSeconds); + LocalSearchWaiters.Waiter waiter = localWaiters.register(searchKey); + try { + if (stateStore.getState(searchKey) == SearchState.COMPLETED) return aggregate(searchKey); + dispatcher.dispatch(searchKey, suppliers, request); + return awaitResults(searchKey, request.getTotalTimeoutMillis(), waiter); + } finally { + localWaiters.remove(searchKey, waiter); + } + } + + private PolicySearchResponse awaitResults(String searchKey, long timeoutMillis, + LocalSearchWaiters.Waiter waiter) throws Exception { + long deadline = System.currentTimeMillis() + timeoutMillis; + while (true) { + SearchState state = stateStore.getState(searchKey); + if (state == SearchState.COMPLETED) return aggregate(searchKey); + long remaining = deadline - System.currentTimeMillis(); + if (state == SearchState.TIMED_OUT || remaining <= 0) { + stateStore.markTimedOut(searchKey, redisTtlSeconds); + return aggregate(searchKey); + } + waiter.await(Math.min(remaining, MAX_WAIT_SLICE_MILLIS)); + } + } + + private PolicySearchResponse aggregate(String searchKey) throws Exception { + List results = new ArrayList(); + for (String payload : stateStore.getResultPayloads(searchKey)) { + results.add(objectMapper.readValue(payload, SupplierCallbackRequest.class)); + } + SearchState state = stateStore.getState(searchKey); + return new PolicySearchResponse(searchKey, state.name(), results); + } + + private static void validate(PolicySearchRequest request) { + if (request == null) throw new IllegalArgumentException("request is required"); + if (request.getTotalTimeoutMillis() <= 0) throw new IllegalArgumentException("timeout must be positive"); + if (request.getSupplierIds().isEmpty()) throw new IllegalArgumentException("supplierIds is required"); + } +} diff --git a/architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java b/architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java new file mode 100644 index 0000000..5ccf471 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java @@ -0,0 +1,32 @@ +package com.arch.policy.aggregation; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public final class LocalSearchWaiters { + private final ConcurrentMap waiters = new ConcurrentHashMap(); + + public Waiter register(String searchKey) { + Waiter waiter = new Waiter(); + Waiter existing = waiters.putIfAbsent(searchKey, waiter); + if (existing != null) throw new IllegalStateException("duplicate local search: " + searchKey); + return waiter; + } + + public void remove(String searchKey, Waiter waiter) { waiters.remove(searchKey, waiter); } + + public void signal(String searchKey) { + Waiter waiter = waiters.get(searchKey); + if (waiter != null) waiter.signal(); + } + + public static final class Waiter { + private final CountDownLatch completed = new CountDownLatch(1); + public void await(long millis) throws InterruptedException { + completed.await(millis, TimeUnit.MILLISECONDS); + } + private void signal() { completed.countDown(); } + } +} diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SearchState.java b/architecture/src/main/java/com/arch/policy/aggregation/SearchState.java new file mode 100644 index 0000000..fa6ae2e --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/aggregation/SearchState.java @@ -0,0 +1,3 @@ +package com.arch.policy.aggregation; + +public enum SearchState { WAITING, COMPLETED, TIMED_OUT } diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java b/architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java new file mode 100644 index 0000000..c0a0a33 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java @@ -0,0 +1,13 @@ +package com.arch.policy.aggregation; + +import java.util.List; +import java.util.Set; + +public interface SearchStateStore { + void initialize(String searchKey, Set supplierIds, long ttlSeconds); + SearchState getState(String searchKey); + List getResultPayloads(String searchKey); + void recordCallback(String searchKey, String supplierId, String payload, + boolean finished, long ttlSeconds); + void markTimedOut(String searchKey, long ttlSeconds); +} diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java b/architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java new file mode 100644 index 0000000..6113c36 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java @@ -0,0 +1,38 @@ +package com.arch.policy.aggregation; + +import com.arch.policy.api.SupplierCallbackRequest; +import com.fasterxml.jackson.databind.ObjectMapper; + +public final class SupplierCallbackService { + private final SearchStateStore stateStore; + private final ObjectMapper objectMapper; + private final long redisTtlSeconds; + + public SupplierCallbackService(SearchStateStore stateStore, ObjectMapper objectMapper, + long redisTtlSeconds) { + this.stateStore = stateStore; + this.objectMapper = objectMapper; + this.redisTtlSeconds = redisTtlSeconds; + } + + public void callback(SupplierCallbackRequest request) throws Exception { + validate(request); + String payload = request.getResults().isEmpty() ? "" : objectMapper.writeValueAsString(request); + stateStore.recordCallback(request.getSearchKey(), request.getSupplierId(), payload, + request.isSearchFinished(), redisTtlSeconds); + } + + public void markFinishedWithoutResult(String searchKey, String supplierId) throws Exception { + SupplierCallbackRequest request = new SupplierCallbackRequest(); + request.setSearchKey(searchKey); + request.setSupplierId(supplierId); + request.setSearchFinished(true); + callback(request); + } + + private static void validate(SupplierCallbackRequest request) { + if (request == null || request.getSearchKey() == null || request.getSupplierId() == null) { + throw new IllegalArgumentException("searchKey and supplierId are required"); + } + } +} diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java b/architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java new file mode 100644 index 0000000..ab43db7 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java @@ -0,0 +1,9 @@ +package com.arch.policy.aggregation; + +import com.arch.policy.api.PolicySearchRequest; + +import java.util.Set; + +public interface SupplierTaskDispatcher { + void dispatch(String searchKey, Set supplierIds, PolicySearchRequest request); +} diff --git a/architecture/src/main/java/com/arch/policy/api/CallbackResponse.java b/architecture/src/main/java/com/arch/policy/api/CallbackResponse.java new file mode 100644 index 0000000..43b5166 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/api/CallbackResponse.java @@ -0,0 +1,10 @@ +package com.arch.policy.api; + +import java.io.Serializable; + +public final class CallbackResponse implements Serializable { + private static final long serialVersionUID = 1L; + private final boolean success; + public CallbackResponse(boolean success) { this.success = success; } + public boolean isSuccess() { return success; } +} diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java b/architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java new file mode 100644 index 0000000..fe9a308 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java @@ -0,0 +1,22 @@ +package com.arch.policy.api; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class PolicySearchRequest implements Serializable { + private static final long serialVersionUID = 1L; + private String criteria; + private List supplierIds = new ArrayList(); + private long totalTimeoutMillis = 3000; + + public String getCriteria() { return criteria; } + public void setCriteria(String criteria) { this.criteria = criteria; } + public List getSupplierIds() { return Collections.unmodifiableList(supplierIds); } + public void setSupplierIds(List supplierIds) { + this.supplierIds = supplierIds == null ? new ArrayList() : new ArrayList(supplierIds); + } + public long getTotalTimeoutMillis() { return totalTimeoutMillis; } + public void setTotalTimeoutMillis(long totalTimeoutMillis) { this.totalTimeoutMillis = totalTimeoutMillis; } +} diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java b/architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java new file mode 100644 index 0000000..f8b3ecd --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java @@ -0,0 +1,22 @@ +package com.arch.policy.api; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class PolicySearchResponse implements Serializable { + private static final long serialVersionUID = 1L; + private final String searchKey; + private final String state; + private final List results; + + public PolicySearchResponse(String searchKey, String state, List results) { + this.searchKey = searchKey; + this.state = state; + this.results = Collections.unmodifiableList(new ArrayList(results)); + } + public String getSearchKey() { return searchKey; } + public String getState() { return state; } + public List getResults() { return results; } +} diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java b/architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java new file mode 100644 index 0000000..5ab9e75 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java @@ -0,0 +1,7 @@ +package com.arch.policy.api; + +import java.util.concurrent.CompletableFuture; + +public interface PolicySearchRpcService { + CompletableFuture asyncSearch(PolicySearchRequest request); +} diff --git a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java b/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java new file mode 100644 index 0000000..f965f51 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java @@ -0,0 +1,25 @@ +package com.arch.policy.api; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class SupplierCallbackRequest implements Serializable { + private static final long serialVersionUID = 1L; + private String searchKey; + private String supplierId; + private List results = new ArrayList(); + private boolean searchFinished; + + public String getSearchKey() { return searchKey; } + public void setSearchKey(String searchKey) { this.searchKey = searchKey; } + public String getSupplierId() { return supplierId; } + public void setSupplierId(String supplierId) { this.supplierId = supplierId; } + public List getResults() { return Collections.unmodifiableList(results); } + public void setResults(List results) { + this.results = results == null ? new ArrayList() : new ArrayList(results); + } + public boolean isSearchFinished() { return searchFinished; } + public void setSearchFinished(boolean searchFinished) { this.searchFinished = searchFinished; } +} diff --git a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java b/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java new file mode 100644 index 0000000..1cdff44 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java @@ -0,0 +1,5 @@ +package com.arch.policy.api; + +public interface SupplierCallbackRpcService { + CallbackResponse callback(SupplierCallbackRequest request); +} diff --git a/architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java b/architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java new file mode 100644 index 0000000..d7d1d35 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java @@ -0,0 +1,16 @@ +package com.arch.policy.application; + +import com.arch.policy.ActiveSnapshotRegistry; +import com.arch.policy.PolicyChange; + +public final class PolicyIncrementalUpdater { + private final ActiveSnapshotRegistry registry; + + public PolicyIncrementalUpdater(ActiveSnapshotRegistry registry) { this.registry = registry; } + + public boolean apply(PolicyChange change) throws Exception { + try (ActiveSnapshotRegistry.SnapshotLease lease = registry.acquire()) { + return lease.snapshot().apply(change); + } + } +} diff --git a/architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java b/architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java new file mode 100644 index 0000000..58bbf5f --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java @@ -0,0 +1,19 @@ +package com.arch.policy.application; + +import com.arch.policy.PolicySnapshotService; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; + +public final class PolicyStartupRunner implements ApplicationRunner { + private final PolicySnapshotService snapshotService; + private final String initialVersion; + + public PolicyStartupRunner(PolicySnapshotService snapshotService, String initialVersion) { + this.snapshotService = snapshotService; + this.initialVersion = initialVersion; + } + + @Override public void run(ApplicationArguments args) throws Exception { + snapshotService.initializeBlocking(initialVersion); + } +} diff --git a/architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java b/architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java new file mode 100644 index 0000000..03f770c --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java @@ -0,0 +1,109 @@ +package com.arch.policy.config; + +import com.arch.policy.ActiveSnapshotRegistry; +import com.arch.policy.DefaultSnapshotValidator; +import com.arch.policy.FileSystemSnapshotDirectory; +import com.arch.policy.PolicySnapshotService; +import com.arch.policy.SnapshotBuilder; +import com.arch.policy.SnapshotPorts.FullPolicyLoader; +import com.arch.policy.SnapshotPorts.IncrementalReplayer; +import com.arch.policy.application.PolicyIncrementalUpdater; +import com.arch.policy.application.PolicyStartupRunner; +import com.arch.policy.aggregation.AsyncSearchCoordinator; +import com.arch.policy.aggregation.LocalSearchWaiters; +import com.arch.policy.aggregation.SearchStateStore; +import com.arch.policy.aggregation.SupplierCallbackService; +import com.arch.policy.aggregation.SupplierTaskDispatcher; +import com.arch.policy.demo.DemoSupplierTaskDispatcher; +import com.arch.policy.kafka.KafkaPolicyChangeListener; +import com.arch.policy.redis.RedisSearchStateStore; +import com.arch.policy.redis.SearchFinishedSubscriber; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.nio.file.Paths; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +@Configuration +public class PolicySearchConfiguration { + @Bean public ActiveSnapshotRegistry activeSnapshotRegistry() { return new ActiveSnapshotRegistry(); } + + @Bean public PolicyIncrementalUpdater policyIncrementalUpdater(ActiveSnapshotRegistry registry) { + return new PolicyIncrementalUpdater(registry); + } + + @Bean(destroyMethod = "shutdown") public Executor policySearchExecutor() { + return Executors.newFixedThreadPool(Math.max(2, Runtime.getRuntime().availableProcessors())); + } + + @Bean public ObjectMapper objectMapper() { return new ObjectMapper(); } + + @Bean public SearchStateStore searchStateStore(StringRedisTemplate redis) { + return new RedisSearchStateStore(redis); + } + + @Bean public LocalSearchWaiters localSearchWaiters() { return new LocalSearchWaiters(); } + + @Bean public SupplierCallbackService supplierCallbackService( + SearchStateStore store, ObjectMapper mapper, + @Value("${policy.search.redis-ttl-seconds:60}") long ttlSeconds) { + return new SupplierCallbackService(store, mapper, ttlSeconds); + } + + @Bean(destroyMethod = "shutdown") public ScheduledExecutorService demoSupplierExecutor() { + return Executors.newScheduledThreadPool(4); + } + + @Bean public SupplierTaskDispatcher supplierTaskDispatcher( + ScheduledExecutorService demoSupplierExecutor, SupplierCallbackService callbackService) { + return new DemoSupplierTaskDispatcher(demoSupplierExecutor, callbackService); + } + + @Bean public AsyncSearchCoordinator asyncSearchCoordinator( + SearchStateStore store, SupplierTaskDispatcher dispatcher, LocalSearchWaiters waiters, + ObjectMapper mapper, @Value("${policy.search.redis-ttl-seconds:60}") long ttlSeconds) { + return new AsyncSearchCoordinator(store, dispatcher, waiters, mapper, ttlSeconds); + } + + @Bean public RedisMessageListenerContainer searchFinishedListener( + RedisConnectionFactory connectionFactory, LocalSearchWaiters waiters) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.addMessageListener(new SearchFinishedSubscriber(waiters), + new ChannelTopic(RedisSearchStateStore.FINISHED_CHANNEL)); + return container; + } + + @Bean public KafkaPolicyChangeListener kafkaPolicyChangeListener(ObjectMapper mapper, + PolicyIncrementalUpdater updater) { + return new KafkaPolicyChangeListener(mapper, updater); + } + + @Bean public SnapshotBuilder snapshotBuilder(FullPolicyLoader fullLoader, + IncrementalReplayer replayer, + @Value("${policy.snapshot.directory:./data/policy-snapshots}") String directory) { + return new SnapshotBuilder(fullLoader, replayer, new DefaultSnapshotValidator(), + new FileSystemSnapshotDirectory(Paths.get(directory))); + } + + @Bean(destroyMethod = "close") public PolicySnapshotService policySnapshotService( + SnapshotBuilder builder, ActiveSnapshotRegistry registry, + @Value("${policy.snapshot.retry-delay-ms:5000}") long retryDelayMillis) { + return new PolicySnapshotService(builder, registry, + Executors.newSingleThreadScheduledExecutor(), retryDelayMillis); + } + + @Bean public PolicyStartupRunner policyStartupRunner( + PolicySnapshotService service, + @Value("${policy.snapshot.initial-version:startup}") String initialVersion) { + return new PolicyStartupRunner(service, initialVersion); + } +} diff --git a/architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java b/architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java new file mode 100644 index 0000000..ecb3714 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java @@ -0,0 +1,47 @@ +package com.arch.policy.demo; + +import com.arch.policy.aggregation.SupplierCallbackService; +import com.arch.policy.aggregation.SupplierTaskDispatcher; +import com.arch.policy.api.PolicySearchRequest; +import com.arch.policy.api.SupplierCallbackRequest; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** Simulates downstream suppliers asynchronously calling this module back. */ +public final class DemoSupplierTaskDispatcher implements SupplierTaskDispatcher { + private final ScheduledExecutorService executor; + private final SupplierCallbackService callbackService; + + public DemoSupplierTaskDispatcher(ScheduledExecutorService executor, + SupplierCallbackService callbackService) { + this.executor = executor; + this.callbackService = callbackService; + } + + @Override public void dispatch(final String searchKey, Set supplierIds, + final PolicySearchRequest request) { + int delay = 20; + for (final String supplierId : supplierIds) { + executor.schedule(new Runnable() { + @Override public void run() { + try { + SupplierCallbackRequest callback = new SupplierCallbackRequest(); + callback.setSearchKey(searchKey); + callback.setSupplierId(supplierId); + callback.setResults(Collections.singletonList( + supplierId + " quote for " + request.getCriteria())); + callback.setSearchFinished(true); + callbackService.callback(callback); + } catch (Exception failure) { + try { callbackService.markFinishedWithoutResult(searchKey, supplierId); } + catch (Exception ignored) { /* broker retry/alert belongs in a real adapter */ } + } + } + }, delay, TimeUnit.MILLISECONDS); + delay += 20; + } + } +} diff --git a/architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java b/architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java new file mode 100644 index 0000000..595318c --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java @@ -0,0 +1,45 @@ +package com.arch.policy.kafka; + +import com.arch.policy.MessagePosition; +import com.arch.policy.PolicyChange; +import com.arch.policy.PolicyRecord; +import com.arch.policy.application.PolicyIncrementalUpdater; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.kafka.annotation.KafkaListener; + +import java.nio.charset.StandardCharsets; +import java.util.HashSet; + +public final class KafkaPolicyChangeListener { + private final ObjectMapper objectMapper; + private final PolicyIncrementalUpdater updater; + + public KafkaPolicyChangeListener(ObjectMapper objectMapper, PolicyIncrementalUpdater updater) { + this.objectMapper = objectMapper; + this.updater = updater; + } + + @KafkaListener(topics = "${policy.kafka.topic:policy-change}", + groupId = "${policy.kafka.group-id:policy-search}") + public void onMessage(String json) throws Exception { + PolicyChangeMessage message = objectMapper.readValue(json, PolicyChangeMessage.class); + updater.apply(toChange(message)); + } + + private PolicyChange toChange(PolicyChangeMessage message) { + MessagePosition position = new MessagePosition(message.getPosition()); + if ("DELETE".equalsIgnoreCase(message.getOperation())) { + return PolicyChange.delete(message.getPolicyId(), position); + } + if (!"UPSERT".equalsIgnoreCase(message.getOperation())) { + throw new IllegalArgumentException("unsupported policy operation: " + message.getOperation()); + } + if (message.getDetail() == null || message.getIndexTerms() == null) { + throw new IllegalArgumentException("UPSERT requires detail and indexTerms"); + } + PolicyRecord policy = new PolicyRecord(message.getPolicyId(), + message.getDetail().getBytes(StandardCharsets.UTF_8), + new HashSet(message.getIndexTerms())); + return PolicyChange.upsert(policy, position); + } +} diff --git a/architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java b/architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java new file mode 100644 index 0000000..3f539ec --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java @@ -0,0 +1,23 @@ +package com.arch.policy.kafka; + +import java.util.ArrayList; +import java.util.List; + +public final class PolicyChangeMessage { + private String operation; + private int policyId; + private String detail; + private List indexTerms = new ArrayList(); + private long position; + + public String getOperation() { return operation; } + public void setOperation(String operation) { this.operation = operation; } + public int getPolicyId() { return policyId; } + public void setPolicyId(int policyId) { this.policyId = policyId; } + public String getDetail() { return detail; } + public void setDetail(String detail) { this.detail = detail; } + public List getIndexTerms() { return indexTerms; } + public void setIndexTerms(List indexTerms) { this.indexTerms = indexTerms; } + public long getPosition() { return position; } + public void setPosition(long position) { this.position = position; } +} diff --git a/architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java b/architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java new file mode 100644 index 0000000..49547f0 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java @@ -0,0 +1,84 @@ +package com.arch.policy.redis; + +import com.arch.policy.aggregation.SearchState; +import com.arch.policy.aggregation.SearchStateStore; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +public final class RedisSearchStateStore implements SearchStateStore { + public static final String FINISHED_CHANNEL = "search-finished"; + private static final String EMPTY_RESULT_MARKER = "__SEARCH_INITIALIZED__"; + private static final DefaultRedisScript INIT = script( + "redis.call('DEL', KEYS[1], KEYS[2], KEYS[3]); redis.call('RPUSH', KEYS[1], ARGV[2]); " + + "redis.call('EXPIRE', KEYS[1], ARGV[1]); " + + "for i=3,#ARGV do redis.call('SADD', KEYS[2], ARGV[i]); end; " + + "if redis.call('SCARD', KEYS[2]) == 0 then " + + " redis.call('SET', KEYS[3], 'COMPLETED', 'EX', ARGV[1]); return 1; " + + "end; redis.call('SET', KEYS[3], 'WAITING', 'EX', ARGV[1]); " + + "redis.call('EXPIRE', KEYS[1], ARGV[1]); redis.call('EXPIRE', KEYS[2], ARGV[1]); return 0;"); + private static final DefaultRedisScript CALLBACK = script( + "if redis.call('GET', KEYS[3]) ~= 'WAITING' then return 0; end; " + + "if redis.call('SISMEMBER', KEYS[2], ARGV[2]) == 0 then return 0; end; " + + "if ARGV[1] ~= '' then redis.call('RPUSH', KEYS[1], ARGV[1]); end; " + + "local removed=0; if ARGV[3] == 'true' then removed=redis.call('SREM', KEYS[2], ARGV[2]); end; " + + "redis.call('EXPIRE', KEYS[1], ARGV[5]); redis.call('EXPIRE', KEYS[2], ARGV[5]); " + + "redis.call('EXPIRE', KEYS[3], ARGV[5]); " + + "if removed > 0 and redis.call('SCARD', KEYS[2]) == 0 then " + + " redis.call('SET', KEYS[3], 'COMPLETED', 'EX', ARGV[5]); " + + " redis.call('PUBLISH', ARGV[6], ARGV[4]); return 2; end; return 1;"); + private static final DefaultRedisScript TIMEOUT = script( + "if redis.call('GET', KEYS[3]) == 'WAITING' then " + + " redis.call('SET', KEYS[3], 'TIMED_OUT', 'EX', ARGV[1]); " + + " redis.call('EXPIRE', KEYS[1], ARGV[1]); redis.call('EXPIRE', KEYS[2], ARGV[1]); return 1; end; return 0;"); + + private final StringRedisTemplate redis; + + public RedisSearchStateStore(StringRedisTemplate redis) { this.redis = redis; } + + @Override public void initialize(String searchKey, Set supplierIds, long ttlSeconds) { + List args = new ArrayList(); + args.add(Long.toString(ttlSeconds)); + args.add(EMPTY_RESULT_MARKER); + args.addAll(supplierIds); + redis.execute(INIT, keys(searchKey), args.toArray()); + } + + @Override public SearchState getState(String searchKey) { + String state = redis.opsForValue().get(stateKey(searchKey)); + return state == null ? SearchState.TIMED_OUT : SearchState.valueOf(state); + } + + @Override public List getResultPayloads(String searchKey) { + List values = redis.opsForList().range(resultKey(searchKey), 0, -1); + if (values == null) return Collections.emptyList(); + List results = new ArrayList(values); + results.remove(EMPTY_RESULT_MARKER); + return results; + } + + @Override public void recordCallback(String searchKey, String supplierId, String payload, + boolean finished, long ttlSeconds) { + redis.execute(CALLBACK, keys(searchKey), payload, supplierId, Boolean.toString(finished), + searchKey, Long.toString(ttlSeconds), FINISHED_CHANNEL); + } + + @Override public void markTimedOut(String searchKey, long ttlSeconds) { + redis.execute(TIMEOUT, keys(searchKey), Long.toString(ttlSeconds)); + } + + private static List keys(String searchKey) { + return Arrays.asList(resultKey(searchKey), pendingKey(searchKey), stateKey(searchKey)); + } + private static String resultKey(String key) { return "search:" + key + ":results"; } + private static String pendingKey(String key) { return "search:" + key + ":pending"; } + private static String stateKey(String key) { return "search:" + key + ":state"; } + private static DefaultRedisScript script(String source) { + return new DefaultRedisScript(source, Long.class); + } +} diff --git a/architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java b/architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java new file mode 100644 index 0000000..f8ea514 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java @@ -0,0 +1,15 @@ +package com.arch.policy.redis; + +import com.arch.policy.aggregation.LocalSearchWaiters; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; + +import java.nio.charset.StandardCharsets; + +public final class SearchFinishedSubscriber implements MessageListener { + private final LocalSearchWaiters waiters; + public SearchFinishedSubscriber(LocalSearchWaiters waiters) { this.waiters = waiters; } + @Override public void onMessage(Message message, byte[] pattern) { + waiters.signal(new String(message.getBody(), StandardCharsets.UTF_8)); + } +} diff --git a/architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java b/architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java new file mode 100644 index 0000000..ef51aa6 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java @@ -0,0 +1,34 @@ +package com.arch.policy.rpc; + +import com.arch.policy.api.PolicySearchRequest; +import com.arch.policy.api.PolicySearchResponse; +import com.arch.policy.api.PolicySearchRpcService; +import com.arch.policy.aggregation.AsyncSearchCoordinator; +import org.apache.dubbo.config.annotation.DubboService; +import org.springframework.beans.factory.annotation.Qualifier; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +@DubboService(version = "1.0.0", timeout = 3000) +public final class DubboPolicySearchService implements PolicySearchRpcService { + private final AsyncSearchCoordinator searchService; + private final Executor searchExecutor; + + public DubboPolicySearchService(AsyncSearchCoordinator searchService, + @Qualifier("policySearchExecutor") Executor searchExecutor) { + this.searchService = searchService; + this.searchExecutor = searchExecutor; + } + + @Override public CompletableFuture asyncSearch(final PolicySearchRequest request) { + final CompletableFuture future = new CompletableFuture(); + searchExecutor.execute(new Runnable() { + @Override public void run() { + try { future.complete(searchService.search(request)); } + catch (Exception failure) { future.completeExceptionally(failure); } + } + }); + return future; + } +} diff --git a/architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java b/architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java new file mode 100644 index 0000000..f685dc1 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java @@ -0,0 +1,23 @@ +package com.arch.policy.rpc; + +import com.arch.policy.aggregation.SupplierCallbackService; +import com.arch.policy.api.CallbackResponse; +import com.arch.policy.api.SupplierCallbackRequest; +import com.arch.policy.api.SupplierCallbackRpcService; +import org.apache.dubbo.config.annotation.DubboService; + +@DubboService(version = "1.0.0", timeout = 3000) +public final class DubboSupplierCallbackService implements SupplierCallbackRpcService { + private final SupplierCallbackService callbackService; + public DubboSupplierCallbackService(SupplierCallbackService callbackService) { + this.callbackService = callbackService; + } + @Override public CallbackResponse callback(SupplierCallbackRequest request) { + try { + callbackService.callback(request); + return new CallbackResponse(true); + } catch (Exception failure) { + throw new IllegalStateException("supplier callback failed", failure); + } + } +} diff --git a/architecture/src/main/resources/application.yml b/architecture/src/main/resources/application.yml new file mode 100644 index 0000000..c30aadb --- /dev/null +++ b/architecture/src/main/resources/application.yml @@ -0,0 +1,32 @@ +spring: + application: + name: policy-search + kafka: + consumer: + enable-auto-commit: false + auto-offset-reset: earliest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer + listener: + ack-mode: record + redis: + host: localhost + port: 6379 + +dubbo: + application: + name: policy-search + protocol: + name: dubbo + port: 20880 + +policy: + search: + redis-ttl-seconds: 60 + snapshot: + directory: ./data/policy-snapshots + initial-version: startup + retry-delay-ms: 5000 + kafka: + topic: policy-change + group-id: policy-search diff --git a/architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java b/architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java new file mode 100644 index 0000000..fe28969 --- /dev/null +++ b/architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java @@ -0,0 +1,99 @@ +package com.arch.policy; + +import com.arch.policy.aggregation.*; +import com.arch.policy.api.PolicySearchRequest; +import com.arch.policy.api.PolicySearchResponse; +import com.arch.policy.api.SupplierCallbackRequest; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AsyncSearchCoordinatorTest { + @Test + void waitsForAllSuppliersAndAggregatesAsyncCallbacks() throws Exception { + final MemoryStore store = new MemoryStore(); + final LocalSearchWaiters waiters = new LocalSearchWaiters(); + final ObjectMapper mapper = new ObjectMapper(); + SupplierTaskDispatcher dispatcher = (searchKey, suppliers, request) -> { + new Thread(() -> { + try { + for (String supplier : suppliers) { + SupplierCallbackRequest callback = callback(searchKey, supplier, supplier + "-quote", true); + store.recordCallback(searchKey, supplier, mapper.writeValueAsString(callback), true, 60); + } + waiters.signal(searchKey); + } catch (Exception failure) { throw new RuntimeException(failure); } + }).start(); + }; + AsyncSearchCoordinator coordinator = new AsyncSearchCoordinator(store, dispatcher, waiters, mapper, 60); + PolicySearchRequest request = request(1000, "A", "B"); + + PolicySearchResponse response = coordinator.search(request); + + assertEquals(SearchState.COMPLETED.name(), response.getState()); + assertEquals(2, response.getResults().size()); + } + + @Test + void timeoutReturnsPartialResultsAndRejectsLateCallbacks() throws Exception { + final MemoryStore store = new MemoryStore(); + final ObjectMapper mapper = new ObjectMapper(); + SupplierTaskDispatcher dispatcher = (searchKey, suppliers, request) -> { + try { + SupplierCallbackRequest partial = callback(searchKey, "A", "A-quote", true); + store.recordCallback(searchKey, "A", mapper.writeValueAsString(partial), true, 60); + } catch (Exception failure) { throw new RuntimeException(failure); } + }; + AsyncSearchCoordinator coordinator = new AsyncSearchCoordinator( + store, dispatcher, new LocalSearchWaiters(), mapper, 60); + + PolicySearchResponse response = coordinator.search(request(30, "A", "B")); + + assertEquals(SearchState.TIMED_OUT.name(), response.getState()); + assertEquals(1, response.getResults().size()); + } + + private static PolicySearchRequest request(long timeout, String... suppliers) { + PolicySearchRequest request = new PolicySearchRequest(); + request.setCriteria("demo"); + request.setSupplierIds(Arrays.asList(suppliers)); + request.setTotalTimeoutMillis(timeout); + return request; + } + + private static SupplierCallbackRequest callback(String key, String supplier, String result, boolean done) { + SupplierCallbackRequest request = new SupplierCallbackRequest(); + request.setSearchKey(key); + request.setSupplierId(supplier); + request.setResults(Collections.singletonList(result)); + request.setSearchFinished(done); + return request; + } + + private static final class MemoryStore implements SearchStateStore { + private SearchState state; + private final Set pending = new HashSet(); + private final List results = new ArrayList(); + @Override public synchronized void initialize(String key, Set suppliers, long ttl) { + pending.addAll(suppliers); + state = suppliers.isEmpty() ? SearchState.COMPLETED : SearchState.WAITING; + } + @Override public synchronized SearchState getState(String key) { return state; } + @Override public synchronized List getResultPayloads(String key) { + return new ArrayList(results); + } + @Override public synchronized void recordCallback(String key, String supplier, String payload, + boolean finished, long ttl) { + if (state != SearchState.WAITING) return; + if (!payload.isEmpty()) results.add(payload); + if (finished) pending.remove(supplier); + if (pending.isEmpty()) state = SearchState.COMPLETED; + } + @Override public synchronized void markTimedOut(String key, long ttl) { + if (state == SearchState.WAITING) state = SearchState.TIMED_OUT; + } + } +} diff --git a/architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java b/architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java new file mode 100644 index 0000000..297a7c0 --- /dev/null +++ b/architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java @@ -0,0 +1,119 @@ +package com.arch.policy; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import static com.arch.policy.SnapshotPorts.*; +import static org.junit.jupiter.api.Assertions.*; + +class SnapshotLifecycleTest { + @TempDir Path root; + + @Test + void buildsFullDataReplaysChangesAndValidatesPosition() throws Exception { + SnapshotBuilder builder = builder(false); + + PolicySnapshot snapshot = builder.build("V101"); + + assertEquals(new MessagePosition(12), snapshot.getPosition()); + assertArrayEquals(bytes("new-detail"), snapshot.findDetail(1)); + assertEquals(1, snapshot.findPolicyIds("active").getCardinality()); + assertEquals(0, snapshot.findPolicyIds("draft").getCardinality()); + snapshot.closeAndDelete(); + } + + @Test + void switchWaitsForOldQueriesBeforeClosingAndDeletingOldVersion() throws Exception { + ActiveSnapshotRegistry registry = new ActiveSnapshotRegistry(); + PolicySnapshot old = builder(false).build("V100"); + PolicySnapshot replacement = builder(false).build("V101"); + registry.activate(old); + ActiveSnapshotRegistry.SnapshotLease oldQuery = registry.acquire(); + + registry.activate(replacement); + + assertTrue(Files.exists(old.getDirectory())); + assertArrayEquals(bytes("new-detail"), oldQuery.snapshot().findDetail(1)); + try (ActiveSnapshotRegistry.SnapshotLease newQuery = registry.acquire()) { + assertEquals("V101", newQuery.snapshot().getVersion()); + } + oldQuery.close(); + assertFalse(Files.exists(old.getDirectory())); + assertThrows(IllegalStateException.class, () -> old.findPolicyIds("active")); + registry.close(); + } + + @Test + void failedRuntimeCandidateDoesNotReplaceServingSnapshot() throws Exception { + ActiveSnapshotRegistry registry = new ActiveSnapshotRegistry(); + registry.activate(builder(false).build("V100")); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + PolicySnapshotService service = new PolicySnapshotService(builder(true), registry, executor, 1); + + assertTrue(service.refresh("V101")); + waitUntilBuildFinishes(service); + + assertTrue(service.isReady()); + try (ActiveSnapshotRegistry.SnapshotLease query = registry.acquire()) { + assertEquals("V100", query.snapshot().getVersion()); + } + assertFalse(Files.exists(root.resolve("V101"))); + service.close(); + } + + private SnapshotBuilder builder(final boolean failValidation) { + FullPolicyLoader full = target -> { + target.upsert(new PolicyRecord(1, bytes("old-detail"), Collections.singleton("draft"))); + return new MessagePosition(10); + }; + IncrementalReplayer replay = new IncrementalReplayer() { + @Override public MessagePosition latestPosition() { return new MessagePosition(12); } + @Override public MessagePosition replayInto(PolicySnapshot target, MessagePosition from, + MessagePosition latest) throws Exception { + target.upsert(new PolicyRecord(1, bytes("new-detail"), + Collections.singleton("active"))); + return latest; + } + }; + SnapshotValidator validator = (candidate, position) -> { + new DefaultSnapshotValidator().validate(candidate, position); + if (failValidation) throw new IllegalStateException("invalid candidate"); + }; + SnapshotDirectory directories = new SnapshotDirectory() { + @Override public Path create(String version) throws Exception { + Path path = root.resolve(version); + Files.createDirectories(path); + return path; + } + @Override public void delete(Path directory) throws Exception { + if (!Files.exists(directory)) return; + Files.walk(directory).sorted(Comparator.reverseOrder()).forEach(path -> { + try { Files.deleteIfExists(path); } + catch (Exception failure) { throw new RuntimeException(failure); } + }); + } + }; + return new SnapshotBuilder(full, replay, validator, directories); + } + + private static byte[] bytes(String value) { + return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); + } + + private static void waitUntilBuildFinishes(PolicySnapshotService service) throws Exception { + long deadline = System.currentTimeMillis() + 3000; + while (System.currentTimeMillis() < deadline) { + if (!service.isBuilding()) return; + Thread.sleep(10); + } + fail("candidate build did not finish"); + } +} From 5deeb99325653b946b380e7d854a735b1a809965 Mon Sep 17 00:00:00 2001 From: coderbruis Date: Sun, 19 Jul 2026 21:12:39 +0800 Subject: [PATCH 17/21] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BC=AA=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=9E=B6=E6=9E=84=E5=BA=93=E5=88=86=E5=B1=82=E7=BB=93?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- architecture/README.md | 214 ++++++++++++++---- .../api/SupplierCallbackRpcService.java | 5 - .../{ => search}/PolicySearchRpcService.java | 5 +- .../search/SupplierCallbackRpcService.java | 8 + .../config/PolicySearchConfiguration.java | 40 ++-- .../{ => common/model}/MessagePosition.java | 2 +- .../{ => common/model}/PolicyChange.java | 2 +- .../{ => common/model}/PolicyRecord.java | 2 +- .../search}/CallbackResponse.java | 2 +- .../search}/PolicySearchRequest.java | 2 +- .../search}/PolicySearchResponse.java | 2 +- .../search}/SupplierCallbackRequest.java | 2 +- .../application}/AsyncSearchCoordinator.java | 8 +- .../application}/LocalSearchWaiters.java | 2 +- .../application/PolicyIncrementalUpdater.java | 6 +- .../application/PolicyStartupRunner.java | 4 +- .../application}/SearchState.java | 2 +- .../application}/SearchStateStore.java | 2 +- .../application}/SupplierCallbackService.java | 4 +- .../application}/SupplierTaskDispatcher.java | 4 +- .../snapshot}/ActiveSnapshotRegistry.java | 2 +- .../snapshot}/DefaultSnapshotValidator.java | 6 +- .../FileSystemSnapshotDirectory.java | 4 +- .../domain/snapshot}/PolicySnapshot.java | 6 +- .../snapshot}/PolicySnapshotService.java | 2 +- .../domain/snapshot}/SnapshotBuilder.java | 6 +- .../domain/snapshot}/SnapshotPorts.java | 4 +- .../demo/DemoSupplierTaskDispatcher.java | 10 +- .../kafka/KafkaPolicyChangeListener.java | 10 +- .../kafka/PolicyChangeMessage.java | 2 +- .../redis/RedisSearchStateStore.java | 6 +- .../redis/SearchFinishedSubscriber.java | 4 +- .../rpc/DubboPolicySearchService.java | 10 +- .../rpc/DubboSupplierCallbackService.java | 10 +- .../AsyncSearchCoordinatorTest.java | 9 +- .../snapshot}/SnapshotLifecycleTest.java | 6 +- 36 files changed, 277 insertions(+), 138 deletions(-) delete mode 100644 architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java rename architecture/src/main/java/com/arch/policy/api/{ => search}/PolicySearchRpcService.java (53%) create mode 100644 architecture/src/main/java/com/arch/policy/api/search/SupplierCallbackRpcService.java rename architecture/src/main/java/com/arch/policy/{ => common}/config/PolicySearchConfiguration.java (77%) rename architecture/src/main/java/com/arch/policy/{ => common/model}/MessagePosition.java (95%) rename architecture/src/main/java/com/arch/policy/{ => common/model}/PolicyChange.java (96%) rename architecture/src/main/java/com/arch/policy/{ => common/model}/PolicyRecord.java (95%) rename architecture/src/main/java/com/arch/policy/{api => common/search}/CallbackResponse.java (88%) rename architecture/src/main/java/com/arch/policy/{api => common/search}/PolicySearchRequest.java (96%) rename architecture/src/main/java/com/arch/policy/{api => common/search}/PolicySearchResponse.java (95%) rename architecture/src/main/java/com/arch/policy/{api => common/search}/SupplierCallbackRequest.java (96%) rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/AsyncSearchCoordinator.java (93%) rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/LocalSearchWaiters.java (96%) rename architecture/src/main/java/com/arch/policy/{ => search}/application/PolicyIncrementalUpdater.java (71%) rename architecture/src/main/java/com/arch/policy/{ => search}/application/PolicyStartupRunner.java (84%) rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/SearchState.java (57%) rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/SearchStateStore.java (91%) rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/SupplierCallbackService.java (93%) rename architecture/src/main/java/com/arch/policy/{aggregation => search/application}/SupplierTaskDispatcher.java (61%) rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/ActiveSnapshotRegistry.java (97%) rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/DefaultSnapshotValidator.java (76%) rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/FileSystemSnapshotDirectory.java (91%) rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/PolicySnapshot.java (96%) rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/PolicySnapshotService.java (98%) rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/SnapshotBuilder.java (92%) rename architecture/src/main/java/com/arch/policy/{ => search/domain/snapshot}/SnapshotPorts.java (90%) rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/demo/DemoSupplierTaskDispatcher.java (86%) rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/kafka/KafkaPolicyChangeListener.java (86%) rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/kafka/PolicyChangeMessage.java (94%) rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/redis/RedisSearchStateStore.java (96%) rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/redis/SearchFinishedSubscriber.java (82%) rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/rpc/DubboPolicySearchService.java (80%) rename architecture/src/main/java/com/arch/policy/{ => search/infrastructure}/rpc/DubboSupplierCallbackService.java (70%) rename architecture/src/test/java/com/arch/policy/{ => search/application}/AsyncSearchCoordinatorTest.java (95%) rename architecture/src/test/java/com/arch/policy/{ => search/domain/snapshot}/SnapshotLifecycleTest.java (95%) diff --git a/architecture/README.md b/architecture/README.md index ed7e14c..58cde70 100644 --- a/architecture/README.md +++ b/architecture/README.md @@ -1,46 +1,168 @@ -# Policy snapshot lifecycle - -This module implements version-isolated policy snapshots. Each snapshot owns a RocksDB detail -store, a bitmap index, and the incremental-message position at which both were built. - -## Integration flow - -1. Implement `FullPolicyLoader` to stream the full-data cut into `PolicySnapshot.upsert` and - return the cut's message position. -2. Implement `IncrementalReplayer` to apply upserts/deletes after that position. The builder - repeats replay until it reaches a stable latest position. -3. Supply `DefaultSnapshotValidator` (or a stricter domain validator), a - `FileSystemSnapshotDirectory`, and construct `SnapshotBuilder`. -4. Call `PolicySnapshotService.start(version)` during startup. Keep the application's readiness - probe bound to `service.isReady()`; failures remain unready and retry. -5. Call `refresh(newVersion)` at runtime. A failed candidate is discarded. A valid candidate is - atomically activated while the old snapshot continues serving existing leases. -6. Every search must use `try (SnapshotLease lease = registry.acquire())`. Releasing the last old - lease closes RocksDB and deletes that retired version's directory. - -The module intentionally leaves message-broker and full-data-source clients behind interfaces so -the snapshot consistency rules are independent of Kafka, HTTP, database, or framework choices. - -## Package layout - -- `api`: serializable Dubbo contract and request/response DTOs. -- `rpc`: Dubbo search provider and supplier callback provider. -- `aggregation`: Redis-backed fan-out/fan-in search coordination and local waiters. -- `redis`: atomic Lua state transitions and Pub/Sub early wake-up. -- `demo`: asynchronous downstream supplier simulation. -- `kafka`: JSON policy-change consumer and message DTO. -- `application`: startup and incremental-update use cases. -- root `policy` package: versioned RocksDB/Bitmap snapshot domain and lifecycle. - -Kafka messages use a globally monotonic `position` so duplicate/out-of-order delivery is ignored. -If the topic has multiple partitions, the producer must supply this global sequence; otherwise the -position model should be replaced with a per-partition offset map. - -## Async supplier search - -`asyncSearch` initializes pending suppliers, state and result TTL atomically in Redis, registers a -local waiter, double-checks Redis, then dispatches all supplier tasks. Supplier callbacks append -result chunks and remove a supplier from the pending set only on its final callback. The Lua script -sets `COMPLETED` and publishes `search-finished` when the last supplier finishes. Pub/Sub only wakes -the local waiter early; Redis remains the source of truth and is checked every 200 ms. Timeout is -also a Lua state transition and returns all partial results already recorded. +# 架构方案伪代码库 + +`architecture` module 用于沉淀不同业务场景下的架构思路、设计方案和关键伪代码。它关注的是 +复杂问题如何拆分、核心流程如何协作、数据一致性如何保证,以及不同技术组件之间的边界,而不是提供 +可以直接上线的完整业务系统。 + +当前实现以机票业务为背景,模拟以下两个相互关联的架构场景: + +1. **多供应商并发搜索**:一次机票查询并发分发给多个供应商,通过异步回调聚合结果,并支持超时返回部分结果。 +2. **海量政策高性能匹配**:使用 RocksDB 保存政策明细、RoaringBitmap 构建匹配索引,并通过版本化快照实现运行时无损切换。 + +后续会继续增加交易、下单等其他场景的架构伪代码,使该 module 逐步形成可复用的架构与设计方案集合。 + +## 模块目标 + +- 用最少的代码表达架构中的核心职责、协作关系和一致性约束。 +- 为类似业务问题提供可讨论、可验证、可演进的设计参考。 +- 通过接口隔离外部系统,使方案不被 Dubbo、Kafka、Redis 等具体技术绑定。 +- 通过测试验证关键架构行为,而不是覆盖完整的生产业务流程。 +- 持续积累搜索、政策匹配、交易、下单等不同场景的方案。 + +本模块属于架构伪代码,生产落地时仍需根据实际情况补充鉴权、限流、熔断、监控、链路追踪、异常分级、 +数据迁移、容量评估、容灾和部署方案。 + +## 场景目录 + +| 场景 | 核心问题 | 当前状态 | +| --- | --- | --- | +| 多供应商并发搜索 | 任务分发、异步回调、结果聚合、超时与迟到回调 | 已实现伪代码 | +| 海量政策匹配 | 全量加载、增量追平、位图索引、快照校验与热切换 | 已实现伪代码 | +| 交易/下单 | 幂等、状态流转、库存确认、分布式一致性与补偿 | 待补充 | + +## 分层结构 + +代码先区分项目级公开契约、公共能力和具体业务场景,再在场景内部按照应用、领域和基础设施分层。 + +```text +com.arch.policy +├── api +│ └── search # 项目级搜索契约、请求和响应 DTO +├── common +│ ├── config # Spring Bean 装配 +│ └── model # 可跨场景复用的模型 +├── search +│ ├── application # 查询编排、回调处理、启动和增量更新用例 +│ ├── domain +│ │ └── snapshot # RocksDB/位图快照领域逻辑 +│ └── infrastructure +│ ├── demo # 模拟供应商 +│ ├── kafka # 政策变更消息适配器 +│ ├── redis # 聚合状态和完成通知适配器 +│ └── rpc # Dubbo 接口实现 +└── PolicySearchApplication # 当前搜索场景的启动入口 +``` + +依赖方向如下: + +```mermaid +flowchart LR + Caller["外部调用方"] --> API["api.search
项目级公开契约"] + Infrastructure["search.infrastructure
RPC / Redis / Kafka"] --> API + Infrastructure --> Application["search.application
流程编排"] + Application --> Domain["search.domain
领域规则"] + Application --> Common["common
公共配置与模型"] + Domain --> Common +``` + +公开 API 不依赖场景内部实现,领域层不依赖 Dubbo、Redis、Kafka 等外部技术。后续增加交易场景时, +可以平行新增 `api.order` 和 `order.application/domain/infrastructure`,避免交易代码与搜索代码混杂。 +只有真正跨场景稳定复用的模型或能力才应放入 `common`。 + +## 场景一:多供应商并发搜索 + +查询服务将一次请求分发给多个供应商。供应商可以分批回调结果,最后一次回调负责声明该供应商完成。 +Redis 保存聚合结果、待完成供应商集合和查询状态,是整个流程的唯一事实来源。 + +```mermaid +sequenceDiagram + autonumber + actor Caller as 调用方 + participant SearchAPI as PolicySearchRpcService + participant Coordinator as AsyncSearchCoordinator + participant Redis as RedisSearchStateStore + participant Dispatcher as SupplierTaskDispatcher + participant Supplier as 多个供应商 + participant CallbackAPI as SupplierCallbackRpcService + participant Callback as SupplierCallbackService + participant Waiter as LocalSearchWaiters + + Caller->>SearchAPI: asyncSearch(request) + SearchAPI->>Coordinator: 执行查询 + Coordinator->>Redis: 初始化状态、待完成集合和 TTL + Coordinator->>Waiter: 注册本地等待器 + Coordinator->>Dispatcher: 并发分发供应商任务 + Dispatcher-->>Supplier: 异步查询 + + loop 供应商分批返回结果 + Supplier->>CallbackAPI: callback(result, searchFinished) + CallbackAPI->>Callback: 处理回调 + Callback->>Redis: 追加结果并更新待完成集合 + end + + Redis-->>Waiter: 全部完成后发布通知 + Waiter-->>Coordinator: 提前唤醒 + Coordinator->>Redis: 读取最终状态和结果 + Coordinator-->>SearchAPI: PolicySearchResponse + SearchAPI-->>Caller: 完成异步调用 +``` + +本地等待器和 Redis Pub/Sub 只用于提前唤醒。即使通知丢失,查询线程仍会定期检查 Redis。超过总超时时间后, +Lua 脚本会原子地将查询状态更新为 `TIMED_OUT`,返回已经收到的部分结果,并拒绝迟到回调继续修改结果。 + +## 场景二:海量政策匹配与快照切换 + +每个政策快照使用独立目录保存 RocksDB 明细库,并在内存中维护 RoaringBitmap 匹配索引。新版本先在旁路 +完成全量加载、增量追平和一致性校验,只有通过校验后才会替换当前服务版本。 + +```mermaid +flowchart TD + Start["应用启动或运行时刷新"] --> Create["创建版本隔离目录"] + Create --> Full["加载全量政策"] + Full --> Position["记录全量数据切点"] + Position --> Replay["回放切点后的增量消息"] + Replay --> CaughtUp{"已追平最新位置?"} + CaughtUp -- 否 --> Replay + CaughtUp -- 是 --> Validate["校验明细、索引和消息位置"] + Validate --> Valid{"校验成功?"} + Valid -- 否 --> Discard["关闭并删除候选快照"] + Valid -- 是 --> Activate["原子激活新快照"] + Activate --> NewQuery["新查询使用新版本"] + Activate --> Drain["旧版本等待已有查询释放租约"] + Drain --> Delete["关闭并删除旧快照"] +``` + +查询通过 `ActiveSnapshotRegistry.acquire()` 获取快照租约,并使用 `try-with-resources` 释放。切换完成后, +新查询立即使用新快照,旧快照则保留到最后一个旧查询结束,从而避免正在执行的查询访问已关闭的 RocksDB。 + +快照方案的主要扩展点: + +1. `FullPolicyLoader`:加载全量政策,并返回全量数据对应的消息位置。 +2. `IncrementalReplayer`:回放全量切点之后的新增、修改和删除事件。 +3. `SnapshotValidator`:校验政策明细、位图索引和消息位置的一致性。 +4. `SnapshotDirectory`:隔离不同快照版本的存储目录。 +5. `PolicySnapshotService`:负责首次初始化、失败重试和运行时刷新。 + +Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序消息会被忽略。如果 Topic 使用多个分区, +生产端必须提供全局序列;否则应将当前位置模型调整为按分区保存 offset。 + +## 新增架构场景的约定 + +新增交易、下单或其他架构伪代码时,应遵循以下约定: + +1. 在 README 的场景目录中说明要解决的问题、关键约束和方案状态。 +2. 对外契约放在 `api.`,内部实现放在对应的 `` 业务包。 +3. 使用 `application/domain/infrastructure` 表达职责边界,不让领域规则依赖具体中间件。 +4. 优先提供展示关键协作关系的最小实现,避免把伪代码扩展成不完整的生产框架。 +5. 为幂等、一致性、并发、超时、切换和失败隔离等关键架构行为编写测试。 +6. 在场景文档中记录设计取舍、适用边界,以及生产落地仍需补充的能力。 + +## 运行验证 + +在仓库根目录执行: + +```bash +mvn -f architecture/pom.xml clean test +``` + +当前测试覆盖异步供应商聚合、超时返回部分结果、全量加增量快照构建、运行时快照切换以及失败候选版本隔离。 diff --git a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java b/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java deleted file mode 100644 index 1cdff44..0000000 --- a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRpcService.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.arch.policy.api; - -public interface SupplierCallbackRpcService { - CallbackResponse callback(SupplierCallbackRequest request); -} diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java b/architecture/src/main/java/com/arch/policy/api/search/PolicySearchRpcService.java similarity index 53% rename from architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java rename to architecture/src/main/java/com/arch/policy/api/search/PolicySearchRpcService.java index 5ab9e75..98533cf 100644 --- a/architecture/src/main/java/com/arch/policy/api/PolicySearchRpcService.java +++ b/architecture/src/main/java/com/arch/policy/api/search/PolicySearchRpcService.java @@ -1,4 +1,7 @@ -package com.arch.policy.api; +package com.arch.policy.api.search; + +import com.arch.policy.common.search.PolicySearchRequest; +import com.arch.policy.common.search.PolicySearchResponse; import java.util.concurrent.CompletableFuture; diff --git a/architecture/src/main/java/com/arch/policy/api/search/SupplierCallbackRpcService.java b/architecture/src/main/java/com/arch/policy/api/search/SupplierCallbackRpcService.java new file mode 100644 index 0000000..8995d25 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/api/search/SupplierCallbackRpcService.java @@ -0,0 +1,8 @@ +package com.arch.policy.api.search; + +import com.arch.policy.common.search.CallbackResponse; +import com.arch.policy.common.search.SupplierCallbackRequest; + +public interface SupplierCallbackRpcService { + CallbackResponse callback(SupplierCallbackRequest request); +} diff --git a/architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java b/architecture/src/main/java/com/arch/policy/common/config/PolicySearchConfiguration.java similarity index 77% rename from architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java rename to architecture/src/main/java/com/arch/policy/common/config/PolicySearchConfiguration.java index 03f770c..21191fb 100644 --- a/architecture/src/main/java/com/arch/policy/config/PolicySearchConfiguration.java +++ b/architecture/src/main/java/com/arch/policy/common/config/PolicySearchConfiguration.java @@ -1,23 +1,23 @@ -package com.arch.policy.config; - -import com.arch.policy.ActiveSnapshotRegistry; -import com.arch.policy.DefaultSnapshotValidator; -import com.arch.policy.FileSystemSnapshotDirectory; -import com.arch.policy.PolicySnapshotService; -import com.arch.policy.SnapshotBuilder; -import com.arch.policy.SnapshotPorts.FullPolicyLoader; -import com.arch.policy.SnapshotPorts.IncrementalReplayer; -import com.arch.policy.application.PolicyIncrementalUpdater; -import com.arch.policy.application.PolicyStartupRunner; -import com.arch.policy.aggregation.AsyncSearchCoordinator; -import com.arch.policy.aggregation.LocalSearchWaiters; -import com.arch.policy.aggregation.SearchStateStore; -import com.arch.policy.aggregation.SupplierCallbackService; -import com.arch.policy.aggregation.SupplierTaskDispatcher; -import com.arch.policy.demo.DemoSupplierTaskDispatcher; -import com.arch.policy.kafka.KafkaPolicyChangeListener; -import com.arch.policy.redis.RedisSearchStateStore; -import com.arch.policy.redis.SearchFinishedSubscriber; +package com.arch.policy.common.config; + +import com.arch.policy.search.application.AsyncSearchCoordinator; +import com.arch.policy.search.application.LocalSearchWaiters; +import com.arch.policy.search.application.PolicyIncrementalUpdater; +import com.arch.policy.search.application.PolicyStartupRunner; +import com.arch.policy.search.application.SearchStateStore; +import com.arch.policy.search.application.SupplierCallbackService; +import com.arch.policy.search.application.SupplierTaskDispatcher; +import com.arch.policy.search.domain.snapshot.ActiveSnapshotRegistry; +import com.arch.policy.search.domain.snapshot.DefaultSnapshotValidator; +import com.arch.policy.search.domain.snapshot.FileSystemSnapshotDirectory; +import com.arch.policy.search.domain.snapshot.PolicySnapshotService; +import com.arch.policy.search.domain.snapshot.SnapshotBuilder; +import com.arch.policy.search.domain.snapshot.SnapshotPorts.FullPolicyLoader; +import com.arch.policy.search.domain.snapshot.SnapshotPorts.IncrementalReplayer; +import com.arch.policy.search.infrastructure.demo.DemoSupplierTaskDispatcher; +import com.arch.policy.search.infrastructure.kafka.KafkaPolicyChangeListener; +import com.arch.policy.search.infrastructure.redis.RedisSearchStateStore; +import com.arch.policy.search.infrastructure.redis.SearchFinishedSubscriber; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/architecture/src/main/java/com/arch/policy/MessagePosition.java b/architecture/src/main/java/com/arch/policy/common/model/MessagePosition.java similarity index 95% rename from architecture/src/main/java/com/arch/policy/MessagePosition.java rename to architecture/src/main/java/com/arch/policy/common/model/MessagePosition.java index 50794e1..edca20b 100644 --- a/architecture/src/main/java/com/arch/policy/MessagePosition.java +++ b/architecture/src/main/java/com/arch/policy/common/model/MessagePosition.java @@ -1,4 +1,4 @@ -package com.arch.policy; +package com.arch.policy.common.model; public final class MessagePosition implements Comparable { public static final MessagePosition BEGINNING = new MessagePosition(0); diff --git a/architecture/src/main/java/com/arch/policy/PolicyChange.java b/architecture/src/main/java/com/arch/policy/common/model/PolicyChange.java similarity index 96% rename from architecture/src/main/java/com/arch/policy/PolicyChange.java rename to architecture/src/main/java/com/arch/policy/common/model/PolicyChange.java index 5dfcecc..b014367 100644 --- a/architecture/src/main/java/com/arch/policy/PolicyChange.java +++ b/architecture/src/main/java/com/arch/policy/common/model/PolicyChange.java @@ -1,4 +1,4 @@ -package com.arch.policy; +package com.arch.policy.common.model; public final class PolicyChange { public enum Type { UPSERT, DELETE } diff --git a/architecture/src/main/java/com/arch/policy/PolicyRecord.java b/architecture/src/main/java/com/arch/policy/common/model/PolicyRecord.java similarity index 95% rename from architecture/src/main/java/com/arch/policy/PolicyRecord.java rename to architecture/src/main/java/com/arch/policy/common/model/PolicyRecord.java index 42371e3..6fbab70 100644 --- a/architecture/src/main/java/com/arch/policy/PolicyRecord.java +++ b/architecture/src/main/java/com/arch/policy/common/model/PolicyRecord.java @@ -1,4 +1,4 @@ -package com.arch.policy; +package com.arch.policy.common.model; import java.util.Arrays; import java.util.Collections; diff --git a/architecture/src/main/java/com/arch/policy/api/CallbackResponse.java b/architecture/src/main/java/com/arch/policy/common/search/CallbackResponse.java similarity index 88% rename from architecture/src/main/java/com/arch/policy/api/CallbackResponse.java rename to architecture/src/main/java/com/arch/policy/common/search/CallbackResponse.java index 43b5166..f5fccf7 100644 --- a/architecture/src/main/java/com/arch/policy/api/CallbackResponse.java +++ b/architecture/src/main/java/com/arch/policy/common/search/CallbackResponse.java @@ -1,4 +1,4 @@ -package com.arch.policy.api; +package com.arch.policy.common.search; import java.io.Serializable; diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java b/architecture/src/main/java/com/arch/policy/common/search/PolicySearchRequest.java similarity index 96% rename from architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java rename to architecture/src/main/java/com/arch/policy/common/search/PolicySearchRequest.java index fe9a308..6553673 100644 --- a/architecture/src/main/java/com/arch/policy/api/PolicySearchRequest.java +++ b/architecture/src/main/java/com/arch/policy/common/search/PolicySearchRequest.java @@ -1,4 +1,4 @@ -package com.arch.policy.api; +package com.arch.policy.common.search; import java.io.Serializable; import java.util.ArrayList; diff --git a/architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java b/architecture/src/main/java/com/arch/policy/common/search/PolicySearchResponse.java similarity index 95% rename from architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java rename to architecture/src/main/java/com/arch/policy/common/search/PolicySearchResponse.java index f8b3ecd..fa0d708 100644 --- a/architecture/src/main/java/com/arch/policy/api/PolicySearchResponse.java +++ b/architecture/src/main/java/com/arch/policy/common/search/PolicySearchResponse.java @@ -1,4 +1,4 @@ -package com.arch.policy.api; +package com.arch.policy.common.search; import java.io.Serializable; import java.util.ArrayList; diff --git a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java b/architecture/src/main/java/com/arch/policy/common/search/SupplierCallbackRequest.java similarity index 96% rename from architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java rename to architecture/src/main/java/com/arch/policy/common/search/SupplierCallbackRequest.java index f965f51..a4b5ed2 100644 --- a/architecture/src/main/java/com/arch/policy/api/SupplierCallbackRequest.java +++ b/architecture/src/main/java/com/arch/policy/common/search/SupplierCallbackRequest.java @@ -1,4 +1,4 @@ -package com.arch.policy.api; +package com.arch.policy.common.search; import java.io.Serializable; import java.util.ArrayList; diff --git a/architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java b/architecture/src/main/java/com/arch/policy/search/application/AsyncSearchCoordinator.java similarity index 93% rename from architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java rename to architecture/src/main/java/com/arch/policy/search/application/AsyncSearchCoordinator.java index a58d313..6491af2 100644 --- a/architecture/src/main/java/com/arch/policy/aggregation/AsyncSearchCoordinator.java +++ b/architecture/src/main/java/com/arch/policy/search/application/AsyncSearchCoordinator.java @@ -1,8 +1,8 @@ -package com.arch.policy.aggregation; +package com.arch.policy.search.application; -import com.arch.policy.api.PolicySearchRequest; -import com.arch.policy.api.PolicySearchResponse; -import com.arch.policy.api.SupplierCallbackRequest; +import com.arch.policy.common.search.PolicySearchRequest; +import com.arch.policy.common.search.PolicySearchResponse; +import com.arch.policy.common.search.SupplierCallbackRequest; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.ArrayList; diff --git a/architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java b/architecture/src/main/java/com/arch/policy/search/application/LocalSearchWaiters.java similarity index 96% rename from architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java rename to architecture/src/main/java/com/arch/policy/search/application/LocalSearchWaiters.java index 5ccf471..66c27c7 100644 --- a/architecture/src/main/java/com/arch/policy/aggregation/LocalSearchWaiters.java +++ b/architecture/src/main/java/com/arch/policy/search/application/LocalSearchWaiters.java @@ -1,4 +1,4 @@ -package com.arch.policy.aggregation; +package com.arch.policy.search.application; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; diff --git a/architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java b/architecture/src/main/java/com/arch/policy/search/application/PolicyIncrementalUpdater.java similarity index 71% rename from architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java rename to architecture/src/main/java/com/arch/policy/search/application/PolicyIncrementalUpdater.java index d7d1d35..b8deab9 100644 --- a/architecture/src/main/java/com/arch/policy/application/PolicyIncrementalUpdater.java +++ b/architecture/src/main/java/com/arch/policy/search/application/PolicyIncrementalUpdater.java @@ -1,7 +1,7 @@ -package com.arch.policy.application; +package com.arch.policy.search.application; -import com.arch.policy.ActiveSnapshotRegistry; -import com.arch.policy.PolicyChange; +import com.arch.policy.common.model.PolicyChange; +import com.arch.policy.search.domain.snapshot.ActiveSnapshotRegistry; public final class PolicyIncrementalUpdater { private final ActiveSnapshotRegistry registry; diff --git a/architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java b/architecture/src/main/java/com/arch/policy/search/application/PolicyStartupRunner.java similarity index 84% rename from architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java rename to architecture/src/main/java/com/arch/policy/search/application/PolicyStartupRunner.java index 58bbf5f..40ec539 100644 --- a/architecture/src/main/java/com/arch/policy/application/PolicyStartupRunner.java +++ b/architecture/src/main/java/com/arch/policy/search/application/PolicyStartupRunner.java @@ -1,6 +1,6 @@ -package com.arch.policy.application; +package com.arch.policy.search.application; -import com.arch.policy.PolicySnapshotService; +import com.arch.policy.search.domain.snapshot.PolicySnapshotService; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SearchState.java b/architecture/src/main/java/com/arch/policy/search/application/SearchState.java similarity index 57% rename from architecture/src/main/java/com/arch/policy/aggregation/SearchState.java rename to architecture/src/main/java/com/arch/policy/search/application/SearchState.java index fa6ae2e..140010a 100644 --- a/architecture/src/main/java/com/arch/policy/aggregation/SearchState.java +++ b/architecture/src/main/java/com/arch/policy/search/application/SearchState.java @@ -1,3 +1,3 @@ -package com.arch.policy.aggregation; +package com.arch.policy.search.application; public enum SearchState { WAITING, COMPLETED, TIMED_OUT } diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java b/architecture/src/main/java/com/arch/policy/search/application/SearchStateStore.java similarity index 91% rename from architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java rename to architecture/src/main/java/com/arch/policy/search/application/SearchStateStore.java index c0a0a33..d4bff02 100644 --- a/architecture/src/main/java/com/arch/policy/aggregation/SearchStateStore.java +++ b/architecture/src/main/java/com/arch/policy/search/application/SearchStateStore.java @@ -1,4 +1,4 @@ -package com.arch.policy.aggregation; +package com.arch.policy.search.application; import java.util.List; import java.util.Set; diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java b/architecture/src/main/java/com/arch/policy/search/application/SupplierCallbackService.java similarity index 93% rename from architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java rename to architecture/src/main/java/com/arch/policy/search/application/SupplierCallbackService.java index 6113c36..c3d8c59 100644 --- a/architecture/src/main/java/com/arch/policy/aggregation/SupplierCallbackService.java +++ b/architecture/src/main/java/com/arch/policy/search/application/SupplierCallbackService.java @@ -1,6 +1,6 @@ -package com.arch.policy.aggregation; +package com.arch.policy.search.application; -import com.arch.policy.api.SupplierCallbackRequest; +import com.arch.policy.common.search.SupplierCallbackRequest; import com.fasterxml.jackson.databind.ObjectMapper; public final class SupplierCallbackService { diff --git a/architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java b/architecture/src/main/java/com/arch/policy/search/application/SupplierTaskDispatcher.java similarity index 61% rename from architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java rename to architecture/src/main/java/com/arch/policy/search/application/SupplierTaskDispatcher.java index ab43db7..213e734 100644 --- a/architecture/src/main/java/com/arch/policy/aggregation/SupplierTaskDispatcher.java +++ b/architecture/src/main/java/com/arch/policy/search/application/SupplierTaskDispatcher.java @@ -1,6 +1,6 @@ -package com.arch.policy.aggregation; +package com.arch.policy.search.application; -import com.arch.policy.api.PolicySearchRequest; +import com.arch.policy.common.search.PolicySearchRequest; import java.util.Set; diff --git a/architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/ActiveSnapshotRegistry.java similarity index 97% rename from architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/ActiveSnapshotRegistry.java index f086e2c..44cb02b 100644 --- a/architecture/src/main/java/com/arch/policy/ActiveSnapshotRegistry.java +++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/ActiveSnapshotRegistry.java @@ -1,4 +1,4 @@ -package com.arch.policy; +package com.arch.policy.search.domain.snapshot; import java.util.concurrent.atomic.AtomicBoolean; diff --git a/architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/DefaultSnapshotValidator.java similarity index 76% rename from architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/DefaultSnapshotValidator.java index 2c8c848..2c51ddf 100644 --- a/architecture/src/main/java/com/arch/policy/DefaultSnapshotValidator.java +++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/DefaultSnapshotValidator.java @@ -1,6 +1,8 @@ -package com.arch.policy; +package com.arch.policy.search.domain.snapshot; -import static com.arch.policy.SnapshotPorts.SnapshotValidator; +import com.arch.policy.common.model.MessagePosition; + +import static com.arch.policy.search.domain.snapshot.SnapshotPorts.SnapshotValidator; /** Baseline invariants; domain-specific checks can be supplied through SnapshotValidator. */ public final class DefaultSnapshotValidator implements SnapshotValidator { diff --git a/architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/FileSystemSnapshotDirectory.java similarity index 91% rename from architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/FileSystemSnapshotDirectory.java index 64c0bb1..663fed2 100644 --- a/architecture/src/main/java/com/arch/policy/FileSystemSnapshotDirectory.java +++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/FileSystemSnapshotDirectory.java @@ -1,4 +1,4 @@ -package com.arch.policy; +package com.arch.policy.search.domain.snapshot; import java.io.IOException; import java.nio.file.Files; @@ -6,7 +6,7 @@ import java.util.Comparator; import java.util.stream.Stream; -import static com.arch.policy.SnapshotPorts.SnapshotDirectory; +import static com.arch.policy.search.domain.snapshot.SnapshotPorts.SnapshotDirectory; /** Keeps every version in an isolated directory and removes abandoned candidates. */ public final class FileSystemSnapshotDirectory implements SnapshotDirectory { diff --git a/architecture/src/main/java/com/arch/policy/PolicySnapshot.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshot.java similarity index 96% rename from architecture/src/main/java/com/arch/policy/PolicySnapshot.java rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshot.java index d4e7e53..eb13c1d 100644 --- a/architecture/src/main/java/com/arch/policy/PolicySnapshot.java +++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshot.java @@ -1,4 +1,8 @@ -package com.arch.policy; +package com.arch.policy.search.domain.snapshot; + +import com.arch.policy.common.model.MessagePosition; +import com.arch.policy.common.model.PolicyChange; +import com.arch.policy.common.model.PolicyRecord; import org.roaringbitmap.RoaringBitmap; import org.rocksdb.Options; diff --git a/architecture/src/main/java/com/arch/policy/PolicySnapshotService.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshotService.java similarity index 98% rename from architecture/src/main/java/com/arch/policy/PolicySnapshotService.java rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshotService.java index d90fac8..f2f35f9 100644 --- a/architecture/src/main/java/com/arch/policy/PolicySnapshotService.java +++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/PolicySnapshotService.java @@ -1,4 +1,4 @@ -package com.arch.policy; +package com.arch.policy.search.domain.snapshot; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; diff --git a/architecture/src/main/java/com/arch/policy/SnapshotBuilder.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotBuilder.java similarity index 92% rename from architecture/src/main/java/com/arch/policy/SnapshotBuilder.java rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotBuilder.java index fbd2ccf..49b8797 100644 --- a/architecture/src/main/java/com/arch/policy/SnapshotBuilder.java +++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotBuilder.java @@ -1,10 +1,12 @@ -package com.arch.policy; +package com.arch.policy.search.domain.snapshot; + +import com.arch.policy.common.model.MessagePosition; import org.rocksdb.RocksDBException; import java.nio.file.Path; -import static com.arch.policy.SnapshotPorts.*; +import static com.arch.policy.search.domain.snapshot.SnapshotPorts.*; /** Builds a candidate without exposing it to queries. */ public final class SnapshotBuilder { diff --git a/architecture/src/main/java/com/arch/policy/SnapshotPorts.java b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotPorts.java similarity index 90% rename from architecture/src/main/java/com/arch/policy/SnapshotPorts.java rename to architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotPorts.java index 9e3c4d4..f41f262 100644 --- a/architecture/src/main/java/com/arch/policy/SnapshotPorts.java +++ b/architecture/src/main/java/com/arch/policy/search/domain/snapshot/SnapshotPorts.java @@ -1,4 +1,6 @@ -package com.arch.policy; +package com.arch.policy.search.domain.snapshot; + +import com.arch.policy.common.model.MessagePosition; import java.nio.file.Path; diff --git a/architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/demo/DemoSupplierTaskDispatcher.java similarity index 86% rename from architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java rename to architecture/src/main/java/com/arch/policy/search/infrastructure/demo/DemoSupplierTaskDispatcher.java index ecb3714..7826bf0 100644 --- a/architecture/src/main/java/com/arch/policy/demo/DemoSupplierTaskDispatcher.java +++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/demo/DemoSupplierTaskDispatcher.java @@ -1,9 +1,9 @@ -package com.arch.policy.demo; +package com.arch.policy.search.infrastructure.demo; -import com.arch.policy.aggregation.SupplierCallbackService; -import com.arch.policy.aggregation.SupplierTaskDispatcher; -import com.arch.policy.api.PolicySearchRequest; -import com.arch.policy.api.SupplierCallbackRequest; +import com.arch.policy.search.application.SupplierCallbackService; +import com.arch.policy.search.application.SupplierTaskDispatcher; +import com.arch.policy.common.search.PolicySearchRequest; +import com.arch.policy.common.search.SupplierCallbackRequest; import java.util.Collections; import java.util.Set; diff --git a/architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/KafkaPolicyChangeListener.java similarity index 86% rename from architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java rename to architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/KafkaPolicyChangeListener.java index 595318c..e935aa6 100644 --- a/architecture/src/main/java/com/arch/policy/kafka/KafkaPolicyChangeListener.java +++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/KafkaPolicyChangeListener.java @@ -1,9 +1,9 @@ -package com.arch.policy.kafka; +package com.arch.policy.search.infrastructure.kafka; -import com.arch.policy.MessagePosition; -import com.arch.policy.PolicyChange; -import com.arch.policy.PolicyRecord; -import com.arch.policy.application.PolicyIncrementalUpdater; +import com.arch.policy.common.model.MessagePosition; +import com.arch.policy.common.model.PolicyChange; +import com.arch.policy.common.model.PolicyRecord; +import com.arch.policy.search.application.PolicyIncrementalUpdater; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.kafka.annotation.KafkaListener; diff --git a/architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/PolicyChangeMessage.java similarity index 94% rename from architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java rename to architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/PolicyChangeMessage.java index 3f539ec..9718d5f 100644 --- a/architecture/src/main/java/com/arch/policy/kafka/PolicyChangeMessage.java +++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/kafka/PolicyChangeMessage.java @@ -1,4 +1,4 @@ -package com.arch.policy.kafka; +package com.arch.policy.search.infrastructure.kafka; import java.util.ArrayList; import java.util.List; diff --git a/architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/redis/RedisSearchStateStore.java similarity index 96% rename from architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java rename to architecture/src/main/java/com/arch/policy/search/infrastructure/redis/RedisSearchStateStore.java index 49547f0..6347e52 100644 --- a/architecture/src/main/java/com/arch/policy/redis/RedisSearchStateStore.java +++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/redis/RedisSearchStateStore.java @@ -1,7 +1,7 @@ -package com.arch.policy.redis; +package com.arch.policy.search.infrastructure.redis; -import com.arch.policy.aggregation.SearchState; -import com.arch.policy.aggregation.SearchStateStore; +import com.arch.policy.search.application.SearchState; +import com.arch.policy.search.application.SearchStateStore; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; diff --git a/architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/redis/SearchFinishedSubscriber.java similarity index 82% rename from architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java rename to architecture/src/main/java/com/arch/policy/search/infrastructure/redis/SearchFinishedSubscriber.java index f8ea514..208c8f0 100644 --- a/architecture/src/main/java/com/arch/policy/redis/SearchFinishedSubscriber.java +++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/redis/SearchFinishedSubscriber.java @@ -1,6 +1,6 @@ -package com.arch.policy.redis; +package com.arch.policy.search.infrastructure.redis; -import com.arch.policy.aggregation.LocalSearchWaiters; +import com.arch.policy.search.application.LocalSearchWaiters; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; diff --git a/architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboPolicySearchService.java similarity index 80% rename from architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java rename to architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboPolicySearchService.java index ef51aa6..7fc5cca 100644 --- a/architecture/src/main/java/com/arch/policy/rpc/DubboPolicySearchService.java +++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboPolicySearchService.java @@ -1,9 +1,9 @@ -package com.arch.policy.rpc; +package com.arch.policy.search.infrastructure.rpc; -import com.arch.policy.api.PolicySearchRequest; -import com.arch.policy.api.PolicySearchResponse; -import com.arch.policy.api.PolicySearchRpcService; -import com.arch.policy.aggregation.AsyncSearchCoordinator; +import com.arch.policy.common.search.PolicySearchRequest; +import com.arch.policy.common.search.PolicySearchResponse; +import com.arch.policy.api.search.PolicySearchRpcService; +import com.arch.policy.search.application.AsyncSearchCoordinator; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.beans.factory.annotation.Qualifier; diff --git a/architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java b/architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboSupplierCallbackService.java similarity index 70% rename from architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java rename to architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboSupplierCallbackService.java index f685dc1..ae58dcb 100644 --- a/architecture/src/main/java/com/arch/policy/rpc/DubboSupplierCallbackService.java +++ b/architecture/src/main/java/com/arch/policy/search/infrastructure/rpc/DubboSupplierCallbackService.java @@ -1,9 +1,9 @@ -package com.arch.policy.rpc; +package com.arch.policy.search.infrastructure.rpc; -import com.arch.policy.aggregation.SupplierCallbackService; -import com.arch.policy.api.CallbackResponse; -import com.arch.policy.api.SupplierCallbackRequest; -import com.arch.policy.api.SupplierCallbackRpcService; +import com.arch.policy.search.application.SupplierCallbackService; +import com.arch.policy.common.search.CallbackResponse; +import com.arch.policy.common.search.SupplierCallbackRequest; +import com.arch.policy.api.search.SupplierCallbackRpcService; import org.apache.dubbo.config.annotation.DubboService; @DubboService(version = "1.0.0", timeout = 3000) diff --git a/architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java b/architecture/src/test/java/com/arch/policy/search/application/AsyncSearchCoordinatorTest.java similarity index 95% rename from architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java rename to architecture/src/test/java/com/arch/policy/search/application/AsyncSearchCoordinatorTest.java index fe28969..8117b2a 100644 --- a/architecture/src/test/java/com/arch/policy/AsyncSearchCoordinatorTest.java +++ b/architecture/src/test/java/com/arch/policy/search/application/AsyncSearchCoordinatorTest.java @@ -1,9 +1,8 @@ -package com.arch.policy; +package com.arch.policy.search.application; -import com.arch.policy.aggregation.*; -import com.arch.policy.api.PolicySearchRequest; -import com.arch.policy.api.PolicySearchResponse; -import com.arch.policy.api.SupplierCallbackRequest; +import com.arch.policy.common.search.PolicySearchRequest; +import com.arch.policy.common.search.PolicySearchResponse; +import com.arch.policy.common.search.SupplierCallbackRequest; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; diff --git a/architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java b/architecture/src/test/java/com/arch/policy/search/domain/snapshot/SnapshotLifecycleTest.java similarity index 95% rename from architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java rename to architecture/src/test/java/com/arch/policy/search/domain/snapshot/SnapshotLifecycleTest.java index 297a7c0..9a1ceaf 100644 --- a/architecture/src/test/java/com/arch/policy/SnapshotLifecycleTest.java +++ b/architecture/src/test/java/com/arch/policy/search/domain/snapshot/SnapshotLifecycleTest.java @@ -1,5 +1,7 @@ -package com.arch.policy; +package com.arch.policy.search.domain.snapshot; +import com.arch.policy.common.model.MessagePosition; +import com.arch.policy.common.model.PolicyRecord; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -11,7 +13,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; -import static com.arch.policy.SnapshotPorts.*; +import static com.arch.policy.search.domain.snapshot.SnapshotPorts.*; import static org.junit.jupiter.api.Assertions.*; class SnapshotLifecycleTest { From 4326ef25e694d97b7b9f976a267b360eb071aed7 Mon Sep 17 00:00:00 2001 From: coderbruis Date: Mon, 20 Jul 2026 21:15:38 +0800 Subject: [PATCH 18/21] =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=B8=8B=E5=8D=95?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E4=BC=AA=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- architecture/README.md | 59 ++++++++- .../policy/api/book/BookOrderRpcService.java | 12 ++ .../BookOrderApplicationService.java | 112 ++++++++++++++++++ .../book/application/BookOrderStore.java | 17 +++ .../book/application/OrderOutboxMessage.java | 30 +++++ .../book/application/OrderStateHistory.java | 33 ++++++ .../application/PostTransitionExecutor.java | 8 ++ .../RetryablePostTransitionExecutor.java | 24 ++++ .../application/TransitionCommitResult.java | 24 ++++ .../arch/policy/book/domain/BookOrder.java | 50 ++++++++ .../book/domain/DefaultOrderTransitions.java | 48 ++++++++ .../arch/policy/book/domain/OrderEvent.java | 15 +++ .../arch/policy/book/domain/OrderState.java | 26 ++++ .../policy/book/domain/OrderStateMachine.java | 72 +++++++++++ .../policy/book/domain/OrderTransition.java | 61 ++++++++++ .../book/domain/RequiredAttributeGuard.java | 16 +++ .../book/domain/StateTransitionContext.java | 30 +++++ .../policy/book/domain/TransitionAction.java | 5 + .../book/domain/TransitionExecution.java | 18 +++ .../policy/book/domain/TransitionGuard.java | 5 + .../repository/InMemoryBookOrderStore.java | 86 ++++++++++++++ .../InMemoryFailedPostActionStore.java | 37 ++++++ .../rpc/DubboBookOrderService.java | 25 ++++ .../policy/common/book/BookOrderRequest.java | 24 ++++ .../policy/common/book/BookOrderResponse.java | 20 ++++ .../policy/common/book/OrderEventRequest.java | 32 +++++ .../common/config/BookOrderConfiguration.java | 36 ++++++ .../BookOrderApplicationServiceTest.java | 105 ++++++++++++++++ .../RetryablePostTransitionExecutorTest.java | 40 +++++++ .../book/domain/OrderStateMachineTest.java | 89 ++++++++++++++ 30 files changed, 1157 insertions(+), 2 deletions(-) create mode 100644 architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderState.java create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java create mode 100644 architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java create mode 100644 architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java create mode 100644 architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java create mode 100644 architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java create mode 100644 architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java create mode 100644 architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java create mode 100644 architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java create mode 100644 architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java diff --git a/architecture/README.md b/architecture/README.md index 58cde70..04f7058 100644 --- a/architecture/README.md +++ b/architecture/README.md @@ -28,7 +28,7 @@ | --- | --- | --- | | 多供应商并发搜索 | 任务分发、异步回调、结果聚合、超时与迟到回调 | 已实现伪代码 | | 海量政策匹配 | 全量加载、增量追平、位图索引、快照校验与热切换 | 已实现伪代码 | -| 交易/下单 | 幂等、状态流转、库存确认、分布式一致性与补偿 | 待补充 | +| 交易/下单 | 幂等下单、受控状态流转、乐观并发与失败隔离 | 已实现核心伪代码 | ## 分层结构 @@ -37,6 +37,7 @@ ```text com.arch.policy ├── api +│ ├── book # 下单及订单状态变更契约 │ └── search # 项目级搜索契约、请求和响应 DTO ├── common │ ├── config # Spring Bean 装配 @@ -50,6 +51,10 @@ com.arch.policy │ ├── kafka # 政策变更消息适配器 │ ├── redis # 聚合状态和完成通知适配器 │ └── rpc # Dubbo 接口实现 +├── book +│ ├── application # 幂等下单、状态变更用例和持久化端口 +│ ├── domain # 订单聚合、状态枚举和合法迁移规则 +│ └── infrastructure # 内存仓储示例和 Dubbo 适配器 └── PolicySearchApplication # 当前搜索场景的启动入口 ``` @@ -146,6 +151,55 @@ flowchart TD Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序消息会被忽略。如果 Topic 使用多个分区, 生产端必须提供全局序列;否则应将当前位置模型调整为按分区保存 offset。 +## 场景三:幂等下单与订单状态机 + +`BookOrderRpcService.createOrder` 以调用方生成的 `requestId` 作为幂等键。订单通过 `CREATE_SUCCEEDED` 事件 +从 `CREATE` 进入 `WAIT_PAY`。重复请求返回第一次创建的订单,不重复生成业务单。生产实现应在数据库中为 +`request_id` 建唯一索引,并把创单与初始状态流转放入本地事务。 + +外部系统通过 `BookOrderRpcService.fireEvent` 提交支付、出票、验真、取消等业务事件,不能直接指定目标状态。 +`OrderStateMachine` 使用 `(当前状态, 业务事件)` 定位唯一迁移,状态参考 `ipolicytradecore`: + +```mermaid +stateDiagram-v2 + CREATE --> WAIT_PAY: CREATE_SUCCEEDED + CREATE --> CREATE_FAIL: CREATE_FAILED + CREATE --> CANCEL: CANCEL + WAIT_PAY --> BOOKING: PAY_SUCCEEDED + WAIT_PAY --> CANCEL: CANCEL + BOOKING --> BOOKED: BOOK_SUCCEEDED + BOOKING --> BOOK_FAIL: BOOK_FAILED + BOOKING --> VALIDATING: START_VALIDATE + BOOKED --> VALIDATING: START_VALIDATE + BOOKED --> CANCEL: CANCEL + BOOKED --> REFUNDED: REFUND_SUCCEEDED + BOOK_FAIL --> VALIDATING: START_VALIDATE + BOOK_FAIL --> CANCEL: CANCEL + VALIDATING --> BOOKED: VALIDATE_SUCCEEDED + VALIDATING --> VALIDATE_FAIL: VALIDATE_FAILED + VALIDATE_FAIL --> BOOKED: VALIDATE_SUCCEEDED + VALIDATE_FAIL --> CANCEL: CANCEL + CREATE_FAIL --> DELETED: DELETE + BOOK_FAIL --> DELETED: DELETE + CANCEL --> DELETED: DELETE + REFUNDED --> DELETED: DELETE +``` + +一次迁移依次执行 Guard、前置 Action、领域状态变更、原子持久化和后置 Action。`PAY_SUCCEEDED`、 +`BOOK_SUCCEEDED` 等关键事件通过 Guard 校验支付单号、PNR 等业务凭据;业务可以通过迁移 Builder 注册更多 +风控校验、库存检查和任务创建处理器,而不修改状态机引擎。 + +状态事件携带全局唯一 `eventId` 和 `expectedVersion`。仓储在一个事务边界内完成以下写入: + +1. 使用 `order_no + version` compare-and-set 更新订单,阻止并发覆盖。 +2. 保存 `eventId` 处理结果,重复消息直接返回第一次处理的快照。 +3. 追加包含 `from/event/to/operator` 的完整状态历史。 +4. 写入 Outbox 消息,由独立发布任务可靠投递给库存、支付、出票等下游。 + +后置 Action 仅在事务提交后执行,失败会进入 `FailedPostActionStore` 等待重试,不会把已提交订单回滚成旧状态。 +示例使用内存实现展示原子语义;生产落地应使用数据库唯一索引、条件更新、状态历史表、Outbox 表和重试任务, +并由消息消费方继续按照 `eventId` 幂等。 + ## 新增架构场景的约定 新增交易、下单或其他架构伪代码时,应遵循以下约定: @@ -165,4 +219,5 @@ Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序 mvn -f architecture/pom.xml clean test ``` -当前测试覆盖异步供应商聚合、超时返回部分结果、全量加增量快照构建、运行时快照切换以及失败候选版本隔离。 +当前测试覆盖异步供应商聚合、超时返回部分结果、全量加增量快照构建、运行时快照切换、幂等下单、事件驱动 +状态迁移、Guard/Action 执行顺序、事件幂等、乐观并发、状态历史和 Outbox 原子记录。 diff --git a/architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java b/architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java new file mode 100644 index 0000000..ff75a2c --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java @@ -0,0 +1,12 @@ +package com.arch.policy.api.book; + +import com.arch.policy.common.book.BookOrderRequest; +import com.arch.policy.common.book.BookOrderResponse; +import com.arch.policy.common.book.OrderEventRequest; + +public interface BookOrderRpcService { + + BookOrderResponse createOrder(BookOrderRequest request); + + BookOrderResponse fireEvent(OrderEventRequest request); +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java b/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java new file mode 100644 index 0000000..9531a45 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java @@ -0,0 +1,112 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.BookOrder; +import com.arch.policy.book.domain.OrderEvent; +import com.arch.policy.book.domain.OrderState; +import com.arch.policy.book.domain.OrderStateMachine; +import com.arch.policy.book.domain.StateTransitionContext; +import com.arch.policy.book.domain.TransitionExecution; +import com.arch.policy.common.book.BookOrderRequest; +import com.arch.policy.common.book.BookOrderResponse; +import com.arch.policy.common.book.OrderEventRequest; + +import java.math.BigDecimal; +import java.util.Collections; +import java.util.UUID; + +public final class BookOrderApplicationService { + private final BookOrderStore store; + private final OrderStateMachine stateMachine; + private final PostTransitionExecutor postTransitionExecutor; + + public BookOrderApplicationService(BookOrderStore store, OrderStateMachine stateMachine, + PostTransitionExecutor postTransitionExecutor) { + this.store = store; + this.stateMachine = stateMachine; + this.postTransitionExecutor = postTransitionExecutor; + } + + public BookOrderResponse createOrder(BookOrderRequest request) { + validate(request); + BookOrder existing = store.findByRequestId(request.getRequestId()); + if (existing != null) return response(existing); + + BookOrder order = BookOrder.create(newOrderNo(), request.getRequestId(), request.getCustomerId(), + request.getProductId(), request.getQuantity(), request.getAmount()); + TransitionExecution execution = stateMachine.fire(new StateTransitionContext( + "CREATE:" + request.getRequestId(), OrderEvent.CREATE_SUCCEEDED, order, + request.getCustomerId(), Collections.emptyMap())); + TransitionCommitResult result = store.create(order, history(execution), outbox(execution)); + runPostActionsIfCommitted(result, execution); + return response(result.getOrder()); + } + + public BookOrderResponse fireEvent(OrderEventRequest request) { + validate(request); + BookOrder processed = store.findByEventId(request.getEventId()); + if (processed != null) return response(processed); + BookOrder order = store.findByOrderNo(request.getOrderNo()); + if (order == null) throw new IllegalArgumentException("order not found: " + request.getOrderNo()); + if (order.getVersion() != request.getExpectedVersion()) { + throw new IllegalStateException("stale order version: " + request.getExpectedVersion()); + } + OrderEvent event = parseEvent(request.getEvent()); + TransitionExecution execution = stateMachine.fire(new StateTransitionContext( + request.getEventId(), event, order, request.getOperator(), request.getAttributes())); + TransitionCommitResult result = store.transit(order, execution.getPreviousVersion(), + history(execution), outbox(execution)); + runPostActionsIfCommitted(result, execution); + return response(result.getOrder()); + } + + private void runPostActionsIfCommitted(TransitionCommitResult result, TransitionExecution execution) { + if (!result.isDuplicateEvent()) postTransitionExecutor.execute(stateMachine, execution); + } + + private static OrderStateHistory history(TransitionExecution execution) { + return new OrderStateHistory(execution.getContext().getEventId(), + execution.getContext().getOrder().getOrderNo(), execution.getTransition().getFrom(), + execution.getTransition().getEvent(), execution.getTransition().getTo(), + execution.getContext().getOperator(), System.currentTimeMillis()); + } + + private static OrderOutboxMessage outbox(TransitionExecution execution) { + BookOrder order = execution.getContext().getOrder(); + return new OrderOutboxMessage(execution.getContext().getEventId(), order.getOrderNo(), + execution.getContext().getEvent(), order.getState(), order.getVersion()); + } + + private static void validate(BookOrderRequest request) { + if (request == null) throw new IllegalArgumentException("request is required"); + if (isBlank(request.getRequestId())) throw new IllegalArgumentException("requestId is required"); + if (isBlank(request.getCustomerId())) throw new IllegalArgumentException("customerId is required"); + if (isBlank(request.getProductId())) throw new IllegalArgumentException("productId is required"); + if (request.getQuantity() <= 0) throw new IllegalArgumentException("quantity must be positive"); + if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("amount must not be negative"); + } + } + + private static void validate(OrderEventRequest request) { + if (request == null || isBlank(request.getEventId()) || isBlank(request.getOrderNo()) + || isBlank(request.getEvent()) || isBlank(request.getOperator())) { + throw new IllegalArgumentException("eventId, orderNo, event and operator are required"); + } + } + + private static OrderEvent parseEvent(String event) { + try { return OrderEvent.valueOf(event.trim().toUpperCase()); } + catch (IllegalArgumentException failure) { + throw new IllegalArgumentException("unknown order event: " + event, failure); + } + } + + private static String newOrderNo() { + return "BO" + UUID.randomUUID().toString().replace("-", "").substring(0, 20).toUpperCase(); + } + + private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } + private static BookOrderResponse response(BookOrder order) { + return new BookOrderResponse(order.getOrderNo(), order.getState().name(), order.getVersion()); + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java b/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java new file mode 100644 index 0000000..5e69210 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java @@ -0,0 +1,17 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.BookOrder; + +public interface BookOrderStore { + BookOrder findByOrderNo(String orderNo); + + BookOrder findByRequestId(String requestId); + + BookOrder findByEventId(String eventId); + + TransitionCommitResult create(BookOrder order, OrderStateHistory history, + OrderOutboxMessage outboxMessage); + + TransitionCommitResult transit(BookOrder order, long expectedVersion, + OrderStateHistory history, OrderOutboxMessage outboxMessage); +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java b/architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java new file mode 100644 index 0000000..9e0b4c3 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java @@ -0,0 +1,30 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.OrderEvent; +import com.arch.policy.book.domain.OrderState; + +public final class OrderOutboxMessage { + private final String eventId; + private final String orderNo; + private final OrderEvent event; + private final OrderState state; + private final long orderVersion; + private boolean published; + + public OrderOutboxMessage(String eventId, String orderNo, OrderEvent event, + OrderState state, long orderVersion) { + this.eventId = eventId; + this.orderNo = orderNo; + this.event = event; + this.state = state; + this.orderVersion = orderVersion; + } + + public String getEventId() { return eventId; } + public String getOrderNo() { return orderNo; } + public OrderEvent getEvent() { return event; } + public OrderState getState() { return state; } + public long getOrderVersion() { return orderVersion; } + public boolean isPublished() { return published; } + public void markPublished() { published = true; } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java b/architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java new file mode 100644 index 0000000..38e8fab --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java @@ -0,0 +1,33 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.OrderEvent; +import com.arch.policy.book.domain.OrderState; + +public final class OrderStateHistory { + private final String eventId; + private final String orderNo; + private final OrderState from; + private final OrderEvent event; + private final OrderState to; + private final String operator; + private final long occurredAtMillis; + + public OrderStateHistory(String eventId, String orderNo, OrderState from, OrderEvent event, + OrderState to, String operator, long occurredAtMillis) { + this.eventId = eventId; + this.orderNo = orderNo; + this.from = from; + this.event = event; + this.to = to; + this.operator = operator; + this.occurredAtMillis = occurredAtMillis; + } + + public String getEventId() { return eventId; } + public String getOrderNo() { return orderNo; } + public OrderState getFrom() { return from; } + public OrderEvent getEvent() { return event; } + public OrderState getTo() { return to; } + public String getOperator() { return operator; } + public long getOccurredAtMillis() { return occurredAtMillis; } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java b/architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java new file mode 100644 index 0000000..74e5d54 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java @@ -0,0 +1,8 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.OrderStateMachine; +import com.arch.policy.book.domain.TransitionExecution; + +public interface PostTransitionExecutor { + void execute(OrderStateMachine stateMachine, TransitionExecution execution); +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java b/architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java new file mode 100644 index 0000000..3043b16 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java @@ -0,0 +1,24 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.OrderStateMachine; +import com.arch.policy.book.domain.TransitionExecution; + +public final class RetryablePostTransitionExecutor implements PostTransitionExecutor { + private final FailedPostActionStore failedActionStore; + + public RetryablePostTransitionExecutor(FailedPostActionStore failedActionStore) { + this.failedActionStore = failedActionStore; + } + + @Override public void execute(OrderStateMachine stateMachine, TransitionExecution execution) { + try { + stateMachine.afterCommit(execution); + } catch (RuntimeException failure) { + failedActionStore.record(execution, failure); + } + } + + public interface FailedPostActionStore { + void record(TransitionExecution execution, RuntimeException failure); + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java b/architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java new file mode 100644 index 0000000..6564e33 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java @@ -0,0 +1,24 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.BookOrder; + +public final class TransitionCommitResult { + private final BookOrder order; + private final boolean duplicateEvent; + + private TransitionCommitResult(BookOrder order, boolean duplicateEvent) { + this.order = order; + this.duplicateEvent = duplicateEvent; + } + + public static TransitionCommitResult committed(BookOrder order) { + return new TransitionCommitResult(order, false); + } + + public static TransitionCommitResult duplicate(BookOrder order) { + return new TransitionCommitResult(order, true); + } + + public BookOrder getOrder() { return order; } + public boolean isDuplicateEvent() { return duplicateEvent; } +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java b/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java new file mode 100644 index 0000000..3daa8f3 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java @@ -0,0 +1,50 @@ +package com.arch.policy.book.domain; + +import java.math.BigDecimal; + +public final class BookOrder { + private final String orderNo; + private final String requestId; + private final String customerId; + private final String productId; + private final int quantity; + private final BigDecimal amount; + private OrderState state; + private long version; + + private BookOrder(String orderNo, String requestId, String customerId, String productId, + int quantity, BigDecimal amount, OrderState state, long version) { + this.orderNo = orderNo; + this.requestId = requestId; + this.customerId = customerId; + this.productId = productId; + this.quantity = quantity; + this.amount = amount; + this.state = state; + this.version = version; + } + + public static BookOrder create(String orderNo, String requestId, String customerId, + String productId, int quantity, BigDecimal amount) { + return new BookOrder(orderNo, requestId, customerId, productId, quantity, amount, + OrderState.CREATE, 0L); + } + + public BookOrder copy() { + return new BookOrder(orderNo, requestId, customerId, productId, quantity, amount, state, version); + } + + void applyState(OrderState target) { + state = target; + version++; + } + + public String getOrderNo() { return orderNo; } + public String getRequestId() { return requestId; } + public String getCustomerId() { return customerId; } + public String getProductId() { return productId; } + public int getQuantity() { return quantity; } + public BigDecimal getAmount() { return amount; } + public OrderState getState() { return state; } + public long getVersion() { return version; } +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java b/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java new file mode 100644 index 0000000..bab4e01 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java @@ -0,0 +1,48 @@ +package com.arch.policy.book.domain; + +import java.util.ArrayList; +import java.util.List; + +public final class DefaultOrderTransitions { + private DefaultOrderTransitions() {} + + public static List definitions() { + List transitions = new ArrayList(); + add(transitions, OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY); + add(transitions, OrderState.CREATE, OrderEvent.CREATE_FAILED, OrderState.CREATE_FAIL); + add(transitions, OrderState.CREATE, OrderEvent.CANCEL, OrderState.CANCEL); + transitions.add(OrderTransition + .from(OrderState.WAIT_PAY, OrderEvent.PAY_SUCCEEDED, OrderState.BOOKING) + .guard(new RequiredAttributeGuard("paymentNo")).build()); + add(transitions, OrderState.WAIT_PAY, OrderEvent.CANCEL, OrderState.CANCEL); + transitions.add(OrderTransition + .from(OrderState.BOOKING, OrderEvent.BOOK_SUCCEEDED, OrderState.BOOKED) + .guard(new RequiredAttributeGuard("bookingReference")).build()); + transitions.add(OrderTransition + .from(OrderState.BOOKING, OrderEvent.BOOK_FAILED, OrderState.BOOK_FAIL) + .guard(new RequiredAttributeGuard("failureCode")).build()); + add(transitions, OrderState.BOOKING, OrderEvent.START_VALIDATE, OrderState.VALIDATING); + add(transitions, OrderState.BOOK_FAIL, OrderEvent.START_VALIDATE, OrderState.VALIDATING); + add(transitions, OrderState.BOOK_FAIL, OrderEvent.CANCEL, OrderState.CANCEL); + add(transitions, OrderState.BOOK_FAIL, OrderEvent.DELETE, OrderState.DELETED); + add(transitions, OrderState.BOOKED, OrderEvent.START_VALIDATE, OrderState.VALIDATING); + add(transitions, OrderState.BOOKED, OrderEvent.CANCEL, OrderState.CANCEL); + transitions.add(OrderTransition + .from(OrderState.BOOKED, OrderEvent.REFUND_SUCCEEDED, OrderState.REFUNDED) + .guard(new RequiredAttributeGuard("refundNo")).build()); + add(transitions, OrderState.VALIDATING, OrderEvent.VALIDATE_SUCCEEDED, OrderState.BOOKED); + add(transitions, OrderState.VALIDATING, OrderEvent.VALIDATE_FAILED, OrderState.VALIDATE_FAIL); + add(transitions, OrderState.VALIDATE_FAIL, OrderEvent.VALIDATE_SUCCEEDED, OrderState.BOOKED); + add(transitions, OrderState.VALIDATE_FAIL, OrderEvent.CANCEL, OrderState.CANCEL); + add(transitions, OrderState.CREATE_FAIL, OrderEvent.CANCEL, OrderState.CANCEL); + add(transitions, OrderState.CREATE_FAIL, OrderEvent.DELETE, OrderState.DELETED); + add(transitions, OrderState.CANCEL, OrderEvent.DELETE, OrderState.DELETED); + add(transitions, OrderState.REFUNDED, OrderEvent.DELETE, OrderState.DELETED); + return transitions; + } + + private static void add(List transitions, OrderState from, + OrderEvent event, OrderState to) { + transitions.add(OrderTransition.from(from, event, to).build()); + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java new file mode 100644 index 0000000..a32a008 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java @@ -0,0 +1,15 @@ +package com.arch.policy.book.domain; + +public enum OrderEvent { + CREATE_SUCCEEDED, + CREATE_FAILED, + PAY_SUCCEEDED, + BOOK_SUCCEEDED, + BOOK_FAILED, + START_VALIDATE, + VALIDATE_SUCCEEDED, + VALIDATE_FAILED, + CANCEL, + REFUND_SUCCEEDED, + DELETE +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderState.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderState.java new file mode 100644 index 0000000..a747609 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/domain/OrderState.java @@ -0,0 +1,26 @@ +package com.arch.policy.book.domain; + +public enum OrderState { + CREATE(0, "创单"), + WAIT_PAY(1, "待支付"), + CREATE_FAIL(2, "创单失败"), + CANCEL(3, "已取消"), + BOOKING(4, "预定中"), + BOOKED(5, "已预定"), + BOOK_FAIL(6, "预定失败"), + DELETED(7, "已删除"), + REFUNDED(8, "已退订"), + VALIDATING(10, "验真中"), + VALIDATE_FAIL(11, "验真失败"); + + private final int code; + private final String description; + + OrderState(int code, String description) { + this.code = code; + this.description = description; + } + + public int getCode() { return code; } + public String getDescription() { return description; } +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java new file mode 100644 index 0000000..7d5b19d --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java @@ -0,0 +1,72 @@ +package com.arch.policy.book.domain; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class OrderStateMachine { + private final Map transitions; + + public OrderStateMachine(List definitions) { + Map registry = new HashMap(); + for (OrderTransition transition : definitions) { + TransitionKey key = new TransitionKey(transition.getFrom(), transition.getEvent()); + if (registry.put(key, transition) != null) { + throw new IllegalStateException("duplicate order transition: " + key); + } + } + transitions = Collections.unmodifiableMap(registry); + } + + public TransitionExecution fire(StateTransitionContext context) { + OrderTransition transition = transitions.get( + new TransitionKey(context.getOrder().getState(), context.getEvent())); + if (transition == null) { + throw new IllegalStateException("unsupported order event " + context.getEvent() + + " in state " + context.getOrder().getState()); + } + executeGuards(transition, context); + executeActions(transition.getPreActions(), context); + long previousVersion = context.getOrder().getVersion(); + context.getOrder().applyState(transition.getTo()); + return new TransitionExecution(transition, context, previousVersion); + } + + public void afterCommit(TransitionExecution execution) { + executeActions(execution.getTransition().getPostActions(), execution.getContext()); + } + + public OrderState targetOf(OrderState state, OrderEvent event) { + OrderTransition transition = transitions.get(new TransitionKey(state, event)); + return transition == null ? null : transition.getTo(); + } + + private static void executeGuards(OrderTransition transition, StateTransitionContext context) { + for (TransitionGuard guard : transition.getGuards()) guard.check(context); + } + + private static void executeActions(List actions, StateTransitionContext context) { + for (TransitionAction action : actions) action.execute(context); + } + + private static final class TransitionKey { + private final OrderState state; + private final OrderEvent event; + + private TransitionKey(OrderState state, OrderEvent event) { + this.state = state; + this.event = event; + } + + @Override public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof TransitionKey)) return false; + TransitionKey that = (TransitionKey) other; + return state == that.state && event == that.event; + } + + @Override public int hashCode() { return 31 * state.hashCode() + event.hashCode(); } + @Override public String toString() { return state + " + " + event; } + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java new file mode 100644 index 0000000..4ae61df --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java @@ -0,0 +1,61 @@ +package com.arch.policy.book.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class OrderTransition { + private final OrderState from; + private final OrderEvent event; + private final OrderState to; + private final List guards; + private final List preActions; + private final List postActions; + + private OrderTransition(Builder builder) { + from = builder.from; + event = builder.event; + to = builder.to; + guards = immutableCopy(builder.guards); + preActions = immutableCopy(builder.preActions); + postActions = immutableCopy(builder.postActions); + } + + public static Builder from(OrderState from, OrderEvent event, OrderState to) { + return new Builder(from, event, to); + } + + public OrderState getFrom() { return from; } + public OrderEvent getEvent() { return event; } + public OrderState getTo() { return to; } + public List getGuards() { return guards; } + public List getPreActions() { return preActions; } + public List getPostActions() { return postActions; } + + private static List immutableCopy(List values) { + return Collections.unmodifiableList(new ArrayList(values)); + } + + public static final class Builder { + private final OrderState from; + private final OrderEvent event; + private final OrderState to; + private final List guards = new ArrayList(); + private final List preActions = new ArrayList(); + private final List postActions = new ArrayList(); + + private Builder(OrderState from, OrderEvent event, OrderState to) { + if (from == null || event == null || to == null) { + throw new IllegalArgumentException("from, event and to are required"); + } + this.from = from; + this.event = event; + this.to = to; + } + + public Builder guard(TransitionGuard guard) { guards.add(guard); return this; } + public Builder before(TransitionAction action) { preActions.add(action); return this; } + public Builder after(TransitionAction action) { postActions.add(action); return this; } + public OrderTransition build() { return new OrderTransition(this); } + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java b/architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java new file mode 100644 index 0000000..b636a00 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java @@ -0,0 +1,16 @@ +package com.arch.policy.book.domain; + +public final class RequiredAttributeGuard implements TransitionGuard { + private final String attributeName; + + public RequiredAttributeGuard(String attributeName) { + this.attributeName = attributeName; + } + + @Override public void check(StateTransitionContext context) { + String value = context.attribute(attributeName); + if (value == null || value.trim().isEmpty()) { + throw new IllegalStateException(attributeName + " is required for event " + context.getEvent()); + } + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java b/architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java new file mode 100644 index 0000000..3269260 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java @@ -0,0 +1,30 @@ +package com.arch.policy.book.domain; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class StateTransitionContext { + private final String eventId; + private final OrderEvent event; + private final BookOrder order; + private final String operator; + private final Map attributes; + + public StateTransitionContext(String eventId, OrderEvent event, BookOrder order, + String operator, Map attributes) { + this.eventId = eventId; + this.event = event; + this.order = order; + this.operator = operator; + this.attributes = attributes == null ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap(attributes)); + } + + public String getEventId() { return eventId; } + public OrderEvent getEvent() { return event; } + public BookOrder getOrder() { return order; } + public String getOperator() { return operator; } + public Map getAttributes() { return attributes; } + public String attribute(String name) { return attributes.get(name); } +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java b/architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java new file mode 100644 index 0000000..a035155 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java @@ -0,0 +1,5 @@ +package com.arch.policy.book.domain; + +public interface TransitionAction { + void execute(StateTransitionContext context); +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java b/architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java new file mode 100644 index 0000000..1e4a0c8 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java @@ -0,0 +1,18 @@ +package com.arch.policy.book.domain; + +public final class TransitionExecution { + private final OrderTransition transition; + private final StateTransitionContext context; + private final long previousVersion; + + public TransitionExecution(OrderTransition transition, StateTransitionContext context, + long previousVersion) { + this.transition = transition; + this.context = context; + this.previousVersion = previousVersion; + } + + public OrderTransition getTransition() { return transition; } + public StateTransitionContext getContext() { return context; } + public long getPreviousVersion() { return previousVersion; } +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java b/architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java new file mode 100644 index 0000000..5c88ebe --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java @@ -0,0 +1,5 @@ +package com.arch.policy.book.domain; + +public interface TransitionGuard { + void check(StateTransitionContext context); +} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java new file mode 100644 index 0000000..a76b4af --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java @@ -0,0 +1,86 @@ +package com.arch.policy.book.infrastructure.repository; + +import com.arch.policy.book.application.BookOrderStore; +import com.arch.policy.book.application.OrderOutboxMessage; +import com.arch.policy.book.application.OrderStateHistory; +import com.arch.policy.book.application.TransitionCommitResult; +import com.arch.policy.book.domain.BookOrder; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class InMemoryBookOrderStore implements BookOrderStore { + private final Map orders = new HashMap(); + private final Map requestIndexes = new HashMap(); + private final Map eventResults = new HashMap(); + private final List histories = new ArrayList(); + private final List outboxMessages = new ArrayList(); + + @Override public synchronized BookOrder findByOrderNo(String orderNo) { + return copy(orders.get(orderNo)); + } + + @Override public synchronized BookOrder findByRequestId(String requestId) { + String orderNo = requestIndexes.get(requestId); + return orderNo == null ? null : copy(orders.get(orderNo)); + } + + @Override public synchronized BookOrder findByEventId(String eventId) { + return copy(eventResults.get(eventId)); + } + + @Override public synchronized TransitionCommitResult create( + BookOrder order, OrderStateHistory history, OrderOutboxMessage outboxMessage) { + String existingOrderNo = requestIndexes.get(order.getRequestId()); + if (existingOrderNo != null) { + return TransitionCommitResult.duplicate(copy(orders.get(existingOrderNo))); + } + persist(order, history, outboxMessage); + requestIndexes.put(order.getRequestId(), order.getOrderNo()); + return TransitionCommitResult.committed(order.copy()); + } + + @Override public synchronized TransitionCommitResult transit( + BookOrder order, long expectedVersion, OrderStateHistory history, + OrderOutboxMessage outboxMessage) { + BookOrder processed = eventResults.get(history.getEventId()); + if (processed != null) return TransitionCommitResult.duplicate(processed.copy()); + + BookOrder current = orders.get(order.getOrderNo()); + if (current == null || current.getVersion() != expectedVersion + || order.getVersion() != expectedVersion + 1) { + throw new IllegalStateException("concurrent order update: " + order.getOrderNo()); + } + persist(order, history, outboxMessage); + return TransitionCommitResult.committed(order.copy()); + } + + public synchronized List historiesOf(String orderNo) { + List result = new ArrayList(); + for (OrderStateHistory history : histories) { + if (orderNo.equals(history.getOrderNo())) result.add(history); + } + return Collections.unmodifiableList(result); + } + + public synchronized List unpublishedOutboxMessages() { + List result = new ArrayList(); + for (OrderOutboxMessage message : outboxMessages) { + if (!message.isPublished()) result.add(message); + } + return Collections.unmodifiableList(result); + } + + private void persist(BookOrder order, OrderStateHistory history, OrderOutboxMessage outboxMessage) { + BookOrder snapshot = order.copy(); + orders.put(order.getOrderNo(), snapshot); + eventResults.put(history.getEventId(), snapshot.copy()); + histories.add(history); + outboxMessages.add(outboxMessage); + } + + private static BookOrder copy(BookOrder order) { return order == null ? null : order.copy(); } +} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java new file mode 100644 index 0000000..00c7db5 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java @@ -0,0 +1,37 @@ +package com.arch.policy.book.infrastructure.repository; + +import com.arch.policy.book.application.RetryablePostTransitionExecutor.FailedPostActionStore; +import com.arch.policy.book.domain.TransitionExecution; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class InMemoryFailedPostActionStore implements FailedPostActionStore { + private final List failures = new ArrayList(); + + @Override public synchronized void record(TransitionExecution execution, RuntimeException failure) { + failures.add(new FailedPostAction(execution.getContext().getEventId(), + execution.getContext().getOrder().getOrderNo(), failure.getMessage())); + } + + public synchronized List all() { + return Collections.unmodifiableList(new ArrayList(failures)); + } + + public static final class FailedPostAction { + private final String eventId; + private final String orderNo; + private final String reason; + + public FailedPostAction(String eventId, String orderNo, String reason) { + this.eventId = eventId; + this.orderNo = orderNo; + this.reason = reason; + } + + public String getEventId() { return eventId; } + public String getOrderNo() { return orderNo; } + public String getReason() { return reason; } + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java new file mode 100644 index 0000000..12625eb --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java @@ -0,0 +1,25 @@ +package com.arch.policy.book.infrastructure.rpc; + +import com.arch.policy.api.book.BookOrderRpcService; +import com.arch.policy.book.application.BookOrderApplicationService; +import com.arch.policy.common.book.BookOrderRequest; +import com.arch.policy.common.book.BookOrderResponse; +import com.arch.policy.common.book.OrderEventRequest; +import org.apache.dubbo.config.annotation.DubboService; + +@DubboService(version = "1.0.0", timeout = 3000) +public final class DubboBookOrderService implements BookOrderRpcService { + private final BookOrderApplicationService applicationService; + + public DubboBookOrderService(BookOrderApplicationService applicationService) { + this.applicationService = applicationService; + } + + @Override public BookOrderResponse createOrder(BookOrderRequest request) { + return applicationService.createOrder(request); + } + + @Override public BookOrderResponse fireEvent(OrderEventRequest request) { + return applicationService.fireEvent(request); + } +} diff --git a/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java b/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java new file mode 100644 index 0000000..d890783 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java @@ -0,0 +1,24 @@ +package com.arch.policy.common.book; + +import java.io.Serializable; +import java.math.BigDecimal; + +public final class BookOrderRequest implements Serializable { + private static final long serialVersionUID = 1L; + private String requestId; + private String customerId; + private String productId; + private int quantity; + private BigDecimal amount; + + public String getRequestId() { return requestId; } + public void setRequestId(String requestId) { this.requestId = requestId; } + public String getCustomerId() { return customerId; } + public void setCustomerId(String customerId) { this.customerId = customerId; } + public String getProductId() { return productId; } + public void setProductId(String productId) { this.productId = productId; } + public int getQuantity() { return quantity; } + public void setQuantity(int quantity) { this.quantity = quantity; } + public BigDecimal getAmount() { return amount; } + public void setAmount(BigDecimal amount) { this.amount = amount; } +} diff --git a/architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java b/architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java new file mode 100644 index 0000000..4845559 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java @@ -0,0 +1,20 @@ +package com.arch.policy.common.book; + +import java.io.Serializable; + +public final class BookOrderResponse implements Serializable { + private static final long serialVersionUID = 1L; + private final String orderNo; + private final String state; + private final long version; + + public BookOrderResponse(String orderNo, String state, long version) { + this.orderNo = orderNo; + this.state = state; + this.version = version; + } + + public String getOrderNo() { return orderNo; } + public String getState() { return state; } + public long getVersion() { return version; } +} diff --git a/architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java b/architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java new file mode 100644 index 0000000..0e39d3a --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java @@ -0,0 +1,32 @@ +package com.arch.policy.common.book; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class OrderEventRequest implements Serializable { + private static final long serialVersionUID = 1L; + private String eventId; + private String orderNo; + private String event; + private String operator; + private long expectedVersion; + private Map attributes = new HashMap(); + + public String getEventId() { return eventId; } + public void setEventId(String eventId) { this.eventId = eventId; } + public String getOrderNo() { return orderNo; } + public void setOrderNo(String orderNo) { this.orderNo = orderNo; } + public String getEvent() { return event; } + public void setEvent(String event) { this.event = event; } + public String getOperator() { return operator; } + public void setOperator(String operator) { this.operator = operator; } + public long getExpectedVersion() { return expectedVersion; } + public void setExpectedVersion(long expectedVersion) { this.expectedVersion = expectedVersion; } + public Map getAttributes() { return Collections.unmodifiableMap(attributes); } + public void setAttributes(Map attributes) { + this.attributes = attributes == null + ? new HashMap() : new HashMap(attributes); + } +} diff --git a/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java b/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java new file mode 100644 index 0000000..71716c5 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java @@ -0,0 +1,36 @@ +package com.arch.policy.common.config; + +import com.arch.policy.book.application.BookOrderApplicationService; +import com.arch.policy.book.application.BookOrderStore; +import com.arch.policy.book.application.PostTransitionExecutor; +import com.arch.policy.book.application.RetryablePostTransitionExecutor; +import com.arch.policy.book.domain.DefaultOrderTransitions; +import com.arch.policy.book.domain.OrderStateMachine; +import com.arch.policy.book.infrastructure.repository.InMemoryBookOrderStore; +import com.arch.policy.book.infrastructure.repository.InMemoryFailedPostActionStore; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class BookOrderConfiguration { + @Bean public OrderStateMachine orderStateMachine() { + return new OrderStateMachine(DefaultOrderTransitions.definitions()); + } + + @Bean public BookOrderStore bookOrderStore() { return new InMemoryBookOrderStore(); } + + @Bean public InMemoryFailedPostActionStore failedPostActionStore() { + return new InMemoryFailedPostActionStore(); + } + + @Bean public PostTransitionExecutor postTransitionExecutor( + InMemoryFailedPostActionStore failedPostActionStore) { + return new RetryablePostTransitionExecutor(failedPostActionStore); + } + + @Bean public BookOrderApplicationService bookOrderApplicationService( + BookOrderStore store, OrderStateMachine stateMachine, + PostTransitionExecutor postTransitionExecutor) { + return new BookOrderApplicationService(store, stateMachine, postTransitionExecutor); + } +} diff --git a/architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java b/architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java new file mode 100644 index 0000000..6dffeaf --- /dev/null +++ b/architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java @@ -0,0 +1,105 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.DefaultOrderTransitions; +import com.arch.policy.book.domain.OrderStateMachine; +import com.arch.policy.book.infrastructure.repository.InMemoryBookOrderStore; +import com.arch.policy.book.infrastructure.repository.InMemoryFailedPostActionStore; +import com.arch.policy.common.book.BookOrderRequest; +import com.arch.policy.common.book.BookOrderResponse; +import com.arch.policy.common.book.OrderEventRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class BookOrderApplicationServiceTest { + private InMemoryBookOrderStore store; + private BookOrderApplicationService service; + + @BeforeEach + void setUp() { + store = new InMemoryBookOrderStore(); + OrderStateMachine stateMachine = new OrderStateMachine(DefaultOrderTransitions.definitions()); + service = new BookOrderApplicationService(store, stateMachine, + new RetryablePostTransitionExecutor(new InMemoryFailedPostActionStore())); + } + + @Test + void createsOrderIdempotentlyAndCommitsHistoryWithOutbox() { + BookOrderRequest request = request("request-1"); + + BookOrderResponse first = service.createOrder(request); + BookOrderResponse repeated = service.createOrder(request); + + assertEquals("WAIT_PAY", first.getState()); + assertEquals(1L, first.getVersion()); + assertEquals(first.getOrderNo(), repeated.getOrderNo()); + assertEquals(1, store.historiesOf(first.getOrderNo()).size()); + assertEquals(1, store.unpublishedOutboxMessages().size()); + } + + @Test + void drivesOrderByBusinessEventsInsteadOfTargetStates() { + BookOrderResponse created = service.createOrder(request("request-2")); + + BookOrderResponse booking = fire(created, "pay-1", "PAY_SUCCEEDED"); + BookOrderResponse booked = fire(booking, "book-1", "BOOK_SUCCEEDED"); + + assertEquals("BOOKED", booked.getState()); + assertEquals(3L, booked.getVersion()); + assertEquals(3, store.historiesOf(booked.getOrderNo()).size()); + assertEquals(3, store.unpublishedOutboxMessages().size()); + } + + @Test + void returnsOriginalResultForDuplicateEventEvenWithStaleVersion() { + BookOrderResponse created = service.createOrder(request("request-3")); + BookOrderResponse booking = fire(created, "pay-2", "PAY_SUCCEEDED"); + + BookOrderResponse duplicate = fire(created, "pay-2", "PAY_SUCCEEDED"); + + assertEquals(booking.getState(), duplicate.getState()); + assertEquals(booking.getVersion(), duplicate.getVersion()); + assertEquals(2, store.historiesOf(created.getOrderNo()).size()); + } + + @Test + void rejectsUnsupportedEventAndConcurrentUpdate() { + BookOrderResponse created = service.createOrder(request("request-4")); + assertThrows(IllegalStateException.class, + () -> fire(created, "book-too-early", "BOOK_SUCCEEDED")); + + fire(created, "pay-3", "PAY_SUCCEEDED"); + assertThrows(IllegalStateException.class, + () -> fire(created, "cancel-stale", "CANCEL")); + } + + private BookOrderResponse fire(BookOrderResponse current, String eventId, String event) { + OrderEventRequest request = new OrderEventRequest(); + request.setEventId(eventId); + request.setOrderNo(current.getOrderNo()); + request.setExpectedVersion(current.getVersion()); + request.setEvent(event); + request.setOperator("test-operator"); + if ("PAY_SUCCEEDED".equals(event)) { + request.setAttributes(Collections.singletonMap("paymentNo", "payment-1")); + } else if ("BOOK_SUCCEEDED".equals(event)) { + request.setAttributes(Collections.singletonMap("bookingReference", "PNR001")); + } + return service.fireEvent(request); + } + + private static BookOrderRequest request(String requestId) { + BookOrderRequest request = new BookOrderRequest(); + request.setRequestId(requestId); + request.setCustomerId("customer-1"); + request.setProductId("policy-1"); + request.setQuantity(1); + request.setAmount(new BigDecimal("100.00")); + return request; + } +} diff --git a/architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java b/architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java new file mode 100644 index 0000000..892f71f --- /dev/null +++ b/architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java @@ -0,0 +1,40 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.BookOrder; +import com.arch.policy.book.domain.OrderEvent; +import com.arch.policy.book.domain.OrderState; +import com.arch.policy.book.domain.OrderStateMachine; +import com.arch.policy.book.domain.OrderTransition; +import com.arch.policy.book.domain.StateTransitionContext; +import com.arch.policy.book.domain.TransitionExecution; +import com.arch.policy.book.infrastructure.repository.InMemoryFailedPostActionStore; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RetryablePostTransitionExecutorTest { + @Test + void recordsPostActionFailureForRetryWithoutRevertingCommittedState() { + OrderTransition transition = OrderTransition + .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY) + .after(context -> { throw new IllegalStateException("notification unavailable"); }) + .build(); + OrderStateMachine stateMachine = new OrderStateMachine(Arrays.asList(transition)); + BookOrder order = BookOrder.create("order-1", "request-1", "customer-1", "product-1", + 1, BigDecimal.ONE); + TransitionExecution execution = stateMachine.fire(new StateTransitionContext( + "event-1", OrderEvent.CREATE_SUCCEEDED, order, "tester", + Collections.emptyMap())); + InMemoryFailedPostActionStore failureStore = new InMemoryFailedPostActionStore(); + + new RetryablePostTransitionExecutor(failureStore).execute(stateMachine, execution); + + assertEquals(OrderState.WAIT_PAY, order.getState()); + assertEquals(1, failureStore.all().size()); + assertEquals("event-1", failureStore.all().get(0).getEventId()); + } +} diff --git a/architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java b/architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java new file mode 100644 index 0000000..9b17b6f --- /dev/null +++ b/architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java @@ -0,0 +1,89 @@ +package com.arch.policy.book.domain; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class OrderStateMachineTest { + @Test + void resolvesTargetByCurrentStateAndBusinessEvent() { + OrderStateMachine stateMachine = new OrderStateMachine(DefaultOrderTransitions.definitions()); + + assertEquals(OrderState.WAIT_PAY, + stateMachine.targetOf(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED)); + assertEquals(OrderState.BOOKING, + stateMachine.targetOf(OrderState.WAIT_PAY, OrderEvent.PAY_SUCCEEDED)); + assertEquals(OrderState.BOOKED, + stateMachine.targetOf(OrderState.VALIDATE_FAIL, OrderEvent.VALIDATE_SUCCEEDED)); + assertNull(stateMachine.targetOf(OrderState.WAIT_PAY, OrderEvent.BOOK_SUCCEEDED)); + } + + @Test + void executesGuardAndPreActionBeforeMutationAndPostActionAfterCommit() { + final AtomicInteger calls = new AtomicInteger(); + TransitionGuard guard = context -> calls.compareAndSet(0, 1); + TransitionAction preAction = context -> { + assertEquals(OrderState.CREATE, context.getOrder().getState()); + calls.compareAndSet(1, 2); + }; + TransitionAction postAction = context -> { + assertEquals(OrderState.WAIT_PAY, context.getOrder().getState()); + calls.compareAndSet(2, 3); + }; + OrderTransition transition = OrderTransition + .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY) + .guard(guard).before(preAction).after(postAction).build(); + OrderStateMachine stateMachine = new OrderStateMachine(Arrays.asList(transition)); + BookOrder order = order(); + + TransitionExecution execution = stateMachine.fire(context(order, OrderEvent.CREATE_SUCCEEDED)); + assertEquals(2, calls.get()); + assertEquals(OrderState.WAIT_PAY, order.getState()); + + stateMachine.afterCommit(execution); + assertEquals(3, calls.get()); + } + + @Test + void guardFailurePreventsStateMutation() { + OrderTransition transition = OrderTransition + .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY) + .guard(context -> { throw new IllegalStateException("risk rejected"); }) + .build(); + OrderStateMachine stateMachine = new OrderStateMachine(Arrays.asList(transition)); + BookOrder order = order(); + + assertThrows(IllegalStateException.class, + () -> stateMachine.fire(context(order, OrderEvent.CREATE_SUCCEEDED))); + assertEquals(OrderState.CREATE, order.getState()); + assertEquals(0L, order.getVersion()); + } + + @Test + void rejectsDuplicateTransitionRegistration() { + OrderTransition first = OrderTransition + .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY).build(); + OrderTransition duplicate = OrderTransition + .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.CREATE_FAIL).build(); + + assertThrows(IllegalStateException.class, + () -> new OrderStateMachine(Arrays.asList(first, duplicate))); + } + + private static BookOrder order() { + return BookOrder.create("order-1", "request-1", "customer-1", "product-1", + 1, BigDecimal.ONE); + } + + private static StateTransitionContext context(BookOrder order, OrderEvent event) { + return new StateTransitionContext("event-1", event, order, "tester", + Collections.emptyMap()); + } +} From 1bc1bae44018c3b39cfb708b8e093c9d5eee67a3 Mon Sep 17 00:00:00 2001 From: coderbruis Date: Tue, 21 Jul 2026 21:03:02 +0800 Subject: [PATCH 19/21] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=8B=E5=8D=95Seata?= =?UTF-8?q?=20saga=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- architecture/README.md | 90 +++++++-- architecture/pom.xml | 30 +-- .../BookOrderApplicationService.java | 173 +++++++++++------- .../book/application/BookOrderStore.java | 9 +- .../application/CompensationTaskService.java | 51 ++++++ .../application/CreateOrderResultCache.java | 11 ++ .../application/DirectOrderCreationSaga.java | 24 +++ .../application/ExternalResultStatus.java | 7 + .../book/application/GdsBookingGateway.java | 9 + .../book/application/GdsBookingResult.java | 29 +++ .../application/GdsReconciliationService.java | 25 +++ .../book/application/LocalCreateResult.java | 24 +++ .../OrderCreationOutboxMessage.java | 21 +++ .../OrderCreationOutboxPublisher.java | 25 +++ .../book/application/OrderCreationSaga.java | 20 ++ .../OrderCreationSagaStateServices.java | 86 +++++++++ .../OrderEventApplicationService.java | 97 ++++++++++ .../book/application/OrderReconciler.java | 5 + .../book/application/OrderWorkflowTask.java | 19 ++ .../application/OrderWorkflowTaskStore.java | 5 + .../book/application/PaymentGateway.java | 5 + .../application/PromotionStockGateway.java | 5 + .../policy/book/application/RecoveryTask.java | 66 +++++++ .../application/RecoveryTaskProcessor.java | 67 +++++++ .../book/application/RecoveryTaskStore.java | 11 ++ .../arch/policy/book/domain/BookOrder.java | 12 +- .../book/domain/DefaultOrderTransitions.java | 7 + .../arch/policy/book/domain/OrderEvent.java | 2 + .../cache/DefaultCreateOrderResultCache.java | 54 ++++++ .../demo/DemoGdsBookingGateway.java | 18 ++ .../job/OrderCreationOutboxScheduler.java | 15 ++ .../job/OrderRecoveryScheduler.java | 13 ++ .../repository/InMemoryBookOrderStore.java | 35 +++- .../InMemoryOrderWorkflowTaskStore.java | 17 ++ .../repository/InMemoryRecoveryTaskStore.java | 45 +++++ .../rpc/DubboBookOrderService.java | 8 +- .../seata/SeataOrderCreationSaga.java | 37 ++++ .../policy/common/book/BookOrderRequest.java | 3 + .../common/config/BookOrderConfiguration.java | 109 ++++++++++- .../common/config/RedissonConfiguration.java | 28 +++ .../common/config/SeataSagaConfiguration.java | 54 ++++++ .../redis/RedissonDistributedLock.java | 47 +++++ .../redis/RedissonRedisClient.java | 30 +++ .../policy/common/lock/DistributedLock.java | 14 ++ .../DistributedLockUnavailableException.java | 7 + .../arch/policy/common/redis/RedisClient.java | 7 + .../common/redis/RedisClientException.java | 7 + .../src/main/resources/application.yml | 9 + .../main/resources/db/book_order_schema.sql | 37 ++++ .../statelang/book_order_creation_saga.json | 54 ++++++ .../BookOrderApplicationServiceTest.java | 105 ----------- .../RetryablePostTransitionExecutorTest.java | 40 ---- .../book/domain/OrderStateMachineTest.java | 89 --------- .../AsyncSearchCoordinatorTest.java | 98 ---------- .../snapshot/SnapshotLifecycleTest.java | 121 ------------ 55 files changed, 1469 insertions(+), 567 deletions(-) create mode 100644 architecture/src/main/java/com/arch/policy/book/application/CompensationTaskService.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/CreateOrderResultCache.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/DirectOrderCreationSaga.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/ExternalResultStatus.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/GdsBookingGateway.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/GdsBookingResult.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/GdsReconciliationService.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/LocalCreateResult.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxMessage.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxPublisher.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationSaga.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationSagaStateServices.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderEventApplicationService.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderReconciler.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTask.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTaskStore.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/PaymentGateway.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/PromotionStockGateway.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/RecoveryTask.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskProcessor.java create mode 100644 architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskStore.java create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/cache/DefaultCreateOrderResultCache.java create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/demo/DemoGdsBookingGateway.java create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderCreationOutboxScheduler.java create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderRecoveryScheduler.java create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryOrderWorkflowTaskStore.java create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryRecoveryTaskStore.java create mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/seata/SeataOrderCreationSaga.java create mode 100644 architecture/src/main/java/com/arch/policy/common/config/RedissonConfiguration.java create mode 100644 architecture/src/main/java/com/arch/policy/common/config/SeataSagaConfiguration.java create mode 100644 architecture/src/main/java/com/arch/policy/common/infrastructure/redis/RedissonDistributedLock.java create mode 100644 architecture/src/main/java/com/arch/policy/common/infrastructure/redis/RedissonRedisClient.java create mode 100644 architecture/src/main/java/com/arch/policy/common/lock/DistributedLock.java create mode 100644 architecture/src/main/java/com/arch/policy/common/lock/DistributedLockUnavailableException.java create mode 100644 architecture/src/main/java/com/arch/policy/common/redis/RedisClient.java create mode 100644 architecture/src/main/java/com/arch/policy/common/redis/RedisClientException.java create mode 100644 architecture/src/main/resources/db/book_order_schema.sql create mode 100644 architecture/src/main/resources/statelang/book_order_creation_saga.json delete mode 100644 architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java delete mode 100644 architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java delete mode 100644 architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java delete mode 100644 architecture/src/test/java/com/arch/policy/search/application/AsyncSearchCoordinatorTest.java delete mode 100644 architecture/src/test/java/com/arch/policy/search/domain/snapshot/SnapshotLifecycleTest.java diff --git a/architecture/README.md b/architecture/README.md index 04f7058..bc3298a 100644 --- a/architecture/README.md +++ b/architecture/README.md @@ -14,9 +14,9 @@ ## 模块目标 - 用最少的代码表达架构中的核心职责、协作关系和一致性约束。 -- 为类似业务问题提供可讨论、可验证、可演进的设计参考。 +- 为类似业务问题提供可讨论、可演进的设计参考。 - 通过接口隔离外部系统,使方案不被 Dubbo、Kafka、Redis 等具体技术绑定。 -- 通过测试验证关键架构行为,而不是覆盖完整的生产业务流程。 +- 通过场景说明和关键注释表达核心架构行为及约束。 - 持续积累搜索、政策匹配、交易、下单等不同场景的方案。 本模块属于架构伪代码,生产落地时仍需根据实际情况补充鉴权、限流、熔断、监控、链路追踪、异常分级、 @@ -153,9 +153,54 @@ Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序 ## 场景三:幂等下单与订单状态机 -`BookOrderRpcService.createOrder` 以调用方生成的 `requestId` 作为幂等键。订单通过 `CREATE_SUCCEEDED` 事件 -从 `CREATE` 进入 `WAIT_PAY`。重复请求返回第一次创建的订单,不重复生成业务单。生产实现应在数据库中为 -`request_id` 建唯一索引,并把创单与初始状态流转放入本地事务。 +`BookOrderRpcService.createOrder` 以调用方生成的 `requestId` 作为幂等键。下单首先保存 `CREATE` 状态订单, +提交后才由 Seata Saga 调用 GDS,因此不会在本地数据库事务中持有远程调用。 + +```mermaid +sequenceDiagram + autonumber + participant Caller as 调用方 + participant Order as 下单应用服务 + participant DB as 本地数据库 + participant Saga as Seata Saga + participant GDS as GDS + participant Task as 任务/补偿表 + + Caller->>Order: createOrder(requestId) + Order->>Order: 查询 Redis 幂等结果缓存 + Order->>Order: Redisson RLock 获取 requestId 短锁 + Order->>Order: 锁内二次查缓存,仅持锁者查数据库 + Order->>DB: 本地事务写订单 CREATE + DB-->>Order: 提交成功 + Order->>Order: 回填结果缓存并释放 RLock + Order->>DB: 抢占订单创建调度租约 + Order->>Saga: 启动 BookOrderCreationSaga + Saga->>GDS: 发起 PNR 占编 + GDS-->>Saga: SUCCESS / FAIL / UNKNOWN + alt SUCCESS + Saga->>DB: CREATE_SUCCEEDED → WAIT_PAY + Saga->>Task: 幂等创建待支付任务 + else FAIL + Saga->>DB: CREATE_FAILED → CREATE_FAIL + Saga->>Task: 幂等创建促销库存返还任务 + else UNKNOWN + Saga->>DB: START_VALIDATE → VALIDATING + Saga->>Task: 创建 GDS 核对任务 + 人工任务 + end +``` + +订单与 `START_ORDER_CREATION` Outbox 在同一本地事务中提交。事务后立即尝试发布;如果进程在订单提交后、 +启动 Saga 前崩溃,`OrderCreationOutboxScheduler` 会重新投递未发布消息。Saga 使用 `orderNo` 作为业务幂等键, +已经启动的流程由 Seata 根据持久化的状态机日志继续恢复。 +Saga 使用 `orderNo` 作为业务幂等键;GDS 侧也必须使用订单号或稳定请求号保证占编幂等。 + +高并发重复请求首先读取 `book:create:result:{requestId}` 幂等结果缓存,命中后不访问数据库。缓存未命中时, +使用 Redisson `RLock` 锁定 `book:create:lock:{requestId}`,锁内再次检查缓存,只有锁持有者才允许查询数据库和 +执行本地创建事务;未获得锁的请求等待首个请求回填结果缓存,同样不会查询数据库。RLock 最多等待 300ms, +持锁期间由 Redisson watchdog 自动续期;锁只覆盖本地事务,不覆盖耗时不可控的 Saga/GDS 调用。 + +订单状态迁移后会同步刷新结果缓存,缓存 TTL 为 10 分钟。Redis 不可用时锁和缓存主动降级,最终仍由数据库 +`request_id` 唯一索引保证只能创建一张订单;Redisson 锁和结果缓存是防穿透、削峰层,不是最终一致性依据。 外部系统通过 `BookOrderRpcService.fireEvent` 提交支付、出票、验真、取消等业务事件,不能直接指定目标状态。 `OrderStateMachine` 使用 `(当前状态, 业务事件)` 定位唯一迁移,状态参考 `ipolicytradecore`: @@ -164,6 +209,7 @@ Kafka 政策变更消息使用全局单调递增的 `position`,重复或乱序 stateDiagram-v2 CREATE --> WAIT_PAY: CREATE_SUCCEEDED CREATE --> CREATE_FAIL: CREATE_FAILED + CREATE --> VALIDATING: START_VALIDATE CREATE --> CANCEL: CANCEL WAIT_PAY --> BOOKING: PAY_SUCCEEDED WAIT_PAY --> CANCEL: CANCEL @@ -177,6 +223,8 @@ stateDiagram-v2 BOOK_FAIL --> CANCEL: CANCEL VALIDATING --> BOOKED: VALIDATE_SUCCEEDED VALIDATING --> VALIDATE_FAIL: VALIDATE_FAILED + VALIDATING --> WAIT_PAY: GDS_BOOKING_CONFIRMED + VALIDATING --> CREATE_FAIL: GDS_BOOKING_REJECTED VALIDATE_FAIL --> BOOKED: VALIDATE_SUCCEEDED VALIDATE_FAIL --> CANCEL: CANCEL CREATE_FAIL --> DELETED: DELETE @@ -200,6 +248,25 @@ stateDiagram-v2 示例使用内存实现展示原子语义;生产落地应使用数据库唯一索引、条件更新、状态历史表、Outbox 表和重试任务, 并由消息消费方继续按照 `eventId` 幂等。 +### Seata Saga 与补偿 + +`statelang/book_order_creation_saga.json` 是 Seata 状态语言定义,包含 GDS 服务节点、结果 Choice、异常捕获和 +`CancelReservedPnr` 补偿节点。`SeataOrderCreationSaga` 使用 `orderNo` 作为 business key 启动状态机; +`SeataSagaConfiguration` 在存在 `DataSource` 时创建数据库持久化的 `DbStateMachineConfig` 和 +`StateMachineEngine`。没有引擎 Bean 的本地演示环境使用相同 Saga State Services 直接编排,业务行为一致。 + +Saga 提供的是可恢复的最终一致性,而不是把 GDS 变成支持 ACID 回滚的数据库资源。每个外部正向动作都必须有 +幂等补偿语义,并允许空补偿: + +- PNR 占编失败且已经扣减促销库存:创建 `RETURN_PROMOTION_STOCK` 任务。 +- PNR 已占编但采购商取消:创建 `CANCEL_PNR` 任务。 +- 支付成功但最终出票失败:创建 `REFUND_PAYMENT` 任务。 +- GDS 返回 UNKNOWN:创建 `VALIDATE_GDS_BOOKING` 与人工核对任务,定时比较本地状态和 GDS 实际状态。 + +补偿表使用业务唯一键防重;取消 PNR 额外使用 `(order_no, child_order_no, pnr, task_type)` 唯一索引。任务采用 +指数退避重试,超过五次进入 `MANUAL_REQUIRED`。示例 DDL 位于 `db/book_order_schema.sql`;Seata Server 和 +Saga 引擎日志表应使用部署版本对应的官方脚本创建,避免跨版本复制表结构。 + ## 新增架构场景的约定 新增交易、下单或其他架构伪代码时,应遵循以下约定: @@ -208,16 +275,5 @@ stateDiagram-v2 2. 对外契约放在 `api.`,内部实现放在对应的 `` 业务包。 3. 使用 `application/domain/infrastructure` 表达职责边界,不让领域规则依赖具体中间件。 4. 优先提供展示关键协作关系的最小实现,避免把伪代码扩展成不完整的生产框架。 -5. 为幂等、一致性、并发、超时、切换和失败隔离等关键架构行为编写测试。 +5. 用精简的流程代码和注释表达幂等、一致性、并发、超时、切换和失败隔离等关键行为。 6. 在场景文档中记录设计取舍、适用边界,以及生产落地仍需补充的能力。 - -## 运行验证 - -在仓库根目录执行: - -```bash -mvn -f architecture/pom.xml clean test -``` - -当前测试覆盖异步供应商聚合、超时返回部分结果、全量加增量快照构建、运行时快照切换、幂等下单、事件驱动 -状态迁移、Guard/Action 执行顺序、事件幂等、乐观并发、状态历史和 Outbox 原子记录。 diff --git a/architecture/pom.xml b/architecture/pom.xml index 4f7f396..e677418 100644 --- a/architecture/pom.xml +++ b/architecture/pom.xml @@ -12,9 +12,11 @@ 8 8 UTF-8 - 5.10.2 2.7.18 3.2.15 + 1.8.0 + + 3.24.3 @@ -28,17 +30,16 @@ RoaringBitmap 0.9.47 - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - org.springframework.boot spring-boot-starter ${spring-boot.version} + + org.redisson + redisson + ${redisson.version} + org.springframework.boot spring-boot-starter-data-redis @@ -59,15 +60,20 @@ dubbo-spring-boot-starter ${dubbo.version} + + io.seata + seata-saga-engine + ${seata.version} + + + io.seata + seata-saga-engine-store + ${seata.version} + - - org.apache.maven.plugins - maven-surefire-plugin - 3.2.5 - org.springframework.boot spring-boot-maven-plugin diff --git a/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java b/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java index 9531a45..6a9694f 100644 --- a/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java +++ b/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java @@ -1,79 +1,134 @@ package com.arch.policy.book.application; import com.arch.policy.book.domain.BookOrder; -import com.arch.policy.book.domain.OrderEvent; -import com.arch.policy.book.domain.OrderState; -import com.arch.policy.book.domain.OrderStateMachine; -import com.arch.policy.book.domain.StateTransitionContext; -import com.arch.policy.book.domain.TransitionExecution; import com.arch.policy.common.book.BookOrderRequest; import com.arch.policy.common.book.BookOrderResponse; -import com.arch.policy.common.book.OrderEventRequest; +import com.arch.policy.common.lock.DistributedLock; +import com.arch.policy.common.lock.DistributedLockUnavailableException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.math.BigDecimal; -import java.util.Collections; import java.util.UUID; public final class BookOrderApplicationService { + private static final Logger LOGGER = LoggerFactory.getLogger(BookOrderApplicationService.class); + private static final DistributedLock.LockHandle NO_OP_LOCK = () -> { }; + private static final long CREATE_LOCK_WAIT_MILLIS = 300L; + private static final String CREATE_ORDER_LOCK_KEY_PREFIX = "book:create:lock:"; + private static final long CREATE_RESULT_WAIT_MILLIS = 500L; + private static final long CREATE_RESULT_TTL_MILLIS = 600_000L; private final BookOrderStore store; - private final OrderStateMachine stateMachine; - private final PostTransitionExecutor postTransitionExecutor; + private final OrderCreationOutboxPublisher outboxPublisher; + private final DistributedLock distributedLock; + private final CreateOrderResultCache resultCache; - public BookOrderApplicationService(BookOrderStore store, OrderStateMachine stateMachine, - PostTransitionExecutor postTransitionExecutor) { + public BookOrderApplicationService(BookOrderStore store, + OrderCreationOutboxPublisher outboxPublisher, + DistributedLock distributedLock, + CreateOrderResultCache resultCache) { this.store = store; - this.stateMachine = stateMachine; - this.postTransitionExecutor = postTransitionExecutor; + this.outboxPublisher = outboxPublisher; + this.distributedLock = distributedLock; + this.resultCache = resultCache; } + /** + * 创建订单并在本地事务提交后启动 GDS 占编 Saga。 + * + *

流程边界:

+ *
    + *
  1. 使用 requestId 查询 Redis 幂等结果缓存,命中则直接返回。
  2. + *
  3. 获取 requestId 对应的 Redisson 短锁并进行缓存二次检查。
  4. + *
  5. 在同一本地事务中保存 CREATE 状态订单和 Saga 启动 Outbox。
  6. + *
  7. 事务提交后发布 Outbox,使用 orderNo 幂等启动 Seata Saga。
  8. + *
  9. Saga 根据 GDS 的 SUCCESS、FAIL、UNKNOWN 结果分别推进至 + * WAIT_PAY、CREATE_FAIL、VALIDATING。
  10. + *
  11. 重新读取订单并返回 Saga 执行后的最新状态。
  12. + *
+ * + *

如果进程在订单提交后、Saga 启动前退出,定时发布器会重新投递未发布的 Outbox; + * Seata 根据持久化的状态机日志恢复已经启动的 Saga。

+ * + * @param request 下单请求,requestId 是业务幂等键 + * @return 当前最新订单状态;同步 Saga 通常返回 WAIT_PAY、CREATE_FAIL 或 VALIDATING + * @throws IllegalArgumentException 请求参数不合法时抛出 + */ public BookOrderResponse createOrder(BookOrderRequest request) { validate(request); - BookOrder existing = store.findByRequestId(request.getRequestId()); - if (existing != null) return response(existing); - - BookOrder order = BookOrder.create(newOrderNo(), request.getRequestId(), request.getCustomerId(), - request.getProductId(), request.getQuantity(), request.getAmount()); - TransitionExecution execution = stateMachine.fire(new StateTransitionContext( - "CREATE:" + request.getRequestId(), OrderEvent.CREATE_SUCCEEDED, order, - request.getCustomerId(), Collections.emptyMap())); - TransitionCommitResult result = store.create(order, history(execution), outbox(execution)); - runPostActionsIfCommitted(result, execution); - return response(result.getOrder()); - } - public BookOrderResponse fireEvent(OrderEventRequest request) { - validate(request); - BookOrder processed = store.findByEventId(request.getEventId()); - if (processed != null) return response(processed); - BookOrder order = store.findByOrderNo(request.getOrderNo()); - if (order == null) throw new IllegalArgumentException("order not found: " + request.getOrderNo()); - if (order.getVersion() != request.getExpectedVersion()) { - throw new IllegalStateException("stale order version: " + request.getExpectedVersion()); + // 锁前只访问 Redis,不查询数据库,避免大量重复请求同时穿透到主库。 + BookOrderResponse cached = resultCache.get(request.getRequestId()); + if (cached != null) return cached; + + DistributedLock.LockHandle lock = acquireCreateOrderLock(request.getRequestId()); + if (lock == null) return responseOfCompetingRequest(request.getRequestId()); + + BookOrder order; + OrderCreationOutboxMessage outboxMessage; + try { + // Double check:前一个持锁请求可能已在等待期间完成创建并回填缓存。 + cached = resultCache.get(request.getRequestId()); + if (cached != null) return cached; + + // 只有锁持有者才能访问数据库;用于缓存丢失、过期及 Redis 降级场景。 + BookOrder existing = store.findByRequestId(request.getRequestId()); + if (existing != null) { + BookOrderResponse existingResponse = response(existing); + resultCache.put(request.getRequestId(), existingResponse, CREATE_RESULT_TTL_MILLIS); + return existingResponse; + } + + // 构造下单对象(模拟下单的内容) + order = BookOrder.create(newOrderNo(), request.getRequestId(), request.getCustomerId(), + request.getProductId(), request.getPromotionId(), request.getQuantity(), + request.getAmount()); + + // 数据库 request_id 唯一索引是最终幂等防线。 + outboxMessage = new OrderCreationOutboxMessage(order.getOrderNo(), + order.getPromotionId()); + LocalCreateResult localResult = store.createOrder(order, outboxMessage); + if (localResult.isDuplicateRequest()) { + BookOrderResponse duplicateResponse = response(localResult.getOrder()); + resultCache.put(request.getRequestId(), duplicateResponse, CREATE_RESULT_TTL_MILLIS); + return duplicateResponse; + } + resultCache.put(request.getRequestId(), response(order), CREATE_RESULT_TTL_MILLIS); + } finally { + lock.close(); } - OrderEvent event = parseEvent(request.getEvent()); - TransitionExecution execution = stateMachine.fire(new StateTransitionContext( - request.getEventId(), event, order, request.getOperator(), request.getAttributes())); - TransitionCommitResult result = store.transit(order, execution.getPreviousVersion(), - history(execution), outbox(execution)); - runPostActionsIfCommitted(result, execution); - return response(result.getOrder()); - } - private void runPostActionsIfCommitted(TransitionCommitResult result, TransitionExecution execution) { - if (!result.isDuplicateEvent()) postTransitionExecutor.execute(stateMachine, execution); + // Redis 锁已释放;立即发布一次,崩溃遗漏由定时 Outbox 发布器补发。 + try { + outboxPublisher.publish(outboxMessage); + } catch (RuntimeException publishFailure) { + LOGGER.warn("Creation Outbox will be retried, orderNo={}", order.getOrderNo(), + publishFailure); + } + + // 同步 Saga 可能已经改变订单状态,因此返回前重新读取聚合快照。 + BookOrder latest = store.findByOrderNo(order.getOrderNo()); + BookOrderResponse latestResponse = response(latest == null ? order : latest); + resultCache.put(request.getRequestId(), latestResponse, CREATE_RESULT_TTL_MILLIS); + return latestResponse; } - private static OrderStateHistory history(TransitionExecution execution) { - return new OrderStateHistory(execution.getContext().getEventId(), - execution.getContext().getOrder().getOrderNo(), execution.getTransition().getFrom(), - execution.getTransition().getEvent(), execution.getTransition().getTo(), - execution.getContext().getOperator(), System.currentTimeMillis()); + private BookOrderResponse responseOfCompetingRequest(String requestId) { + BookOrderResponse cached = resultCache.await(requestId, CREATE_RESULT_WAIT_MILLIS); + if (cached != null) return cached; + throw new IllegalStateException("create order request is being processed: " + requestId); } - private static OrderOutboxMessage outbox(TransitionExecution execution) { - BookOrder order = execution.getContext().getOrder(); - return new OrderOutboxMessage(execution.getContext().getEventId(), order.getOrderNo(), - execution.getContext().getEvent(), order.getState(), order.getVersion()); + private DistributedLock.LockHandle acquireCreateOrderLock(String requestId) { + String key = CREATE_ORDER_LOCK_KEY_PREFIX + requestId; + try { + return distributedLock.tryAcquire(key, CREATE_LOCK_WAIT_MILLIS); + } catch (DistributedLockUnavailableException unavailable) { + // 数据库 request_id 唯一索引仍是最终幂等防线,Redis 故障时允许降级。 + LOGGER.warn("Create-order lock unavailable, fall back to database idempotency, key={}", + key, unavailable); + return NO_OP_LOCK; + } } private static void validate(BookOrderRequest request) { @@ -87,20 +142,6 @@ private static void validate(BookOrderRequest request) { } } - private static void validate(OrderEventRequest request) { - if (request == null || isBlank(request.getEventId()) || isBlank(request.getOrderNo()) - || isBlank(request.getEvent()) || isBlank(request.getOperator())) { - throw new IllegalArgumentException("eventId, orderNo, event and operator are required"); - } - } - - private static OrderEvent parseEvent(String event) { - try { return OrderEvent.valueOf(event.trim().toUpperCase()); } - catch (IllegalArgumentException failure) { - throw new IllegalArgumentException("unknown order event: " + event, failure); - } - } - private static String newOrderNo() { return "BO" + UUID.randomUUID().toString().replace("-", "").substring(0, 20).toUpperCase(); } diff --git a/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java b/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java index 5e69210..cf2bd4e 100644 --- a/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java +++ b/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java @@ -2,6 +2,8 @@ import com.arch.policy.book.domain.BookOrder; +import java.util.List; + public interface BookOrderStore { BookOrder findByOrderNo(String orderNo); @@ -9,8 +11,11 @@ public interface BookOrderStore { BookOrder findByEventId(String eventId); - TransitionCommitResult create(BookOrder order, OrderStateHistory history, - OrderOutboxMessage outboxMessage); + LocalCreateResult createOrder(BookOrder order, OrderCreationOutboxMessage outboxMessage); + + List findUnpublishedCreationOutbox(int limit); + + void markCreationOutboxPublished(String messageId); TransitionCommitResult transit(BookOrder order, long expectedVersion, OrderStateHistory history, OrderOutboxMessage outboxMessage); diff --git a/architecture/src/main/java/com/arch/policy/book/application/CompensationTaskService.java b/architecture/src/main/java/com/arch/policy/book/application/CompensationTaskService.java new file mode 100644 index 0000000..843c8f8 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/CompensationTaskService.java @@ -0,0 +1,51 @@ +package com.arch.policy.book.application; + +import java.util.HashMap; +import java.util.Map; + +public final class CompensationTaskService { + private final RecoveryTaskStore taskStore; + + public CompensationTaskService(RecoveryTaskStore taskStore) { this.taskStore = taskStore; } + + public void returnPromotionStock(String orderNo, String promotionId) { + if (isBlank(promotionId)) return; + enqueue("PROMOTION:" + orderNo + ":" + promotionId, + RecoveryTask.RecoveryType.RETURN_PROMOTION_STOCK, orderNo, + params("promotionId", promotionId)); + } + + public void cancelPnr(String orderNo, String childOrderNo, String pnr) { + if (isBlank(pnr)) return; + Map parameters = params("childOrderNo", childOrderNo); + parameters.put("pnr", pnr); + enqueue("CANCEL_PNR:" + orderNo + ":" + childOrderNo + ":" + pnr, + RecoveryTask.RecoveryType.CANCEL_PNR, orderNo, parameters); + } + + public void refundPayment(String orderNo, String paymentNo) { + if (isBlank(paymentNo)) return; + enqueue("REFUND:" + orderNo + ":" + paymentNo, + RecoveryTask.RecoveryType.REFUND_PAYMENT, orderNo, + params("paymentNo", paymentNo)); + } + + public void validateGdsBooking(String orderNo) { + enqueue("VALIDATE_GDS:" + orderNo, + RecoveryTask.RecoveryType.VALIDATE_GDS_BOOKING, orderNo, + new HashMap()); + } + + private void enqueue(String uniqueKey, RecoveryTask.RecoveryType type, String orderNo, + Map parameters) { + taskStore.saveIfAbsent(new RecoveryTask(uniqueKey, type, orderNo, parameters)); + } + + private static Map params(String name, String value) { + Map params = new HashMap(); + params.put(name, value == null ? "" : value); + return params; + } + + private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/CreateOrderResultCache.java b/architecture/src/main/java/com/arch/policy/book/application/CreateOrderResultCache.java new file mode 100644 index 0000000..db48c56 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/CreateOrderResultCache.java @@ -0,0 +1,11 @@ +package com.arch.policy.book.application; + +import com.arch.policy.common.book.BookOrderResponse; + +public interface CreateOrderResultCache { + BookOrderResponse get(String requestId); + + void put(String requestId, BookOrderResponse response, long ttlMillis); + + BookOrderResponse await(String requestId, long waitMillis); +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/DirectOrderCreationSaga.java b/architecture/src/main/java/com/arch/policy/book/application/DirectOrderCreationSaga.java new file mode 100644 index 0000000..cda115b --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/DirectOrderCreationSaga.java @@ -0,0 +1,24 @@ +package com.arch.policy.book.application; + +public final class DirectOrderCreationSaga implements OrderCreationSaga { + private final OrderCreationSagaStateServices services; + + public DirectOrderCreationSaga(OrderCreationSagaStateServices services) { this.services = services; } + + @Override public SagaStartResult start(String orderNo, String promotionId) { + GdsBookingResult result; + try { + result = services.reservePnr(orderNo); + } catch (RuntimeException uncertainFailure) { + result = GdsBookingResult.unknown(uncertainFailure.getClass().getSimpleName()); + } + if (result.getStatus() == ExternalResultStatus.SUCCESS) { + services.markCreateSuccess(orderNo, result.getPnr()); + } else if (result.getStatus() == ExternalResultStatus.FAIL) { + services.markCreateFailure(orderNo, result.getErrorCode(), promotionId); + } else { + services.markCreateUnknown(orderNo, result.getErrorCode()); + } + return new SagaStartResult("DIRECT:" + orderNo, false); + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/ExternalResultStatus.java b/architecture/src/main/java/com/arch/policy/book/application/ExternalResultStatus.java new file mode 100644 index 0000000..099da1a --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/ExternalResultStatus.java @@ -0,0 +1,7 @@ +package com.arch.policy.book.application; + +public enum ExternalResultStatus { + SUCCESS, + FAIL, + UNKNOWN +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/GdsBookingGateway.java b/architecture/src/main/java/com/arch/policy/book/application/GdsBookingGateway.java new file mode 100644 index 0000000..cc689f1 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/GdsBookingGateway.java @@ -0,0 +1,9 @@ +package com.arch.policy.book.application; + +public interface GdsBookingGateway { + GdsBookingResult createPnr(String orderNo); + + GdsBookingResult queryPnr(String orderNo); + + void cancelPnr(String orderNo, String childOrderNo, String pnr); +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/GdsBookingResult.java b/architecture/src/main/java/com/arch/policy/book/application/GdsBookingResult.java new file mode 100644 index 0000000..72b6d20 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/GdsBookingResult.java @@ -0,0 +1,29 @@ +package com.arch.policy.book.application; + +public final class GdsBookingResult { + private final ExternalResultStatus status; + private final String pnr; + private final String errorCode; + + public GdsBookingResult(ExternalResultStatus status, String pnr, String errorCode) { + this.status = status; + this.pnr = pnr; + this.errorCode = errorCode; + } + + public static GdsBookingResult success(String pnr) { + return new GdsBookingResult(ExternalResultStatus.SUCCESS, pnr, null); + } + + public static GdsBookingResult fail(String errorCode) { + return new GdsBookingResult(ExternalResultStatus.FAIL, null, errorCode); + } + + public static GdsBookingResult unknown(String errorCode) { + return new GdsBookingResult(ExternalResultStatus.UNKNOWN, null, errorCode); + } + + public ExternalResultStatus getStatus() { return status; } + public String getPnr() { return pnr; } + public String getErrorCode() { return errorCode; } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/GdsReconciliationService.java b/architecture/src/main/java/com/arch/policy/book/application/GdsReconciliationService.java new file mode 100644 index 0000000..8527498 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/GdsReconciliationService.java @@ -0,0 +1,25 @@ +package com.arch.policy.book.application; + +public final class GdsReconciliationService implements OrderReconciler { + private final GdsBookingGateway gdsGateway; + private final OrderCreationSagaStateServices sagaStates; + + public GdsReconciliationService(GdsBookingGateway gdsGateway, + OrderCreationSagaStateServices sagaStates) { + this.gdsGateway = gdsGateway; + this.sagaStates = sagaStates; + } + + @Override public void reconcile(String orderNo) { + GdsBookingResult result = gdsGateway.queryPnr(orderNo); + if (result.getStatus() == ExternalResultStatus.SUCCESS) { + sagaStates.markGdsValidationSuccess(orderNo, result.getPnr()); + return; + } + if (result.getStatus() == ExternalResultStatus.FAIL) { + sagaStates.markGdsValidationFailure(orderNo, result.getErrorCode()); + return; + } + throw new IllegalStateException("GDS result is still unknown"); + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/LocalCreateResult.java b/architecture/src/main/java/com/arch/policy/book/application/LocalCreateResult.java new file mode 100644 index 0000000..dbeaadd --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/LocalCreateResult.java @@ -0,0 +1,24 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.BookOrder; + +public final class LocalCreateResult { + private final BookOrder order; + private final boolean duplicateRequest; + + private LocalCreateResult(BookOrder order, boolean duplicateRequest) { + this.order = order; + this.duplicateRequest = duplicateRequest; + } + + public static LocalCreateResult created(BookOrder order) { + return new LocalCreateResult(order, false); + } + + public static LocalCreateResult duplicate(BookOrder order) { + return new LocalCreateResult(order, true); + } + + public BookOrder getOrder() { return order; } + public boolean isDuplicateRequest() { return duplicateRequest; } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxMessage.java b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxMessage.java new file mode 100644 index 0000000..cd52227 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxMessage.java @@ -0,0 +1,21 @@ +package com.arch.policy.book.application; + +/** 与订单在同一本地事务中保存,用于可靠启动创建 Saga。 */ +public final class OrderCreationOutboxMessage { + private final String messageId; + private final String orderNo; + private final String promotionId; + private boolean published; + + public OrderCreationOutboxMessage(String orderNo, String promotionId) { + this.messageId = "START_ORDER_CREATION:" + orderNo; + this.orderNo = orderNo; + this.promotionId = promotionId; + } + + public String getMessageId() { return messageId; } + public String getOrderNo() { return orderNo; } + public String getPromotionId() { return promotionId; } + public boolean isPublished() { return published; } + public void markPublished() { published = true; } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxPublisher.java b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxPublisher.java new file mode 100644 index 0000000..a1c5ec7 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxPublisher.java @@ -0,0 +1,25 @@ +package com.arch.policy.book.application; + +import java.util.List; + +/** 以至少一次语义发布 Outbox;OrderCreationSaga 必须使用 orderNo 幂等启动。 */ +public final class OrderCreationOutboxPublisher { + private final BookOrderStore store; + private final OrderCreationSaga creationSaga; + + public OrderCreationOutboxPublisher(BookOrderStore store, OrderCreationSaga creationSaga) { + this.store = store; + this.creationSaga = creationSaga; + } + + public void publish(OrderCreationOutboxMessage message) { + if (message.isPublished()) return; + creationSaga.start(message.getOrderNo(), message.getPromotionId()); + store.markCreationOutboxPublished(message.getMessageId()); + } + + public void publishPending(int limit) { + List messages = store.findUnpublishedCreationOutbox(limit); + for (OrderCreationOutboxMessage message : messages) publish(message); + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSaga.java b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSaga.java new file mode 100644 index 0000000..9f9637c --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSaga.java @@ -0,0 +1,20 @@ +package com.arch.policy.book.application; + +public interface OrderCreationSaga { + + /** 使用 orderNo 作为业务键幂等启动;重复调用不得重复执行外部业务动作。 */ + SagaStartResult start(String orderNo, String promotionId); + + final class SagaStartResult { + private final String instanceId; + private final boolean running; + + public SagaStartResult(String instanceId, boolean running) { + this.instanceId = instanceId; + this.running = running; + } + + public String getInstanceId() { return instanceId; } + public boolean isRunning() { return running; } + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSagaStateServices.java b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSagaStateServices.java new file mode 100644 index 0000000..366c613 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSagaStateServices.java @@ -0,0 +1,86 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.BookOrder; +import com.arch.policy.common.book.OrderEventRequest; + +import java.util.Collections; + +public final class OrderCreationSagaStateServices { + private final BookOrderStore store; + private final GdsBookingGateway gdsGateway; + private final OrderEventApplicationService eventService; + private final CompensationTaskService compensationService; + private final OrderWorkflowTaskStore workflowTaskStore; + + public OrderCreationSagaStateServices(BookOrderStore store, GdsBookingGateway gdsGateway, + OrderEventApplicationService eventService, + CompensationTaskService compensationService, + OrderWorkflowTaskStore workflowTaskStore) { + this.store = store; + this.gdsGateway = gdsGateway; + this.eventService = eventService; + this.compensationService = compensationService; + this.workflowTaskStore = workflowTaskStore; + } + + public GdsBookingResult reservePnr(String orderNo) { return gdsGateway.createPnr(orderNo); } + + public boolean markCreateSuccess(String orderNo, String pnr) { + fire(orderNo, orderNo + ":CREATE_SUCCESS", "CREATE_SUCCEEDED", "gds-booking-saga", + Collections.singletonMap("bookingReference", value(pnr))); + workflowTaskStore.saveIfAbsent(new OrderWorkflowTask("WAIT_PAYMENT:" + orderNo, + orderNo, OrderWorkflowTask.TaskType.WAIT_PAYMENT)); + return true; + } + + public boolean markCreateFailure(String orderNo, String errorCode, String promotionId) { + fire(orderNo, orderNo + ":CREATE_FAIL", "CREATE_FAILED", "gds-booking-saga", + Collections.singletonMap("failureCode", value(errorCode))); + compensationService.returnPromotionStock(orderNo, value(promotionId)); + return true; + } + + public boolean markCreateUnknown(String orderNo, String errorCode) { + fire(orderNo, orderNo + ":CREATE_UNKNOWN", "START_VALIDATE", "gds-booking-saga", + Collections.singletonMap("failureCode", value(errorCode))); + workflowTaskStore.saveIfAbsent(new OrderWorkflowTask("MANUAL_GDS_VALIDATION:" + orderNo, + orderNo, OrderWorkflowTask.TaskType.MANUAL_GDS_VALIDATION)); + compensationService.validateGdsBooking(orderNo); + return true; + } + + public boolean enqueueCancelPnr(String orderNo, String childOrderNo, String pnr) { + compensationService.cancelPnr(orderNo, childOrderNo, pnr); + return true; + } + + public void markGdsValidationSuccess(String orderNo, String pnr) { + fire(orderNo, orderNo + ":RECONCILE_SUCCESS", "GDS_BOOKING_CONFIRMED", + "gds-reconciliation-job", Collections.singletonMap("bookingReference", value(pnr))); + workflowTaskStore.saveIfAbsent(new OrderWorkflowTask("WAIT_PAYMENT:" + orderNo, + orderNo, OrderWorkflowTask.TaskType.WAIT_PAYMENT)); + } + + public void markGdsValidationFailure(String orderNo, String errorCode) { + BookOrder order = store.findByOrderNo(orderNo); + fire(orderNo, orderNo + ":RECONCILE_FAIL", "GDS_BOOKING_REJECTED", + "gds-reconciliation-job", Collections.singletonMap("failureCode", value(errorCode))); + compensationService.returnPromotionStock(orderNo, value(order.getPromotionId())); + } + + private void fire(String orderNo, String eventId, String event, String operator, + java.util.Map attributes) { + BookOrder order = store.findByOrderNo(orderNo); + OrderEventRequest request = new OrderEventRequest(); + request.setOrderNo(orderNo); + request.setEventId(eventId); + request.setEvent(event); + request.setOperator(operator); + request.setExpectedVersion(order.getVersion()); + request.setAttributes(attributes); + eventService.fireEvent(request); + } + + private static String value(String value) { return value == null ? "" : value; } + +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderEventApplicationService.java b/architecture/src/main/java/com/arch/policy/book/application/OrderEventApplicationService.java new file mode 100644 index 0000000..4dc9c52 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/OrderEventApplicationService.java @@ -0,0 +1,97 @@ +package com.arch.policy.book.application; + +import com.arch.policy.book.domain.BookOrder; +import com.arch.policy.book.domain.OrderEvent; +import com.arch.policy.book.domain.OrderStateMachine; +import com.arch.policy.book.domain.StateTransitionContext; +import com.arch.policy.book.domain.TransitionExecution; +import com.arch.policy.common.book.BookOrderResponse; +import com.arch.policy.common.book.OrderEventRequest; + +public final class OrderEventApplicationService { + private static final long CREATE_RESULT_TTL_MILLIS = 600_000L; + private final BookOrderStore store; + private final OrderStateMachine stateMachine; + private final PostTransitionExecutor postTransitionExecutor; + private final CompensationTaskService compensationTaskService; + private final CreateOrderResultCache resultCache; + + public OrderEventApplicationService(BookOrderStore store, OrderStateMachine stateMachine, + PostTransitionExecutor postTransitionExecutor, + CompensationTaskService compensationTaskService, + CreateOrderResultCache resultCache) { + this.store = store; + this.stateMachine = stateMachine; + this.postTransitionExecutor = postTransitionExecutor; + this.compensationTaskService = compensationTaskService; + this.resultCache = resultCache; + } + + public BookOrderResponse fireEvent(OrderEventRequest request) { + validate(request); + BookOrder processed = store.findByEventId(request.getEventId()); + if (processed != null) return response(processed); + BookOrder order = store.findByOrderNo(request.getOrderNo()); + if (order == null) throw new IllegalArgumentException("order not found: " + request.getOrderNo()); + if (order.getVersion() != request.getExpectedVersion()) { + throw new IllegalStateException("stale order version: " + request.getExpectedVersion()); + } + OrderEvent event = parseEvent(request.getEvent()); + TransitionExecution execution = stateMachine.fire(new StateTransitionContext( + request.getEventId(), event, order, request.getOperator(), request.getAttributes())); + TransitionCommitResult result = store.transit(order, execution.getPreviousVersion(), + history(execution), outbox(execution)); + if (!result.isDuplicateEvent()) { + enqueueCompensation(execution); + postTransitionExecutor.execute(stateMachine, execution); + } + BookOrderResponse response = response(result.getOrder()); + resultCache.put(result.getOrder().getRequestId(), response, CREATE_RESULT_TTL_MILLIS); + return response; + } + + private void enqueueCompensation(TransitionExecution execution) { + StateTransitionContext context = execution.getContext(); + if (context.getEvent() == OrderEvent.CANCEL && !isBlank(context.attribute("pnr"))) { + compensationTaskService.cancelPnr(context.getOrder().getOrderNo(), + value(context.attribute("childOrderNo")), context.attribute("pnr")); + } + if (context.getEvent() == OrderEvent.BOOK_FAILED && !isBlank(context.attribute("paymentNo"))) { + compensationTaskService.refundPayment(context.getOrder().getOrderNo(), + context.attribute("paymentNo")); + } + } + + private static OrderStateHistory history(TransitionExecution execution) { + return new OrderStateHistory(execution.getContext().getEventId(), + execution.getContext().getOrder().getOrderNo(), execution.getTransition().getFrom(), + execution.getTransition().getEvent(), execution.getTransition().getTo(), + execution.getContext().getOperator(), System.currentTimeMillis()); + } + + private static OrderOutboxMessage outbox(TransitionExecution execution) { + BookOrder order = execution.getContext().getOrder(); + return new OrderOutboxMessage(execution.getContext().getEventId(), order.getOrderNo(), + execution.getContext().getEvent(), order.getState(), order.getVersion()); + } + + private static void validate(OrderEventRequest request) { + if (request == null || isBlank(request.getEventId()) || isBlank(request.getOrderNo()) + || isBlank(request.getEvent()) || isBlank(request.getOperator())) { + throw new IllegalArgumentException("eventId, orderNo, event and operator are required"); + } + } + + private static OrderEvent parseEvent(String event) { + try { return OrderEvent.valueOf(event.trim().toUpperCase()); } + catch (IllegalArgumentException failure) { + throw new IllegalArgumentException("unknown order event: " + event, failure); + } + } + + private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } + private static String value(String value) { return value == null ? "" : value; } + private static BookOrderResponse response(BookOrder order) { + return new BookOrderResponse(order.getOrderNo(), order.getState().name(), order.getVersion()); + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderReconciler.java b/architecture/src/main/java/com/arch/policy/book/application/OrderReconciler.java new file mode 100644 index 0000000..2420f70 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/OrderReconciler.java @@ -0,0 +1,5 @@ +package com.arch.policy.book.application; + +public interface OrderReconciler { + void reconcile(String orderNo); +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTask.java b/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTask.java new file mode 100644 index 0000000..de6d653 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTask.java @@ -0,0 +1,19 @@ +package com.arch.policy.book.application; + +public final class OrderWorkflowTask { + private final String uniqueKey; + private final String orderNo; + private final TaskType type; + + public OrderWorkflowTask(String uniqueKey, String orderNo, TaskType type) { + this.uniqueKey = uniqueKey; + this.orderNo = orderNo; + this.type = type; + } + + public String getUniqueKey() { return uniqueKey; } + public String getOrderNo() { return orderNo; } + public TaskType getType() { return type; } + + public enum TaskType { WAIT_PAYMENT, MANUAL_GDS_VALIDATION } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTaskStore.java b/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTaskStore.java new file mode 100644 index 0000000..88fb2d5 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTaskStore.java @@ -0,0 +1,5 @@ +package com.arch.policy.book.application; + +public interface OrderWorkflowTaskStore { + void saveIfAbsent(OrderWorkflowTask task); +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/PaymentGateway.java b/architecture/src/main/java/com/arch/policy/book/application/PaymentGateway.java new file mode 100644 index 0000000..6c828b2 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/PaymentGateway.java @@ -0,0 +1,5 @@ +package com.arch.policy.book.application; + +public interface PaymentGateway { + void refund(String orderNo, String paymentNo); +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/PromotionStockGateway.java b/architecture/src/main/java/com/arch/policy/book/application/PromotionStockGateway.java new file mode 100644 index 0000000..23a9132 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/PromotionStockGateway.java @@ -0,0 +1,5 @@ +package com.arch.policy.book.application; + +public interface PromotionStockGateway { + void returnStock(String orderNo, String promotionId); +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/RecoveryTask.java b/architecture/src/main/java/com/arch/policy/book/application/RecoveryTask.java new file mode 100644 index 0000000..0989144 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/RecoveryTask.java @@ -0,0 +1,66 @@ +package com.arch.policy.book.application; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public final class RecoveryTask { + private final String uniqueKey; + private final RecoveryType type; + private final String orderNo; + private final Map parameters; + private RecoveryStatus status; + private int attempts; + private long nextAttemptAtMillis; + private String lastError; + + public RecoveryTask(String uniqueKey, RecoveryType type, String orderNo, + Map parameters) { + this.uniqueKey = uniqueKey; + this.type = type; + this.orderNo = orderNo; + this.parameters = parameters == null ? Collections.emptyMap() + : Collections.unmodifiableMap(new HashMap(parameters)); + this.status = RecoveryStatus.PENDING; + } + + public RecoveryTask copy() { + RecoveryTask copy = new RecoveryTask(uniqueKey, type, orderNo, parameters); + copy.status = status; + copy.attempts = attempts; + copy.nextAttemptAtMillis = nextAttemptAtMillis; + copy.lastError = lastError; + return copy; + } + + public void markSucceeded() { status = RecoveryStatus.SUCCEEDED; lastError = null; } + public void markRetry(long nextAttemptAtMillis, String error) { + attempts++; + status = RecoveryStatus.RETRYING; + this.nextAttemptAtMillis = nextAttemptAtMillis; + lastError = error; + } + public void markManualRequired(String error) { + attempts++; + status = RecoveryStatus.MANUAL_REQUIRED; + lastError = error; + } + + public String getUniqueKey() { return uniqueKey; } + public RecoveryType getType() { return type; } + public String getOrderNo() { return orderNo; } + public Map getParameters() { return parameters; } + public RecoveryStatus getStatus() { return status; } + public int getAttempts() { return attempts; } + public long getNextAttemptAtMillis() { return nextAttemptAtMillis; } + public String getLastError() { return lastError; } + + public enum RecoveryType { + RETURN_PROMOTION_STOCK, + CANCEL_PNR, + REFUND_PAYMENT, + VALIDATE_GDS_BOOKING + } + + public enum RecoveryStatus { PENDING, RETRYING, SUCCEEDED, MANUAL_REQUIRED } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskProcessor.java b/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskProcessor.java new file mode 100644 index 0000000..c010fda --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskProcessor.java @@ -0,0 +1,67 @@ +package com.arch.policy.book.application; + +import java.util.List; + +public final class RecoveryTaskProcessor { + private static final int MAX_ATTEMPTS = 5; + private final RecoveryTaskStore taskStore; + private final PromotionStockGateway promotionStockGateway; + private final GdsBookingGateway gdsGateway; + private final PaymentGateway paymentGateway; + private final OrderReconciler reconciliationService; + + public RecoveryTaskProcessor(RecoveryTaskStore taskStore, + PromotionStockGateway promotionStockGateway, + GdsBookingGateway gdsGateway, + PaymentGateway paymentGateway, + OrderReconciler reconciliationService) { + this.taskStore = taskStore; + this.promotionStockGateway = promotionStockGateway; + this.gdsGateway = gdsGateway; + this.paymentGateway = paymentGateway; + this.reconciliationService = reconciliationService; + } + + public void processBatch(long nowMillis, int limit) { + List tasks = taskStore.findExecutable(nowMillis, limit); + for (RecoveryTask task : tasks) process(task, nowMillis); + } + + private void process(RecoveryTask task, long nowMillis) { + try { + execute(task); + task.markSucceeded(); + } catch (RuntimeException failure) { + if (task.getAttempts() + 1 >= MAX_ATTEMPTS) { + task.markManualRequired(failure.getMessage()); + } else { + task.markRetry(nowMillis + retryDelayMillis(task.getAttempts()), failure.getMessage()); + } + } + taskStore.save(task); + } + + private void execute(RecoveryTask task) { + switch (task.getType()) { + case RETURN_PROMOTION_STOCK: + promotionStockGateway.returnStock(task.getOrderNo(), task.getParameters().get("promotionId")); + return; + case CANCEL_PNR: + gdsGateway.cancelPnr(task.getOrderNo(), task.getParameters().get("childOrderNo"), + task.getParameters().get("pnr")); + return; + case REFUND_PAYMENT: + paymentGateway.refund(task.getOrderNo(), task.getParameters().get("paymentNo")); + return; + case VALIDATE_GDS_BOOKING: + reconciliationService.reconcile(task.getOrderNo()); + return; + default: + throw new IllegalStateException("unsupported recovery type: " + task.getType()); + } + } + + private static long retryDelayMillis(int attempts) { + return Math.min(60_000L, 1_000L << Math.min(attempts, 6)); + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskStore.java b/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskStore.java new file mode 100644 index 0000000..b8ac806 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskStore.java @@ -0,0 +1,11 @@ +package com.arch.policy.book.application; + +import java.util.List; + +public interface RecoveryTaskStore { + RecoveryTask saveIfAbsent(RecoveryTask task); + + List findExecutable(long nowMillis, int limit); + + void save(RecoveryTask task); +} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java b/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java index 3daa8f3..6507001 100644 --- a/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java +++ b/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java @@ -7,17 +7,19 @@ public final class BookOrder { private final String requestId; private final String customerId; private final String productId; + private final String promotionId; private final int quantity; private final BigDecimal amount; private OrderState state; private long version; private BookOrder(String orderNo, String requestId, String customerId, String productId, - int quantity, BigDecimal amount, OrderState state, long version) { + String promotionId, int quantity, BigDecimal amount, OrderState state, long version) { this.orderNo = orderNo; this.requestId = requestId; this.customerId = customerId; this.productId = productId; + this.promotionId = promotionId; this.quantity = quantity; this.amount = amount; this.state = state; @@ -25,13 +27,14 @@ private BookOrder(String orderNo, String requestId, String customerId, String pr } public static BookOrder create(String orderNo, String requestId, String customerId, - String productId, int quantity, BigDecimal amount) { - return new BookOrder(orderNo, requestId, customerId, productId, quantity, amount, + String productId, String promotionId, int quantity, BigDecimal amount) { + return new BookOrder(orderNo, requestId, customerId, productId, promotionId, quantity, amount, OrderState.CREATE, 0L); } public BookOrder copy() { - return new BookOrder(orderNo, requestId, customerId, productId, quantity, amount, state, version); + return new BookOrder(orderNo, requestId, customerId, productId, promotionId, + quantity, amount, state, version); } void applyState(OrderState target) { @@ -43,6 +46,7 @@ void applyState(OrderState target) { public String getRequestId() { return requestId; } public String getCustomerId() { return customerId; } public String getProductId() { return productId; } + public String getPromotionId() { return promotionId; } public int getQuantity() { return quantity; } public BigDecimal getAmount() { return amount; } public OrderState getState() { return state; } diff --git a/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java b/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java index bab4e01..2d51f92 100644 --- a/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java +++ b/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java @@ -10,6 +10,7 @@ public static List definitions() { List transitions = new ArrayList(); add(transitions, OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY); add(transitions, OrderState.CREATE, OrderEvent.CREATE_FAILED, OrderState.CREATE_FAIL); + add(transitions, OrderState.CREATE, OrderEvent.START_VALIDATE, OrderState.VALIDATING); add(transitions, OrderState.CREATE, OrderEvent.CANCEL, OrderState.CANCEL); transitions.add(OrderTransition .from(OrderState.WAIT_PAY, OrderEvent.PAY_SUCCEEDED, OrderState.BOOKING) @@ -32,6 +33,12 @@ public static List definitions() { .guard(new RequiredAttributeGuard("refundNo")).build()); add(transitions, OrderState.VALIDATING, OrderEvent.VALIDATE_SUCCEEDED, OrderState.BOOKED); add(transitions, OrderState.VALIDATING, OrderEvent.VALIDATE_FAILED, OrderState.VALIDATE_FAIL); + transitions.add(OrderTransition + .from(OrderState.VALIDATING, OrderEvent.GDS_BOOKING_CONFIRMED, OrderState.WAIT_PAY) + .guard(new RequiredAttributeGuard("bookingReference")).build()); + transitions.add(OrderTransition + .from(OrderState.VALIDATING, OrderEvent.GDS_BOOKING_REJECTED, OrderState.CREATE_FAIL) + .guard(new RequiredAttributeGuard("failureCode")).build()); add(transitions, OrderState.VALIDATE_FAIL, OrderEvent.VALIDATE_SUCCEEDED, OrderState.BOOKED); add(transitions, OrderState.VALIDATE_FAIL, OrderEvent.CANCEL, OrderState.CANCEL); add(transitions, OrderState.CREATE_FAIL, OrderEvent.CANCEL, OrderState.CANCEL); diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java index a32a008..9bd36b7 100644 --- a/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java +++ b/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java @@ -9,6 +9,8 @@ public enum OrderEvent { START_VALIDATE, VALIDATE_SUCCEEDED, VALIDATE_FAILED, + GDS_BOOKING_CONFIRMED, + GDS_BOOKING_REJECTED, CANCEL, REFUND_SUCCEEDED, DELETE diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/cache/DefaultCreateOrderResultCache.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/cache/DefaultCreateOrderResultCache.java new file mode 100644 index 0000000..fd6bf92 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/cache/DefaultCreateOrderResultCache.java @@ -0,0 +1,54 @@ +package com.arch.policy.book.infrastructure.cache; + +import com.arch.policy.book.application.CreateOrderResultCache; +import com.arch.policy.common.book.BookOrderResponse; +import com.arch.policy.common.redis.RedisClient; +import com.arch.policy.common.redis.RedisClientException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.locks.LockSupport; + +public final class DefaultCreateOrderResultCache implements CreateOrderResultCache { + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultCreateOrderResultCache.class); + private static final String KEY_PREFIX = "book:create:result:"; + private static final long POLL_INTERVAL_NANOS = 20_000_000L; + private final RedisClient redisClient; + + public DefaultCreateOrderResultCache(RedisClient redisClient) { + this.redisClient = redisClient; + } + + @Override public BookOrderResponse get(String requestId) { + try { + return redisClient.get(key(requestId)); + } catch (RedisClientException redisFailure) { + LOGGER.warn("Create-order result cache unavailable, requestId={}", requestId, + redisFailure); + return null; + } + } + + @Override public void put(String requestId, BookOrderResponse response, long ttlMillis) { + try { + redisClient.set(key(requestId), response, ttlMillis); + } catch (RedisClientException redisFailure) { + // 缓存写失败不回滚已经提交的订单,后续请求仍可在锁内查询数据库回填。 + LOGGER.warn("Failed to cache create-order result, requestId={}", requestId, + redisFailure); + } + } + + @Override public BookOrderResponse await(String requestId, long waitMillis) { + long deadlineNanos = System.nanoTime() + waitMillis * 1_000_000L; + do { + BookOrderResponse response = get(requestId); + if (response != null) return response; + if (Thread.currentThread().isInterrupted()) return null; + LockSupport.parkNanos(POLL_INTERVAL_NANOS); + } while (System.nanoTime() < deadlineNanos); + return get(requestId); + } + + private static String key(String requestId) { return KEY_PREFIX + requestId; } +} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/demo/DemoGdsBookingGateway.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/demo/DemoGdsBookingGateway.java new file mode 100644 index 0000000..d965d60 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/demo/DemoGdsBookingGateway.java @@ -0,0 +1,18 @@ +package com.arch.policy.book.infrastructure.demo; + +import com.arch.policy.book.application.GdsBookingGateway; +import com.arch.policy.book.application.GdsBookingResult; + +public final class DemoGdsBookingGateway implements GdsBookingGateway { + @Override public GdsBookingResult createPnr(String orderNo) { + return GdsBookingResult.success("PNR-" + orderNo.substring(Math.max(0, orderNo.length() - 6))); + } + + @Override public GdsBookingResult queryPnr(String orderNo) { + return createPnr(orderNo); + } + + @Override public void cancelPnr(String orderNo, String childOrderNo, String pnr) { + // Demo adapter: production uses a GDS RPC client with the same idempotency key. + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderCreationOutboxScheduler.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderCreationOutboxScheduler.java new file mode 100644 index 0000000..2094263 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderCreationOutboxScheduler.java @@ -0,0 +1,15 @@ +package com.arch.policy.book.infrastructure.job; + +import com.arch.policy.book.application.OrderCreationOutboxPublisher; +import org.springframework.scheduling.annotation.Scheduled; + +public final class OrderCreationOutboxScheduler { + private final OrderCreationOutboxPublisher publisher; + + public OrderCreationOutboxScheduler(OrderCreationOutboxPublisher publisher) { + this.publisher = publisher; + } + + @Scheduled(fixedDelayString = "${book.creation.outbox-delay-ms:3000}") + public void publish() { publisher.publishPending(100); } +} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderRecoveryScheduler.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderRecoveryScheduler.java new file mode 100644 index 0000000..c0c01ef --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderRecoveryScheduler.java @@ -0,0 +1,13 @@ +package com.arch.policy.book.infrastructure.job; + +import com.arch.policy.book.application.RecoveryTaskProcessor; +import org.springframework.scheduling.annotation.Scheduled; + +public final class OrderRecoveryScheduler { + private final RecoveryTaskProcessor processor; + + public OrderRecoveryScheduler(RecoveryTaskProcessor processor) { this.processor = processor; } + + @Scheduled(fixedDelayString = "${book.recovery.scan-delay-ms:5000}") + public void scan() { processor.processBatch(System.currentTimeMillis(), 100); } +} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java index a76b4af..5895766 100644 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java @@ -1,7 +1,9 @@ package com.arch.policy.book.infrastructure.repository; import com.arch.policy.book.application.BookOrderStore; +import com.arch.policy.book.application.LocalCreateResult; import com.arch.policy.book.application.OrderOutboxMessage; +import com.arch.policy.book.application.OrderCreationOutboxMessage; import com.arch.policy.book.application.OrderStateHistory; import com.arch.policy.book.application.TransitionCommitResult; import com.arch.policy.book.domain.BookOrder; @@ -16,14 +18,18 @@ public final class InMemoryBookOrderStore implements BookOrderStore { private final Map orders = new HashMap(); private final Map requestIndexes = new HashMap(); private final Map eventResults = new HashMap(); + private final Map creationOutbox = + new HashMap(); private final List histories = new ArrayList(); private final List outboxMessages = new ArrayList(); + private int findByRequestIdCalls; @Override public synchronized BookOrder findByOrderNo(String orderNo) { return copy(orders.get(orderNo)); } @Override public synchronized BookOrder findByRequestId(String requestId) { + findByRequestIdCalls++; String orderNo = requestIndexes.get(requestId); return orderNo == null ? null : copy(orders.get(orderNo)); } @@ -32,15 +38,31 @@ public final class InMemoryBookOrderStore implements BookOrderStore { return copy(eventResults.get(eventId)); } - @Override public synchronized TransitionCommitResult create( - BookOrder order, OrderStateHistory history, OrderOutboxMessage outboxMessage) { + @Override public synchronized LocalCreateResult createOrder( + BookOrder order, OrderCreationOutboxMessage outboxMessage) { String existingOrderNo = requestIndexes.get(order.getRequestId()); if (existingOrderNo != null) { - return TransitionCommitResult.duplicate(copy(orders.get(existingOrderNo))); + return LocalCreateResult.duplicate(copy(orders.get(existingOrderNo))); } - persist(order, history, outboxMessage); + orders.put(order.getOrderNo(), order.copy()); requestIndexes.put(order.getRequestId(), order.getOrderNo()); - return TransitionCommitResult.committed(order.copy()); + creationOutbox.put(outboxMessage.getMessageId(), outboxMessage); + return LocalCreateResult.created(order.copy()); + } + + @Override public synchronized List findUnpublishedCreationOutbox( + int limit) { + List result = new ArrayList(); + for (OrderCreationOutboxMessage message : creationOutbox.values()) { + if (result.size() >= limit) break; + if (!message.isPublished()) result.add(message); + } + return result; + } + + @Override public synchronized void markCreationOutboxPublished(String messageId) { + OrderCreationOutboxMessage message = creationOutbox.get(messageId); + if (message != null) message.markPublished(); } @Override public synchronized TransitionCommitResult transit( @@ -66,6 +88,8 @@ public synchronized List historiesOf(String orderNo) { return Collections.unmodifiableList(result); } + public synchronized int getFindByRequestIdCalls() { return findByRequestIdCalls; } + public synchronized List unpublishedOutboxMessages() { List result = new ArrayList(); for (OrderOutboxMessage message : outboxMessages) { @@ -83,4 +107,5 @@ private void persist(BookOrder order, OrderStateHistory history, OrderOutboxMess } private static BookOrder copy(BookOrder order) { return order == null ? null : order.copy(); } + } diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryOrderWorkflowTaskStore.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryOrderWorkflowTaskStore.java new file mode 100644 index 0000000..25124df --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryOrderWorkflowTaskStore.java @@ -0,0 +1,17 @@ +package com.arch.policy.book.infrastructure.repository; + +import com.arch.policy.book.application.OrderWorkflowTask; +import com.arch.policy.book.application.OrderWorkflowTaskStore; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class InMemoryOrderWorkflowTaskStore implements OrderWorkflowTaskStore { + private final Map tasks = new LinkedHashMap(); + + @Override public synchronized void saveIfAbsent(OrderWorkflowTask task) { + if (!tasks.containsKey(task.getUniqueKey())) tasks.put(task.getUniqueKey(), task); + } + + public synchronized int size() { return tasks.size(); } +} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryRecoveryTaskStore.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryRecoveryTaskStore.java new file mode 100644 index 0000000..c50d22b --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryRecoveryTaskStore.java @@ -0,0 +1,45 @@ +package com.arch.policy.book.infrastructure.repository; + +import com.arch.policy.book.application.RecoveryTask; +import com.arch.policy.book.application.RecoveryTaskStore; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class InMemoryRecoveryTaskStore implements RecoveryTaskStore { + private final Map tasks = new LinkedHashMap(); + + @Override public synchronized RecoveryTask saveIfAbsent(RecoveryTask task) { + RecoveryTask existing = tasks.get(task.getUniqueKey()); + if (existing != null) return existing.copy(); + tasks.put(task.getUniqueKey(), task.copy()); + return task.copy(); + } + + @Override public synchronized List findExecutable(long nowMillis, int limit) { + List result = new ArrayList(); + for (RecoveryTask task : tasks.values()) { + if (result.size() >= limit) break; + if ((task.getStatus() == RecoveryTask.RecoveryStatus.PENDING + || task.getStatus() == RecoveryTask.RecoveryStatus.RETRYING) + && task.getNextAttemptAtMillis() <= nowMillis) result.add(task.copy()); + } + return result; + } + + @Override public synchronized void save(RecoveryTask task) { + if (!tasks.containsKey(task.getUniqueKey())) { + throw new IllegalArgumentException("recovery task not found: " + task.getUniqueKey()); + } + tasks.put(task.getUniqueKey(), task.copy()); + } + + public synchronized int size() { return tasks.size(); } + + public synchronized RecoveryTask find(String uniqueKey) { + RecoveryTask task = tasks.get(uniqueKey); + return task == null ? null : task.copy(); + } +} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java index 12625eb..869f952 100644 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java @@ -2,6 +2,7 @@ import com.arch.policy.api.book.BookOrderRpcService; import com.arch.policy.book.application.BookOrderApplicationService; +import com.arch.policy.book.application.OrderEventApplicationService; import com.arch.policy.common.book.BookOrderRequest; import com.arch.policy.common.book.BookOrderResponse; import com.arch.policy.common.book.OrderEventRequest; @@ -10,9 +11,12 @@ @DubboService(version = "1.0.0", timeout = 3000) public final class DubboBookOrderService implements BookOrderRpcService { private final BookOrderApplicationService applicationService; + private final OrderEventApplicationService eventService; - public DubboBookOrderService(BookOrderApplicationService applicationService) { + public DubboBookOrderService(BookOrderApplicationService applicationService, + OrderEventApplicationService eventService) { this.applicationService = applicationService; + this.eventService = eventService; } @Override public BookOrderResponse createOrder(BookOrderRequest request) { @@ -20,6 +24,6 @@ public DubboBookOrderService(BookOrderApplicationService applicationService) { } @Override public BookOrderResponse fireEvent(OrderEventRequest request) { - return applicationService.fireEvent(request); + return eventService.fireEvent(request); } } diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/seata/SeataOrderCreationSaga.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/seata/SeataOrderCreationSaga.java new file mode 100644 index 0000000..1c22b60 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/book/infrastructure/seata/SeataOrderCreationSaga.java @@ -0,0 +1,37 @@ +package com.arch.policy.book.infrastructure.seata; + +import com.arch.policy.book.application.OrderCreationSaga; +import io.seata.saga.engine.StateMachineEngine; +import io.seata.saga.statelang.domain.StateMachineInstance; + +import java.util.HashMap; +import java.util.Map; + +public final class SeataOrderCreationSaga implements OrderCreationSaga { + public static final String STATE_MACHINE_NAME = "BookOrderCreationSaga"; + private final StateMachineEngine stateMachineEngine; + private final String tenantId; + + public SeataOrderCreationSaga(StateMachineEngine stateMachineEngine, String tenantId) { + this.stateMachineEngine = stateMachineEngine; + this.tenantId = tenantId; + } + + /** + * 启动订单创建 Saga:调用 GDS 占编,并根据结果推进订单状态或登记补偿任务。 + */ + @Override public SagaStartResult start(String orderNo, String promotionId) { + // Saga 状态节点通过上下文读取订单号和促销活动 ID,作为服务方法的入参。 + Map context = new HashMap(); + context.put("orderNo", orderNo); + // Seata 表达式和服务参数统一使用空字符串,避免 null 在状态机流转中产生歧义。 + context.put("promotionId", promotionId == null ? "" : promotionId); + + // orderNo 作为业务键标识该订单的 Saga 实例,便于恢复任务关联同一笔订单流程。 + StateMachineInstance instance = stateMachineEngine.startWithBusinessKey( + STATE_MACHINE_NAME, tenantId, orderNo, context); + + // 向上层返回 Saga 实例 ID,以及状态机是否仍处于运行中。 + return new SagaStartResult(instance.getId(), instance.isRunning()); + } +} diff --git a/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java b/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java index d890783..08b6962 100644 --- a/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java +++ b/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java @@ -10,6 +10,7 @@ public final class BookOrderRequest implements Serializable { private String productId; private int quantity; private BigDecimal amount; + private String promotionId; public String getRequestId() { return requestId; } public void setRequestId(String requestId) { this.requestId = requestId; } @@ -21,4 +22,6 @@ public final class BookOrderRequest implements Serializable { public void setQuantity(int quantity) { this.quantity = quantity; } public BigDecimal getAmount() { return amount; } public void setAmount(BigDecimal amount) { this.amount = amount; } + public String getPromotionId() { return promotionId; } + public void setPromotionId(String promotionId) { this.promotionId = promotionId; } } diff --git a/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java b/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java index 71716c5..3dfe543 100644 --- a/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java +++ b/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java @@ -2,35 +2,132 @@ import com.arch.policy.book.application.BookOrderApplicationService; import com.arch.policy.book.application.BookOrderStore; +import com.arch.policy.book.application.CompensationTaskService; +import com.arch.policy.book.application.CreateOrderResultCache; +import com.arch.policy.book.application.DirectOrderCreationSaga; +import com.arch.policy.book.application.GdsBookingGateway; +import com.arch.policy.book.application.GdsReconciliationService; +import com.arch.policy.book.application.OrderCreationSaga; +import com.arch.policy.book.application.OrderCreationOutboxPublisher; +import com.arch.policy.book.application.OrderCreationSagaStateServices; +import com.arch.policy.book.application.OrderEventApplicationService; +import com.arch.policy.book.application.OrderWorkflowTaskStore; +import com.arch.policy.book.application.PaymentGateway; import com.arch.policy.book.application.PostTransitionExecutor; +import com.arch.policy.book.application.PromotionStockGateway; +import com.arch.policy.book.application.RecoveryTaskProcessor; +import com.arch.policy.book.application.RecoveryTaskStore; import com.arch.policy.book.application.RetryablePostTransitionExecutor; import com.arch.policy.book.domain.DefaultOrderTransitions; import com.arch.policy.book.domain.OrderStateMachine; +import com.arch.policy.book.infrastructure.demo.DemoGdsBookingGateway; +import com.arch.policy.book.infrastructure.job.OrderRecoveryScheduler; +import com.arch.policy.book.infrastructure.job.OrderCreationOutboxScheduler; import com.arch.policy.book.infrastructure.repository.InMemoryBookOrderStore; import com.arch.policy.book.infrastructure.repository.InMemoryFailedPostActionStore; +import com.arch.policy.book.infrastructure.repository.InMemoryOrderWorkflowTaskStore; +import com.arch.policy.book.infrastructure.repository.InMemoryRecoveryTaskStore; +import com.arch.policy.book.infrastructure.cache.DefaultCreateOrderResultCache; +import com.arch.policy.common.infrastructure.redis.RedissonDistributedLock; +import com.arch.policy.common.infrastructure.redis.RedissonRedisClient; +import com.arch.policy.common.lock.DistributedLock; +import com.arch.policy.common.redis.RedisClient; +import com.arch.policy.book.infrastructure.seata.SeataOrderCreationSaga; +import io.seata.saga.engine.StateMachineEngine; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; @Configuration +@EnableScheduling public class BookOrderConfiguration { @Bean public OrderStateMachine orderStateMachine() { return new OrderStateMachine(DefaultOrderTransitions.definitions()); } @Bean public BookOrderStore bookOrderStore() { return new InMemoryBookOrderStore(); } - + @Bean public DistributedLock distributedLock(RedissonClient redisson) { + return new RedissonDistributedLock(redisson); + } + @Bean public RedisClient redisClient(RedissonClient redisson) { + return new RedissonRedisClient(redisson); + } + @Bean public CreateOrderResultCache createOrderResultCache(RedisClient redisClient) { + return new DefaultCreateOrderResultCache(redisClient); + } + @Bean public RecoveryTaskStore recoveryTaskStore() { return new InMemoryRecoveryTaskStore(); } + @Bean public OrderWorkflowTaskStore orderWorkflowTaskStore() { + return new InMemoryOrderWorkflowTaskStore(); + } @Bean public InMemoryFailedPostActionStore failedPostActionStore() { return new InMemoryFailedPostActionStore(); } - @Bean public PostTransitionExecutor postTransitionExecutor( InMemoryFailedPostActionStore failedPostActionStore) { return new RetryablePostTransitionExecutor(failedPostActionStore); } - - @Bean public BookOrderApplicationService bookOrderApplicationService( + @Bean public OrderEventApplicationService orderEventApplicationService( BookOrderStore store, OrderStateMachine stateMachine, - PostTransitionExecutor postTransitionExecutor) { - return new BookOrderApplicationService(store, stateMachine, postTransitionExecutor); + PostTransitionExecutor postTransitionExecutor, + CompensationTaskService compensationTaskService, + CreateOrderResultCache resultCache) { + return new OrderEventApplicationService(store, stateMachine, postTransitionExecutor, + compensationTaskService, resultCache); + } + @Bean public GdsBookingGateway gdsBookingGateway() { return new DemoGdsBookingGateway(); } + @Bean public PromotionStockGateway promotionStockGateway() { + return (orderNo, promotionId) -> { }; + } + @Bean public PaymentGateway paymentGateway() { return (orderNo, paymentNo) -> { }; } + @Bean public CompensationTaskService compensationTaskService(RecoveryTaskStore store) { + return new CompensationTaskService(store); + } + @Bean(name = "orderCreationSagaStateServices") + public OrderCreationSagaStateServices orderCreationSagaStateServices( + BookOrderStore store, GdsBookingGateway gdsGateway, + OrderEventApplicationService eventService, CompensationTaskService compensationService, + OrderWorkflowTaskStore workflowTaskStore) { + return new OrderCreationSagaStateServices(store, gdsGateway, eventService, + compensationService, workflowTaskStore); + } + @Bean public OrderCreationSaga orderCreationSaga( + ObjectProvider engineProvider, + OrderCreationSagaStateServices services, + @Value("${book.seata.tenant-id:book}") String tenantId) { + StateMachineEngine engine = engineProvider.getIfAvailable(); + return engine == null ? new DirectOrderCreationSaga(services) + : new SeataOrderCreationSaga(engine, tenantId); + } + @Bean public BookOrderApplicationService bookOrderApplicationService( + BookOrderStore store, OrderCreationOutboxPublisher outboxPublisher, + DistributedLock distributedLock, + CreateOrderResultCache resultCache) { + return new BookOrderApplicationService(store, outboxPublisher, distributedLock, + resultCache); + } + @Bean public OrderCreationOutboxPublisher orderCreationOutboxPublisher( + BookOrderStore store, OrderCreationSaga saga) { + return new OrderCreationOutboxPublisher(store, saga); + } + @Bean public OrderCreationOutboxScheduler orderCreationOutboxScheduler( + OrderCreationOutboxPublisher publisher) { + return new OrderCreationOutboxScheduler(publisher); + } + @Bean public GdsReconciliationService gdsReconciliationService( + GdsBookingGateway gdsGateway, OrderCreationSagaStateServices sagaStates) { + return new GdsReconciliationService(gdsGateway, sagaStates); + } + @Bean public RecoveryTaskProcessor recoveryTaskProcessor( + RecoveryTaskStore taskStore, PromotionStockGateway promotionGateway, + GdsBookingGateway gdsGateway, PaymentGateway paymentGateway, + GdsReconciliationService reconciliationService) { + return new RecoveryTaskProcessor(taskStore, promotionGateway, gdsGateway, + paymentGateway, reconciliationService); + } + @Bean public OrderRecoveryScheduler orderRecoveryScheduler(RecoveryTaskProcessor processor) { + return new OrderRecoveryScheduler(processor); } } diff --git a/architecture/src/main/java/com/arch/policy/common/config/RedissonConfiguration.java b/architecture/src/main/java/com/arch/policy/common/config/RedissonConfiguration.java new file mode 100644 index 0000000..5fb5cc6 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/config/RedissonConfiguration.java @@ -0,0 +1,28 @@ +package com.arch.policy.common.config; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.SingleServerConfig; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfiguration { + @Bean(destroyMethod = "shutdown") + @ConditionalOnMissingBean(RedissonClient.class) + public RedissonClient redissonClient( + @Value("${spring.redis.host:localhost}") String host, + @Value("${spring.redis.port:6379}") int port, + @Value("${spring.redis.database:0}") int database, + @Value("${spring.redis.password:}") String password) { + Config config = new Config(); + SingleServerConfig server = config.useSingleServer() + .setAddress("redis://" + host + ":" + port) + .setDatabase(database); + if (password != null && !password.trim().isEmpty()) server.setPassword(password); + return Redisson.create(config); + } +} diff --git a/architecture/src/main/java/com/arch/policy/common/config/SeataSagaConfiguration.java b/architecture/src/main/java/com/arch/policy/common/config/SeataSagaConfiguration.java new file mode 100644 index 0000000..2aa68e8 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/config/SeataSagaConfiguration.java @@ -0,0 +1,54 @@ +package com.arch.policy.common.config; + +import io.seata.saga.engine.StateMachineEngine; +import io.seata.saga.engine.config.DbStateMachineConfig; +import io.seata.saga.engine.impl.ProcessCtrlStateMachineEngine; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +@Configuration +public class SeataSagaConfiguration { + @Bean(destroyMethod = "shutdown") + @ConditionalOnBean(DataSource.class) + public ThreadPoolExecutor seataSagaExecutor() { + return new ThreadPoolExecutor(2, 16, 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue(1000), new ThreadPoolExecutor.CallerRunsPolicy()); + } + + @Bean + @ConditionalOnBean(DataSource.class) + public DbStateMachineConfig dbStateMachineConfig( + DataSource dataSource, ThreadPoolExecutor seataSagaExecutor, + @Value("${spring.application.name:architecture}") String applicationId, + @Value("${book.seata.tx-service-group:book-saga-group}") String txServiceGroup, + @Value("${book.seata.tenant-id:book}") String tenantId) { + DbStateMachineConfig config = new DbStateMachineConfig(); + config.setDataSource(dataSource); + config.setApplicationId(applicationId); + config.setTxServiceGroup(txServiceGroup); + config.setDefaultTenantId(tenantId); + config.setThreadPoolExecutor(seataSagaExecutor); + config.setAutoRegisterResources(true); + config.setResources(new String[] { "classpath*:statelang/book_order_creation_saga.json" }); + config.setSagaJsonParser("jackson"); + config.setSagaBranchRegisterEnable(true); + return config; + } + + @Bean + @ConditionalOnBean(DbStateMachineConfig.class) + @ConditionalOnMissingBean(StateMachineEngine.class) + public StateMachineEngine stateMachineEngine(DbStateMachineConfig config) { + ProcessCtrlStateMachineEngine engine = new ProcessCtrlStateMachineEngine(); + engine.setStateMachineConfig(config); + return engine; + } +} diff --git a/architecture/src/main/java/com/arch/policy/common/infrastructure/redis/RedissonDistributedLock.java b/architecture/src/main/java/com/arch/policy/common/infrastructure/redis/RedissonDistributedLock.java new file mode 100644 index 0000000..810190c --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/infrastructure/redis/RedissonDistributedLock.java @@ -0,0 +1,47 @@ +package com.arch.policy.common.infrastructure.redis; + +import com.arch.policy.common.lock.DistributedLock; +import com.arch.policy.common.lock.DistributedLockUnavailableException; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.redisson.client.RedisException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public final class RedissonDistributedLock implements DistributedLock { + private static final Logger LOGGER = LoggerFactory.getLogger(RedissonDistributedLock.class); + private final RedissonClient redisson; + + public RedissonDistributedLock(RedissonClient redisson) { this.redisson = redisson; } + + @Override public LockHandle tryAcquire(String key, long waitMillis) { + RLock lock = redisson.getLock(key); + try { + // 不指定 leaseTime,交由 Redisson watchdog 在持锁线程存活期间自动续期。 + boolean acquired = lock.tryLock(waitMillis, TimeUnit.MILLISECONDS); + return acquired ? new RedissonLockHandle(lock) : null; + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + return null; + } catch (RedisException redisFailure) { + throw new DistributedLockUnavailableException( + "Redisson lock unavailable, key=" + key, redisFailure); + } + } + + private static final class RedissonLockHandle implements LockHandle { + private final RLock lock; + + private RedissonLockHandle(RLock lock) { this.lock = lock; } + + @Override public void close() { + try { + if (lock.isHeldByCurrentThread()) lock.unlock(); + } catch (RedisException redisFailure) { + LOGGER.warn("Failed to release Redisson lock, key={}", lock.getName(), redisFailure); + } + } + } +} diff --git a/architecture/src/main/java/com/arch/policy/common/infrastructure/redis/RedissonRedisClient.java b/architecture/src/main/java/com/arch/policy/common/infrastructure/redis/RedissonRedisClient.java new file mode 100644 index 0000000..599ce29 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/infrastructure/redis/RedissonRedisClient.java @@ -0,0 +1,30 @@ +package com.arch.policy.common.infrastructure.redis; + +import com.arch.policy.common.redis.RedisClient; +import com.arch.policy.common.redis.RedisClientException; +import org.redisson.api.RedissonClient; +import org.redisson.client.RedisException; + +import java.util.concurrent.TimeUnit; + +public final class RedissonRedisClient implements RedisClient { + private final RedissonClient redisson; + + public RedissonRedisClient(RedissonClient redisson) { this.redisson = redisson; } + + @Override public T get(String key) { + try { + return redisson.getBucket(key).get(); + } catch (RedisException redisFailure) { + throw new RedisClientException("Failed to read Redis key=" + key, redisFailure); + } + } + + @Override public void set(String key, Object value, long ttlMillis) { + try { + redisson.getBucket(key).set(value, ttlMillis, TimeUnit.MILLISECONDS); + } catch (RedisException redisFailure) { + throw new RedisClientException("Failed to write Redis key=" + key, redisFailure); + } + } +} diff --git a/architecture/src/main/java/com/arch/policy/common/lock/DistributedLock.java b/architecture/src/main/java/com/arch/policy/common/lock/DistributedLock.java new file mode 100644 index 0000000..dbd5277 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/lock/DistributedLock.java @@ -0,0 +1,14 @@ +package com.arch.policy.common.lock; + +public interface DistributedLock { + /** + * 尝试获取指定 key 对应的分布式锁。 + * + * @return 锁句柄;等待超时返回 null + */ + LockHandle tryAcquire(String key, long waitMillis); + + interface LockHandle extends AutoCloseable { + @Override void close(); + } +} diff --git a/architecture/src/main/java/com/arch/policy/common/lock/DistributedLockUnavailableException.java b/architecture/src/main/java/com/arch/policy/common/lock/DistributedLockUnavailableException.java new file mode 100644 index 0000000..1e8828f --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/lock/DistributedLockUnavailableException.java @@ -0,0 +1,7 @@ +package com.arch.policy.common.lock; + +public final class DistributedLockUnavailableException extends RuntimeException { + public DistributedLockUnavailableException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/architecture/src/main/java/com/arch/policy/common/redis/RedisClient.java b/architecture/src/main/java/com/arch/policy/common/redis/RedisClient.java new file mode 100644 index 0000000..a8a2f88 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/redis/RedisClient.java @@ -0,0 +1,7 @@ +package com.arch.policy.common.redis; + +public interface RedisClient { + T get(String key); + + void set(String key, Object value, long ttlMillis); +} diff --git a/architecture/src/main/java/com/arch/policy/common/redis/RedisClientException.java b/architecture/src/main/java/com/arch/policy/common/redis/RedisClientException.java new file mode 100644 index 0000000..15dfdcf --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/common/redis/RedisClientException.java @@ -0,0 +1,7 @@ +package com.arch.policy.common.redis; + +public final class RedisClientException extends RuntimeException { + public RedisClientException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/architecture/src/main/resources/application.yml b/architecture/src/main/resources/application.yml index c30aadb..02c14bb 100644 --- a/architecture/src/main/resources/application.yml +++ b/architecture/src/main/resources/application.yml @@ -20,6 +20,15 @@ dubbo: name: dubbo port: 20880 +book: + seata: + tenant-id: book + tx-service-group: book-saga-group + recovery: + scan-delay-ms: 5000 + creation: + scan-delay-ms: 3000 + policy: search: redis-ttl-seconds: 60 diff --git a/architecture/src/main/resources/db/book_order_schema.sql b/architecture/src/main/resources/db/book_order_schema.sql new file mode 100644 index 0000000..4de28ff --- /dev/null +++ b/architecture/src/main/resources/db/book_order_schema.sql @@ -0,0 +1,37 @@ +CREATE TABLE book_order_recovery_task ( + task_id BIGINT PRIMARY KEY AUTO_INCREMENT, + unique_key VARCHAR(256) NOT NULL, + order_no VARCHAR(64) NOT NULL, + child_order_no VARCHAR(64) NOT NULL DEFAULT '', + pnr VARCHAR(32) NOT NULL DEFAULT '', + task_type VARCHAR(32) NOT NULL, + task_status VARCHAR(32) NOT NULL, + attempts INT NOT NULL DEFAULT 0, + next_attempt_at TIMESTAMP NULL, + last_error VARCHAR(1024), + UNIQUE KEY uk_recovery_business (unique_key), + UNIQUE KEY uk_cancel_pnr (order_no, child_order_no, pnr, task_type) +); + +CREATE TABLE book_order_state_history ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + event_id VARCHAR(128) NOT NULL, + order_no VARCHAR(64) NOT NULL, + from_state VARCHAR(32) NOT NULL, + event_type VARCHAR(64) NOT NULL, + to_state VARCHAR(32) NOT NULL, + operator VARCHAR(64) NOT NULL, + occurred_at TIMESTAMP NOT NULL, + UNIQUE KEY uk_order_event (event_id) +); + +CREATE TABLE book_order_outbox ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + event_id VARCHAR(128) NOT NULL, + order_no VARCHAR(64) NOT NULL, + event_type VARCHAR(64) NOT NULL, + payload TEXT NOT NULL, + published TINYINT NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL, + UNIQUE KEY uk_outbox_event (event_id) +); diff --git a/architecture/src/main/resources/statelang/book_order_creation_saga.json b/architecture/src/main/resources/statelang/book_order_creation_saga.json new file mode 100644 index 0000000..c6284bd --- /dev/null +++ b/architecture/src/main/resources/statelang/book_order_creation_saga.json @@ -0,0 +1,54 @@ +{ + "Name": "BookOrderCreationSaga", + "Comment": "本地订单CREATE提交后执行GDS占编,并按SUCCESS/FAIL/UNKNOWN分流", + "Version": "1.0.0", + "StartState": "ReservePnr", + "States": { + "ReservePnr": { + "Type": "ServiceTask", + "ServiceName": "orderCreationSagaStateServices", + "ServiceMethod": "reservePnr", + "Input": ["$.[orderNo]"], + "Output": {"gdsResult": "$#root"}, + "CompensateState": "CancelReservedPnr", + "Catch": [{"Exceptions": ["java.lang.Throwable"], "Next": "MarkUnknown"}], + "Next": "RouteGdsResult" + }, + "RouteGdsResult": { + "Type": "Choice", + "Choices": [ + {"Expression": "$.[gdsResult].status.name() == 'SUCCESS'", "Next": "MarkSuccess"}, + {"Expression": "$.[gdsResult].status.name() == 'FAIL'", "Next": "MarkFailure"} + ], + "Default": "MarkUnknown" + }, + "MarkSuccess": { + "Type": "ServiceTask", + "ServiceName": "orderCreationSagaStateServices", + "ServiceMethod": "markCreateSuccess", + "Input": ["$.[orderNo]", "$.[gdsResult].pnr"], + "Next": "Succeed" + }, + "MarkFailure": { + "Type": "ServiceTask", + "ServiceName": "orderCreationSagaStateServices", + "ServiceMethod": "markCreateFailure", + "Input": ["$.[orderNo]", "$.[gdsResult].errorCode", "$.[promotionId]"], + "Next": "Succeed" + }, + "MarkUnknown": { + "Type": "ServiceTask", + "ServiceName": "orderCreationSagaStateServices", + "ServiceMethod": "markCreateUnknown", + "Input": ["$.[orderNo]", "GDS_RESULT_UNKNOWN"], + "Next": "Succeed" + }, + "CancelReservedPnr": { + "Type": "ServiceTask", + "ServiceName": "orderCreationSagaStateServices", + "ServiceMethod": "enqueueCancelPnr", + "Input": ["$.[orderNo]", "", "$.[gdsResult].pnr"] + }, + "Succeed": {"Type": "Succeed"} + } +} diff --git a/architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java b/architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java deleted file mode 100644 index 6dffeaf..0000000 --- a/architecture/src/test/java/com/arch/policy/book/application/BookOrderApplicationServiceTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.DefaultOrderTransitions; -import com.arch.policy.book.domain.OrderStateMachine; -import com.arch.policy.book.infrastructure.repository.InMemoryBookOrderStore; -import com.arch.policy.book.infrastructure.repository.InMemoryFailedPostActionStore; -import com.arch.policy.common.book.BookOrderRequest; -import com.arch.policy.common.book.BookOrderResponse; -import com.arch.policy.common.book.OrderEventRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class BookOrderApplicationServiceTest { - private InMemoryBookOrderStore store; - private BookOrderApplicationService service; - - @BeforeEach - void setUp() { - store = new InMemoryBookOrderStore(); - OrderStateMachine stateMachine = new OrderStateMachine(DefaultOrderTransitions.definitions()); - service = new BookOrderApplicationService(store, stateMachine, - new RetryablePostTransitionExecutor(new InMemoryFailedPostActionStore())); - } - - @Test - void createsOrderIdempotentlyAndCommitsHistoryWithOutbox() { - BookOrderRequest request = request("request-1"); - - BookOrderResponse first = service.createOrder(request); - BookOrderResponse repeated = service.createOrder(request); - - assertEquals("WAIT_PAY", first.getState()); - assertEquals(1L, first.getVersion()); - assertEquals(first.getOrderNo(), repeated.getOrderNo()); - assertEquals(1, store.historiesOf(first.getOrderNo()).size()); - assertEquals(1, store.unpublishedOutboxMessages().size()); - } - - @Test - void drivesOrderByBusinessEventsInsteadOfTargetStates() { - BookOrderResponse created = service.createOrder(request("request-2")); - - BookOrderResponse booking = fire(created, "pay-1", "PAY_SUCCEEDED"); - BookOrderResponse booked = fire(booking, "book-1", "BOOK_SUCCEEDED"); - - assertEquals("BOOKED", booked.getState()); - assertEquals(3L, booked.getVersion()); - assertEquals(3, store.historiesOf(booked.getOrderNo()).size()); - assertEquals(3, store.unpublishedOutboxMessages().size()); - } - - @Test - void returnsOriginalResultForDuplicateEventEvenWithStaleVersion() { - BookOrderResponse created = service.createOrder(request("request-3")); - BookOrderResponse booking = fire(created, "pay-2", "PAY_SUCCEEDED"); - - BookOrderResponse duplicate = fire(created, "pay-2", "PAY_SUCCEEDED"); - - assertEquals(booking.getState(), duplicate.getState()); - assertEquals(booking.getVersion(), duplicate.getVersion()); - assertEquals(2, store.historiesOf(created.getOrderNo()).size()); - } - - @Test - void rejectsUnsupportedEventAndConcurrentUpdate() { - BookOrderResponse created = service.createOrder(request("request-4")); - assertThrows(IllegalStateException.class, - () -> fire(created, "book-too-early", "BOOK_SUCCEEDED")); - - fire(created, "pay-3", "PAY_SUCCEEDED"); - assertThrows(IllegalStateException.class, - () -> fire(created, "cancel-stale", "CANCEL")); - } - - private BookOrderResponse fire(BookOrderResponse current, String eventId, String event) { - OrderEventRequest request = new OrderEventRequest(); - request.setEventId(eventId); - request.setOrderNo(current.getOrderNo()); - request.setExpectedVersion(current.getVersion()); - request.setEvent(event); - request.setOperator("test-operator"); - if ("PAY_SUCCEEDED".equals(event)) { - request.setAttributes(Collections.singletonMap("paymentNo", "payment-1")); - } else if ("BOOK_SUCCEEDED".equals(event)) { - request.setAttributes(Collections.singletonMap("bookingReference", "PNR001")); - } - return service.fireEvent(request); - } - - private static BookOrderRequest request(String requestId) { - BookOrderRequest request = new BookOrderRequest(); - request.setRequestId(requestId); - request.setCustomerId("customer-1"); - request.setProductId("policy-1"); - request.setQuantity(1); - request.setAmount(new BigDecimal("100.00")); - return request; - } -} diff --git a/architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java b/architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java deleted file mode 100644 index 892f71f..0000000 --- a/architecture/src/test/java/com/arch/policy/book/application/RetryablePostTransitionExecutorTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.BookOrder; -import com.arch.policy.book.domain.OrderEvent; -import com.arch.policy.book.domain.OrderState; -import com.arch.policy.book.domain.OrderStateMachine; -import com.arch.policy.book.domain.OrderTransition; -import com.arch.policy.book.domain.StateTransitionContext; -import com.arch.policy.book.domain.TransitionExecution; -import com.arch.policy.book.infrastructure.repository.InMemoryFailedPostActionStore; -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class RetryablePostTransitionExecutorTest { - @Test - void recordsPostActionFailureForRetryWithoutRevertingCommittedState() { - OrderTransition transition = OrderTransition - .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY) - .after(context -> { throw new IllegalStateException("notification unavailable"); }) - .build(); - OrderStateMachine stateMachine = new OrderStateMachine(Arrays.asList(transition)); - BookOrder order = BookOrder.create("order-1", "request-1", "customer-1", "product-1", - 1, BigDecimal.ONE); - TransitionExecution execution = stateMachine.fire(new StateTransitionContext( - "event-1", OrderEvent.CREATE_SUCCEEDED, order, "tester", - Collections.emptyMap())); - InMemoryFailedPostActionStore failureStore = new InMemoryFailedPostActionStore(); - - new RetryablePostTransitionExecutor(failureStore).execute(stateMachine, execution); - - assertEquals(OrderState.WAIT_PAY, order.getState()); - assertEquals(1, failureStore.all().size()); - assertEquals("event-1", failureStore.all().get(0).getEventId()); - } -} diff --git a/architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java b/architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java deleted file mode 100644 index 9b17b6f..0000000 --- a/architecture/src/test/java/com/arch/policy/book/domain/OrderStateMachineTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.arch.policy.book.domain; - -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Collections; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class OrderStateMachineTest { - @Test - void resolvesTargetByCurrentStateAndBusinessEvent() { - OrderStateMachine stateMachine = new OrderStateMachine(DefaultOrderTransitions.definitions()); - - assertEquals(OrderState.WAIT_PAY, - stateMachine.targetOf(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED)); - assertEquals(OrderState.BOOKING, - stateMachine.targetOf(OrderState.WAIT_PAY, OrderEvent.PAY_SUCCEEDED)); - assertEquals(OrderState.BOOKED, - stateMachine.targetOf(OrderState.VALIDATE_FAIL, OrderEvent.VALIDATE_SUCCEEDED)); - assertNull(stateMachine.targetOf(OrderState.WAIT_PAY, OrderEvent.BOOK_SUCCEEDED)); - } - - @Test - void executesGuardAndPreActionBeforeMutationAndPostActionAfterCommit() { - final AtomicInteger calls = new AtomicInteger(); - TransitionGuard guard = context -> calls.compareAndSet(0, 1); - TransitionAction preAction = context -> { - assertEquals(OrderState.CREATE, context.getOrder().getState()); - calls.compareAndSet(1, 2); - }; - TransitionAction postAction = context -> { - assertEquals(OrderState.WAIT_PAY, context.getOrder().getState()); - calls.compareAndSet(2, 3); - }; - OrderTransition transition = OrderTransition - .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY) - .guard(guard).before(preAction).after(postAction).build(); - OrderStateMachine stateMachine = new OrderStateMachine(Arrays.asList(transition)); - BookOrder order = order(); - - TransitionExecution execution = stateMachine.fire(context(order, OrderEvent.CREATE_SUCCEEDED)); - assertEquals(2, calls.get()); - assertEquals(OrderState.WAIT_PAY, order.getState()); - - stateMachine.afterCommit(execution); - assertEquals(3, calls.get()); - } - - @Test - void guardFailurePreventsStateMutation() { - OrderTransition transition = OrderTransition - .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY) - .guard(context -> { throw new IllegalStateException("risk rejected"); }) - .build(); - OrderStateMachine stateMachine = new OrderStateMachine(Arrays.asList(transition)); - BookOrder order = order(); - - assertThrows(IllegalStateException.class, - () -> stateMachine.fire(context(order, OrderEvent.CREATE_SUCCEEDED))); - assertEquals(OrderState.CREATE, order.getState()); - assertEquals(0L, order.getVersion()); - } - - @Test - void rejectsDuplicateTransitionRegistration() { - OrderTransition first = OrderTransition - .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY).build(); - OrderTransition duplicate = OrderTransition - .from(OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.CREATE_FAIL).build(); - - assertThrows(IllegalStateException.class, - () -> new OrderStateMachine(Arrays.asList(first, duplicate))); - } - - private static BookOrder order() { - return BookOrder.create("order-1", "request-1", "customer-1", "product-1", - 1, BigDecimal.ONE); - } - - private static StateTransitionContext context(BookOrder order, OrderEvent event) { - return new StateTransitionContext("event-1", event, order, "tester", - Collections.emptyMap()); - } -} diff --git a/architecture/src/test/java/com/arch/policy/search/application/AsyncSearchCoordinatorTest.java b/architecture/src/test/java/com/arch/policy/search/application/AsyncSearchCoordinatorTest.java deleted file mode 100644 index 8117b2a..0000000 --- a/architecture/src/test/java/com/arch/policy/search/application/AsyncSearchCoordinatorTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.arch.policy.search.application; - -import com.arch.policy.common.search.PolicySearchRequest; -import com.arch.policy.common.search.PolicySearchResponse; -import com.arch.policy.common.search.SupplierCallbackRequest; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; - -import java.util.*; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class AsyncSearchCoordinatorTest { - @Test - void waitsForAllSuppliersAndAggregatesAsyncCallbacks() throws Exception { - final MemoryStore store = new MemoryStore(); - final LocalSearchWaiters waiters = new LocalSearchWaiters(); - final ObjectMapper mapper = new ObjectMapper(); - SupplierTaskDispatcher dispatcher = (searchKey, suppliers, request) -> { - new Thread(() -> { - try { - for (String supplier : suppliers) { - SupplierCallbackRequest callback = callback(searchKey, supplier, supplier + "-quote", true); - store.recordCallback(searchKey, supplier, mapper.writeValueAsString(callback), true, 60); - } - waiters.signal(searchKey); - } catch (Exception failure) { throw new RuntimeException(failure); } - }).start(); - }; - AsyncSearchCoordinator coordinator = new AsyncSearchCoordinator(store, dispatcher, waiters, mapper, 60); - PolicySearchRequest request = request(1000, "A", "B"); - - PolicySearchResponse response = coordinator.search(request); - - assertEquals(SearchState.COMPLETED.name(), response.getState()); - assertEquals(2, response.getResults().size()); - } - - @Test - void timeoutReturnsPartialResultsAndRejectsLateCallbacks() throws Exception { - final MemoryStore store = new MemoryStore(); - final ObjectMapper mapper = new ObjectMapper(); - SupplierTaskDispatcher dispatcher = (searchKey, suppliers, request) -> { - try { - SupplierCallbackRequest partial = callback(searchKey, "A", "A-quote", true); - store.recordCallback(searchKey, "A", mapper.writeValueAsString(partial), true, 60); - } catch (Exception failure) { throw new RuntimeException(failure); } - }; - AsyncSearchCoordinator coordinator = new AsyncSearchCoordinator( - store, dispatcher, new LocalSearchWaiters(), mapper, 60); - - PolicySearchResponse response = coordinator.search(request(30, "A", "B")); - - assertEquals(SearchState.TIMED_OUT.name(), response.getState()); - assertEquals(1, response.getResults().size()); - } - - private static PolicySearchRequest request(long timeout, String... suppliers) { - PolicySearchRequest request = new PolicySearchRequest(); - request.setCriteria("demo"); - request.setSupplierIds(Arrays.asList(suppliers)); - request.setTotalTimeoutMillis(timeout); - return request; - } - - private static SupplierCallbackRequest callback(String key, String supplier, String result, boolean done) { - SupplierCallbackRequest request = new SupplierCallbackRequest(); - request.setSearchKey(key); - request.setSupplierId(supplier); - request.setResults(Collections.singletonList(result)); - request.setSearchFinished(done); - return request; - } - - private static final class MemoryStore implements SearchStateStore { - private SearchState state; - private final Set pending = new HashSet(); - private final List results = new ArrayList(); - @Override public synchronized void initialize(String key, Set suppliers, long ttl) { - pending.addAll(suppliers); - state = suppliers.isEmpty() ? SearchState.COMPLETED : SearchState.WAITING; - } - @Override public synchronized SearchState getState(String key) { return state; } - @Override public synchronized List getResultPayloads(String key) { - return new ArrayList(results); - } - @Override public synchronized void recordCallback(String key, String supplier, String payload, - boolean finished, long ttl) { - if (state != SearchState.WAITING) return; - if (!payload.isEmpty()) results.add(payload); - if (finished) pending.remove(supplier); - if (pending.isEmpty()) state = SearchState.COMPLETED; - } - @Override public synchronized void markTimedOut(String key, long ttl) { - if (state == SearchState.WAITING) state = SearchState.TIMED_OUT; - } - } -} diff --git a/architecture/src/test/java/com/arch/policy/search/domain/snapshot/SnapshotLifecycleTest.java b/architecture/src/test/java/com/arch/policy/search/domain/snapshot/SnapshotLifecycleTest.java deleted file mode 100644 index 9a1ceaf..0000000 --- a/architecture/src/test/java/com/arch/policy/search/domain/snapshot/SnapshotLifecycleTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.arch.policy.search.domain.snapshot; - -import com.arch.policy.common.model.MessagePosition; -import com.arch.policy.common.model.PolicyRecord; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; - -import static com.arch.policy.search.domain.snapshot.SnapshotPorts.*; -import static org.junit.jupiter.api.Assertions.*; - -class SnapshotLifecycleTest { - @TempDir Path root; - - @Test - void buildsFullDataReplaysChangesAndValidatesPosition() throws Exception { - SnapshotBuilder builder = builder(false); - - PolicySnapshot snapshot = builder.build("V101"); - - assertEquals(new MessagePosition(12), snapshot.getPosition()); - assertArrayEquals(bytes("new-detail"), snapshot.findDetail(1)); - assertEquals(1, snapshot.findPolicyIds("active").getCardinality()); - assertEquals(0, snapshot.findPolicyIds("draft").getCardinality()); - snapshot.closeAndDelete(); - } - - @Test - void switchWaitsForOldQueriesBeforeClosingAndDeletingOldVersion() throws Exception { - ActiveSnapshotRegistry registry = new ActiveSnapshotRegistry(); - PolicySnapshot old = builder(false).build("V100"); - PolicySnapshot replacement = builder(false).build("V101"); - registry.activate(old); - ActiveSnapshotRegistry.SnapshotLease oldQuery = registry.acquire(); - - registry.activate(replacement); - - assertTrue(Files.exists(old.getDirectory())); - assertArrayEquals(bytes("new-detail"), oldQuery.snapshot().findDetail(1)); - try (ActiveSnapshotRegistry.SnapshotLease newQuery = registry.acquire()) { - assertEquals("V101", newQuery.snapshot().getVersion()); - } - oldQuery.close(); - assertFalse(Files.exists(old.getDirectory())); - assertThrows(IllegalStateException.class, () -> old.findPolicyIds("active")); - registry.close(); - } - - @Test - void failedRuntimeCandidateDoesNotReplaceServingSnapshot() throws Exception { - ActiveSnapshotRegistry registry = new ActiveSnapshotRegistry(); - registry.activate(builder(false).build("V100")); - ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); - PolicySnapshotService service = new PolicySnapshotService(builder(true), registry, executor, 1); - - assertTrue(service.refresh("V101")); - waitUntilBuildFinishes(service); - - assertTrue(service.isReady()); - try (ActiveSnapshotRegistry.SnapshotLease query = registry.acquire()) { - assertEquals("V100", query.snapshot().getVersion()); - } - assertFalse(Files.exists(root.resolve("V101"))); - service.close(); - } - - private SnapshotBuilder builder(final boolean failValidation) { - FullPolicyLoader full = target -> { - target.upsert(new PolicyRecord(1, bytes("old-detail"), Collections.singleton("draft"))); - return new MessagePosition(10); - }; - IncrementalReplayer replay = new IncrementalReplayer() { - @Override public MessagePosition latestPosition() { return new MessagePosition(12); } - @Override public MessagePosition replayInto(PolicySnapshot target, MessagePosition from, - MessagePosition latest) throws Exception { - target.upsert(new PolicyRecord(1, bytes("new-detail"), - Collections.singleton("active"))); - return latest; - } - }; - SnapshotValidator validator = (candidate, position) -> { - new DefaultSnapshotValidator().validate(candidate, position); - if (failValidation) throw new IllegalStateException("invalid candidate"); - }; - SnapshotDirectory directories = new SnapshotDirectory() { - @Override public Path create(String version) throws Exception { - Path path = root.resolve(version); - Files.createDirectories(path); - return path; - } - @Override public void delete(Path directory) throws Exception { - if (!Files.exists(directory)) return; - Files.walk(directory).sorted(Comparator.reverseOrder()).forEach(path -> { - try { Files.deleteIfExists(path); } - catch (Exception failure) { throw new RuntimeException(failure); } - }); - } - }; - return new SnapshotBuilder(full, replay, validator, directories); - } - - private static byte[] bytes(String value) { - return value.getBytes(java.nio.charset.StandardCharsets.UTF_8); - } - - private static void waitUntilBuildFinishes(PolicySnapshotService service) throws Exception { - long deadline = System.currentTimeMillis() + 3000; - while (System.currentTimeMillis() < deadline) { - if (!service.isBuilding()) return; - Thread.sleep(10); - } - fail("candidate build did not finish"); - } -} From 17f1b09d7c2f2e842ea16cc08eaf7aef9c529807 Mon Sep 17 00:00:00 2001 From: coderbruis Date: Wed, 22 Jul 2026 21:59:01 +0800 Subject: [PATCH 20/21] =?UTF-8?q?=E4=B8=8B=E5=8D=95=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E4=B8=8B=E5=8D=95+=E5=BA=93?= =?UTF-8?q?=E5=AD=98=E6=89=A3=E5=87=8F+=E4=B8=89=E6=96=B9=E4=B8=8B?= =?UTF-8?q?=E5=8D=95=EF=BC=8C=E4=BD=BF=E7=94=A8Seata=20saga=E7=AE=A1?= =?UTF-8?q?=E6=8E=A7=E4=BA=8B=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../policy/api/book/BookOrderRpcService.java | 12 -- .../policy/api/order/OrderRpcService.java | 14 ++ .../BookOrderApplicationService.java | 153 ------------------ .../book/application/BookOrderStore.java | 22 --- .../application/CompensationTaskService.java | 51 ------ .../application/CreateOrderResultCache.java | 11 -- .../application/DirectOrderCreationSaga.java | 24 --- .../application/ExternalResultStatus.java | 7 - .../book/application/GdsBookingGateway.java | 9 -- .../book/application/GdsBookingResult.java | 29 ---- .../application/GdsReconciliationService.java | 25 --- .../book/application/LocalCreateResult.java | 24 --- .../OrderCreationOutboxMessage.java | 21 --- .../OrderCreationOutboxPublisher.java | 25 --- .../book/application/OrderCreationSaga.java | 20 --- .../OrderCreationSagaStateServices.java | 86 ---------- .../OrderEventApplicationService.java | 97 ----------- .../book/application/OrderOutboxMessage.java | 30 ---- .../book/application/OrderReconciler.java | 5 - .../book/application/OrderStateHistory.java | 33 ---- .../book/application/OrderWorkflowTask.java | 19 --- .../application/OrderWorkflowTaskStore.java | 5 - .../book/application/PaymentGateway.java | 5 - .../application/PostTransitionExecutor.java | 8 - .../application/PromotionStockGateway.java | 5 - .../policy/book/application/RecoveryTask.java | 66 -------- .../application/RecoveryTaskProcessor.java | 67 -------- .../book/application/RecoveryTaskStore.java | 11 -- .../RetryablePostTransitionExecutor.java | 24 --- .../application/TransitionCommitResult.java | 24 --- .../arch/policy/book/domain/BookOrder.java | 54 ------- .../book/domain/DefaultOrderTransitions.java | 55 ------- .../arch/policy/book/domain/OrderEvent.java | 17 -- .../arch/policy/book/domain/OrderState.java | 26 --- .../policy/book/domain/OrderStateMachine.java | 72 --------- .../policy/book/domain/OrderTransition.java | 61 ------- .../book/domain/RequiredAttributeGuard.java | 16 -- .../book/domain/StateTransitionContext.java | 30 ---- .../policy/book/domain/TransitionAction.java | 5 - .../book/domain/TransitionExecution.java | 18 --- .../policy/book/domain/TransitionGuard.java | 5 - .../cache/DefaultCreateOrderResultCache.java | 54 ------- .../demo/DemoGdsBookingGateway.java | 18 --- .../job/OrderCreationOutboxScheduler.java | 15 -- .../job/OrderRecoveryScheduler.java | 13 -- .../repository/InMemoryBookOrderStore.java | 111 ------------- .../InMemoryFailedPostActionStore.java | 37 ----- .../InMemoryOrderWorkflowTaskStore.java | 17 -- .../repository/InMemoryRecoveryTaskStore.java | 45 ------ .../rpc/DubboBookOrderService.java | 29 ---- .../seata/SeataOrderCreationSaga.java | 37 ----- .../policy/common/book/BookOrderRequest.java | 27 ---- .../policy/common/book/BookOrderResponse.java | 20 --- .../policy/common/book/OrderEventRequest.java | 32 ---- .../common/config/BookOrderConfiguration.java | 133 --------------- .../common/config/SeataSagaConfiguration.java | 6 +- .../policy/order/cache/OrderCacheUtil.java | 47 ++++++ .../policy/order/cache/OrderLockUtil.java | 45 ++++++ .../order/model/InventoryTransaction.java | 23 +++ .../model/InventoryTransactionStatus.java | 25 +++ .../arch/policy/order/model/OrderRequest.java | 65 ++++++++ .../policy/order/model/OrderResponse.java | 30 ++++ .../arch/policy/order/model/OrderStatus.java | 25 +++ .../policy/order/model/SupplierOrder.java | 26 +++ .../order/model/SupplierOrderStatus.java | 28 ++++ .../policy/order/model/ThirdOrderResult.java | 14 ++ .../policy/order/model/ThirdOrderStatus.java | 13 ++ .../arch/policy/order/model/TradeOrder.java | 23 +++ .../service/InventoryTransactionService.java | 80 +++++++++ .../order/service/OrderCreationSaga.java | 9 ++ .../OrderCreationSagaStateServices.java | 74 +++++++++ .../order/service/OrderRecoveryService.java | 67 ++++++++ .../order/service/OrderRpcServiceImpl.java | 116 +++++++++++++ .../service/OrderTransactionService.java | 43 +++++ .../order/service/SeataOrderCreationSaga.java | 31 ++++ .../order/service/SupplierOrderService.java | 29 ++++ .../SupplierOrderTransactionService.java | 52 ++++++ .../statelang/book_order_creation_saga.json | 54 ------- .../statelang/order_creation_saga.json | 68 ++++++++ 79 files changed, 950 insertions(+), 1922 deletions(-) delete mode 100644 architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java create mode 100644 architecture/src/main/java/com/arch/policy/api/order/OrderRpcService.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/CompensationTaskService.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/CreateOrderResultCache.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/DirectOrderCreationSaga.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/ExternalResultStatus.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/GdsBookingGateway.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/GdsBookingResult.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/GdsReconciliationService.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/LocalCreateResult.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxMessage.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxPublisher.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationSaga.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderCreationSagaStateServices.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderEventApplicationService.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderReconciler.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTask.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTaskStore.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/PaymentGateway.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/PromotionStockGateway.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/RecoveryTask.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskProcessor.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskStore.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderState.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/cache/DefaultCreateOrderResultCache.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/demo/DemoGdsBookingGateway.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderCreationOutboxScheduler.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderRecoveryScheduler.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryOrderWorkflowTaskStore.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryRecoveryTaskStore.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java delete mode 100644 architecture/src/main/java/com/arch/policy/book/infrastructure/seata/SeataOrderCreationSaga.java delete mode 100644 architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java delete mode 100644 architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java delete mode 100644 architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java delete mode 100644 architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java create mode 100644 architecture/src/main/java/com/arch/policy/order/cache/OrderCacheUtil.java create mode 100644 architecture/src/main/java/com/arch/policy/order/cache/OrderLockUtil.java create mode 100644 architecture/src/main/java/com/arch/policy/order/model/InventoryTransaction.java create mode 100644 architecture/src/main/java/com/arch/policy/order/model/InventoryTransactionStatus.java create mode 100644 architecture/src/main/java/com/arch/policy/order/model/OrderRequest.java create mode 100644 architecture/src/main/java/com/arch/policy/order/model/OrderResponse.java create mode 100644 architecture/src/main/java/com/arch/policy/order/model/OrderStatus.java create mode 100644 architecture/src/main/java/com/arch/policy/order/model/SupplierOrder.java create mode 100644 architecture/src/main/java/com/arch/policy/order/model/SupplierOrderStatus.java create mode 100644 architecture/src/main/java/com/arch/policy/order/model/ThirdOrderResult.java create mode 100644 architecture/src/main/java/com/arch/policy/order/model/ThirdOrderStatus.java create mode 100644 architecture/src/main/java/com/arch/policy/order/model/TradeOrder.java create mode 100644 architecture/src/main/java/com/arch/policy/order/service/InventoryTransactionService.java create mode 100644 architecture/src/main/java/com/arch/policy/order/service/OrderCreationSaga.java create mode 100644 architecture/src/main/java/com/arch/policy/order/service/OrderCreationSagaStateServices.java create mode 100644 architecture/src/main/java/com/arch/policy/order/service/OrderRecoveryService.java create mode 100644 architecture/src/main/java/com/arch/policy/order/service/OrderRpcServiceImpl.java create mode 100644 architecture/src/main/java/com/arch/policy/order/service/OrderTransactionService.java create mode 100644 architecture/src/main/java/com/arch/policy/order/service/SeataOrderCreationSaga.java create mode 100644 architecture/src/main/java/com/arch/policy/order/service/SupplierOrderService.java create mode 100644 architecture/src/main/java/com/arch/policy/order/service/SupplierOrderTransactionService.java delete mode 100644 architecture/src/main/resources/statelang/book_order_creation_saga.json create mode 100644 architecture/src/main/resources/statelang/order_creation_saga.json diff --git a/architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java b/architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java deleted file mode 100644 index ff75a2c..0000000 --- a/architecture/src/main/java/com/arch/policy/api/book/BookOrderRpcService.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.arch.policy.api.book; - -import com.arch.policy.common.book.BookOrderRequest; -import com.arch.policy.common.book.BookOrderResponse; -import com.arch.policy.common.book.OrderEventRequest; - -public interface BookOrderRpcService { - - BookOrderResponse createOrder(BookOrderRequest request); - - BookOrderResponse fireEvent(OrderEventRequest request); -} diff --git a/architecture/src/main/java/com/arch/policy/api/order/OrderRpcService.java b/architecture/src/main/java/com/arch/policy/api/order/OrderRpcService.java new file mode 100644 index 0000000..2b2c840 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/api/order/OrderRpcService.java @@ -0,0 +1,14 @@ +package com.arch.policy.api.order; + +import com.arch.policy.order.model.OrderRequest; +import com.arch.policy.order.model.OrderResponse; + +/** + * @Author : haiyang.luo + * @Date : 2026/7/22 15:44 + * @Description : + */ +public interface OrderRpcService { + + OrderResponse createOrder(OrderRequest orderRequest); +} diff --git a/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java b/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java deleted file mode 100644 index 6a9694f..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/BookOrderApplicationService.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.BookOrder; -import com.arch.policy.common.book.BookOrderRequest; -import com.arch.policy.common.book.BookOrderResponse; -import com.arch.policy.common.lock.DistributedLock; -import com.arch.policy.common.lock.DistributedLockUnavailableException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.math.BigDecimal; -import java.util.UUID; - -public final class BookOrderApplicationService { - private static final Logger LOGGER = LoggerFactory.getLogger(BookOrderApplicationService.class); - private static final DistributedLock.LockHandle NO_OP_LOCK = () -> { }; - private static final long CREATE_LOCK_WAIT_MILLIS = 300L; - private static final String CREATE_ORDER_LOCK_KEY_PREFIX = "book:create:lock:"; - private static final long CREATE_RESULT_WAIT_MILLIS = 500L; - private static final long CREATE_RESULT_TTL_MILLIS = 600_000L; - private final BookOrderStore store; - private final OrderCreationOutboxPublisher outboxPublisher; - private final DistributedLock distributedLock; - private final CreateOrderResultCache resultCache; - - public BookOrderApplicationService(BookOrderStore store, - OrderCreationOutboxPublisher outboxPublisher, - DistributedLock distributedLock, - CreateOrderResultCache resultCache) { - this.store = store; - this.outboxPublisher = outboxPublisher; - this.distributedLock = distributedLock; - this.resultCache = resultCache; - } - - /** - * 创建订单并在本地事务提交后启动 GDS 占编 Saga。 - * - *

流程边界:

- *
    - *
  1. 使用 requestId 查询 Redis 幂等结果缓存,命中则直接返回。
  2. - *
  3. 获取 requestId 对应的 Redisson 短锁并进行缓存二次检查。
  4. - *
  5. 在同一本地事务中保存 CREATE 状态订单和 Saga 启动 Outbox。
  6. - *
  7. 事务提交后发布 Outbox,使用 orderNo 幂等启动 Seata Saga。
  8. - *
  9. Saga 根据 GDS 的 SUCCESS、FAIL、UNKNOWN 结果分别推进至 - * WAIT_PAY、CREATE_FAIL、VALIDATING。
  10. - *
  11. 重新读取订单并返回 Saga 执行后的最新状态。
  12. - *
- * - *

如果进程在订单提交后、Saga 启动前退出,定时发布器会重新投递未发布的 Outbox; - * Seata 根据持久化的状态机日志恢复已经启动的 Saga。

- * - * @param request 下单请求,requestId 是业务幂等键 - * @return 当前最新订单状态;同步 Saga 通常返回 WAIT_PAY、CREATE_FAIL 或 VALIDATING - * @throws IllegalArgumentException 请求参数不合法时抛出 - */ - public BookOrderResponse createOrder(BookOrderRequest request) { - validate(request); - - // 锁前只访问 Redis,不查询数据库,避免大量重复请求同时穿透到主库。 - BookOrderResponse cached = resultCache.get(request.getRequestId()); - if (cached != null) return cached; - - DistributedLock.LockHandle lock = acquireCreateOrderLock(request.getRequestId()); - if (lock == null) return responseOfCompetingRequest(request.getRequestId()); - - BookOrder order; - OrderCreationOutboxMessage outboxMessage; - try { - // Double check:前一个持锁请求可能已在等待期间完成创建并回填缓存。 - cached = resultCache.get(request.getRequestId()); - if (cached != null) return cached; - - // 只有锁持有者才能访问数据库;用于缓存丢失、过期及 Redis 降级场景。 - BookOrder existing = store.findByRequestId(request.getRequestId()); - if (existing != null) { - BookOrderResponse existingResponse = response(existing); - resultCache.put(request.getRequestId(), existingResponse, CREATE_RESULT_TTL_MILLIS); - return existingResponse; - } - - // 构造下单对象(模拟下单的内容) - order = BookOrder.create(newOrderNo(), request.getRequestId(), request.getCustomerId(), - request.getProductId(), request.getPromotionId(), request.getQuantity(), - request.getAmount()); - - // 数据库 request_id 唯一索引是最终幂等防线。 - outboxMessage = new OrderCreationOutboxMessage(order.getOrderNo(), - order.getPromotionId()); - LocalCreateResult localResult = store.createOrder(order, outboxMessage); - if (localResult.isDuplicateRequest()) { - BookOrderResponse duplicateResponse = response(localResult.getOrder()); - resultCache.put(request.getRequestId(), duplicateResponse, CREATE_RESULT_TTL_MILLIS); - return duplicateResponse; - } - resultCache.put(request.getRequestId(), response(order), CREATE_RESULT_TTL_MILLIS); - } finally { - lock.close(); - } - - // Redis 锁已释放;立即发布一次,崩溃遗漏由定时 Outbox 发布器补发。 - try { - outboxPublisher.publish(outboxMessage); - } catch (RuntimeException publishFailure) { - LOGGER.warn("Creation Outbox will be retried, orderNo={}", order.getOrderNo(), - publishFailure); - } - - // 同步 Saga 可能已经改变订单状态,因此返回前重新读取聚合快照。 - BookOrder latest = store.findByOrderNo(order.getOrderNo()); - BookOrderResponse latestResponse = response(latest == null ? order : latest); - resultCache.put(request.getRequestId(), latestResponse, CREATE_RESULT_TTL_MILLIS); - return latestResponse; - } - - private BookOrderResponse responseOfCompetingRequest(String requestId) { - BookOrderResponse cached = resultCache.await(requestId, CREATE_RESULT_WAIT_MILLIS); - if (cached != null) return cached; - throw new IllegalStateException("create order request is being processed: " + requestId); - } - - private DistributedLock.LockHandle acquireCreateOrderLock(String requestId) { - String key = CREATE_ORDER_LOCK_KEY_PREFIX + requestId; - try { - return distributedLock.tryAcquire(key, CREATE_LOCK_WAIT_MILLIS); - } catch (DistributedLockUnavailableException unavailable) { - // 数据库 request_id 唯一索引仍是最终幂等防线,Redis 故障时允许降级。 - LOGGER.warn("Create-order lock unavailable, fall back to database idempotency, key={}", - key, unavailable); - return NO_OP_LOCK; - } - } - - private static void validate(BookOrderRequest request) { - if (request == null) throw new IllegalArgumentException("request is required"); - if (isBlank(request.getRequestId())) throw new IllegalArgumentException("requestId is required"); - if (isBlank(request.getCustomerId())) throw new IllegalArgumentException("customerId is required"); - if (isBlank(request.getProductId())) throw new IllegalArgumentException("productId is required"); - if (request.getQuantity() <= 0) throw new IllegalArgumentException("quantity must be positive"); - if (request.getAmount() == null || request.getAmount().compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException("amount must not be negative"); - } - } - - private static String newOrderNo() { - return "BO" + UUID.randomUUID().toString().replace("-", "").substring(0, 20).toUpperCase(); - } - - private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } - private static BookOrderResponse response(BookOrder order) { - return new BookOrderResponse(order.getOrderNo(), order.getState().name(), order.getVersion()); - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java b/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java deleted file mode 100644 index cf2bd4e..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/BookOrderStore.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.BookOrder; - -import java.util.List; - -public interface BookOrderStore { - BookOrder findByOrderNo(String orderNo); - - BookOrder findByRequestId(String requestId); - - BookOrder findByEventId(String eventId); - - LocalCreateResult createOrder(BookOrder order, OrderCreationOutboxMessage outboxMessage); - - List findUnpublishedCreationOutbox(int limit); - - void markCreationOutboxPublished(String messageId); - - TransitionCommitResult transit(BookOrder order, long expectedVersion, - OrderStateHistory history, OrderOutboxMessage outboxMessage); -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/CompensationTaskService.java b/architecture/src/main/java/com/arch/policy/book/application/CompensationTaskService.java deleted file mode 100644 index 843c8f8..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/CompensationTaskService.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.arch.policy.book.application; - -import java.util.HashMap; -import java.util.Map; - -public final class CompensationTaskService { - private final RecoveryTaskStore taskStore; - - public CompensationTaskService(RecoveryTaskStore taskStore) { this.taskStore = taskStore; } - - public void returnPromotionStock(String orderNo, String promotionId) { - if (isBlank(promotionId)) return; - enqueue("PROMOTION:" + orderNo + ":" + promotionId, - RecoveryTask.RecoveryType.RETURN_PROMOTION_STOCK, orderNo, - params("promotionId", promotionId)); - } - - public void cancelPnr(String orderNo, String childOrderNo, String pnr) { - if (isBlank(pnr)) return; - Map parameters = params("childOrderNo", childOrderNo); - parameters.put("pnr", pnr); - enqueue("CANCEL_PNR:" + orderNo + ":" + childOrderNo + ":" + pnr, - RecoveryTask.RecoveryType.CANCEL_PNR, orderNo, parameters); - } - - public void refundPayment(String orderNo, String paymentNo) { - if (isBlank(paymentNo)) return; - enqueue("REFUND:" + orderNo + ":" + paymentNo, - RecoveryTask.RecoveryType.REFUND_PAYMENT, orderNo, - params("paymentNo", paymentNo)); - } - - public void validateGdsBooking(String orderNo) { - enqueue("VALIDATE_GDS:" + orderNo, - RecoveryTask.RecoveryType.VALIDATE_GDS_BOOKING, orderNo, - new HashMap()); - } - - private void enqueue(String uniqueKey, RecoveryTask.RecoveryType type, String orderNo, - Map parameters) { - taskStore.saveIfAbsent(new RecoveryTask(uniqueKey, type, orderNo, parameters)); - } - - private static Map params(String name, String value) { - Map params = new HashMap(); - params.put(name, value == null ? "" : value); - return params; - } - - private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/CreateOrderResultCache.java b/architecture/src/main/java/com/arch/policy/book/application/CreateOrderResultCache.java deleted file mode 100644 index db48c56..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/CreateOrderResultCache.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.common.book.BookOrderResponse; - -public interface CreateOrderResultCache { - BookOrderResponse get(String requestId); - - void put(String requestId, BookOrderResponse response, long ttlMillis); - - BookOrderResponse await(String requestId, long waitMillis); -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/DirectOrderCreationSaga.java b/architecture/src/main/java/com/arch/policy/book/application/DirectOrderCreationSaga.java deleted file mode 100644 index cda115b..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/DirectOrderCreationSaga.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.arch.policy.book.application; - -public final class DirectOrderCreationSaga implements OrderCreationSaga { - private final OrderCreationSagaStateServices services; - - public DirectOrderCreationSaga(OrderCreationSagaStateServices services) { this.services = services; } - - @Override public SagaStartResult start(String orderNo, String promotionId) { - GdsBookingResult result; - try { - result = services.reservePnr(orderNo); - } catch (RuntimeException uncertainFailure) { - result = GdsBookingResult.unknown(uncertainFailure.getClass().getSimpleName()); - } - if (result.getStatus() == ExternalResultStatus.SUCCESS) { - services.markCreateSuccess(orderNo, result.getPnr()); - } else if (result.getStatus() == ExternalResultStatus.FAIL) { - services.markCreateFailure(orderNo, result.getErrorCode(), promotionId); - } else { - services.markCreateUnknown(orderNo, result.getErrorCode()); - } - return new SagaStartResult("DIRECT:" + orderNo, false); - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/ExternalResultStatus.java b/architecture/src/main/java/com/arch/policy/book/application/ExternalResultStatus.java deleted file mode 100644 index 099da1a..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/ExternalResultStatus.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.arch.policy.book.application; - -public enum ExternalResultStatus { - SUCCESS, - FAIL, - UNKNOWN -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/GdsBookingGateway.java b/architecture/src/main/java/com/arch/policy/book/application/GdsBookingGateway.java deleted file mode 100644 index cc689f1..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/GdsBookingGateway.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.arch.policy.book.application; - -public interface GdsBookingGateway { - GdsBookingResult createPnr(String orderNo); - - GdsBookingResult queryPnr(String orderNo); - - void cancelPnr(String orderNo, String childOrderNo, String pnr); -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/GdsBookingResult.java b/architecture/src/main/java/com/arch/policy/book/application/GdsBookingResult.java deleted file mode 100644 index 72b6d20..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/GdsBookingResult.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.arch.policy.book.application; - -public final class GdsBookingResult { - private final ExternalResultStatus status; - private final String pnr; - private final String errorCode; - - public GdsBookingResult(ExternalResultStatus status, String pnr, String errorCode) { - this.status = status; - this.pnr = pnr; - this.errorCode = errorCode; - } - - public static GdsBookingResult success(String pnr) { - return new GdsBookingResult(ExternalResultStatus.SUCCESS, pnr, null); - } - - public static GdsBookingResult fail(String errorCode) { - return new GdsBookingResult(ExternalResultStatus.FAIL, null, errorCode); - } - - public static GdsBookingResult unknown(String errorCode) { - return new GdsBookingResult(ExternalResultStatus.UNKNOWN, null, errorCode); - } - - public ExternalResultStatus getStatus() { return status; } - public String getPnr() { return pnr; } - public String getErrorCode() { return errorCode; } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/GdsReconciliationService.java b/architecture/src/main/java/com/arch/policy/book/application/GdsReconciliationService.java deleted file mode 100644 index 8527498..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/GdsReconciliationService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.arch.policy.book.application; - -public final class GdsReconciliationService implements OrderReconciler { - private final GdsBookingGateway gdsGateway; - private final OrderCreationSagaStateServices sagaStates; - - public GdsReconciliationService(GdsBookingGateway gdsGateway, - OrderCreationSagaStateServices sagaStates) { - this.gdsGateway = gdsGateway; - this.sagaStates = sagaStates; - } - - @Override public void reconcile(String orderNo) { - GdsBookingResult result = gdsGateway.queryPnr(orderNo); - if (result.getStatus() == ExternalResultStatus.SUCCESS) { - sagaStates.markGdsValidationSuccess(orderNo, result.getPnr()); - return; - } - if (result.getStatus() == ExternalResultStatus.FAIL) { - sagaStates.markGdsValidationFailure(orderNo, result.getErrorCode()); - return; - } - throw new IllegalStateException("GDS result is still unknown"); - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/LocalCreateResult.java b/architecture/src/main/java/com/arch/policy/book/application/LocalCreateResult.java deleted file mode 100644 index dbeaadd..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/LocalCreateResult.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.BookOrder; - -public final class LocalCreateResult { - private final BookOrder order; - private final boolean duplicateRequest; - - private LocalCreateResult(BookOrder order, boolean duplicateRequest) { - this.order = order; - this.duplicateRequest = duplicateRequest; - } - - public static LocalCreateResult created(BookOrder order) { - return new LocalCreateResult(order, false); - } - - public static LocalCreateResult duplicate(BookOrder order) { - return new LocalCreateResult(order, true); - } - - public BookOrder getOrder() { return order; } - public boolean isDuplicateRequest() { return duplicateRequest; } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxMessage.java b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxMessage.java deleted file mode 100644 index cd52227..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxMessage.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.arch.policy.book.application; - -/** 与订单在同一本地事务中保存,用于可靠启动创建 Saga。 */ -public final class OrderCreationOutboxMessage { - private final String messageId; - private final String orderNo; - private final String promotionId; - private boolean published; - - public OrderCreationOutboxMessage(String orderNo, String promotionId) { - this.messageId = "START_ORDER_CREATION:" + orderNo; - this.orderNo = orderNo; - this.promotionId = promotionId; - } - - public String getMessageId() { return messageId; } - public String getOrderNo() { return orderNo; } - public String getPromotionId() { return promotionId; } - public boolean isPublished() { return published; } - public void markPublished() { published = true; } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxPublisher.java b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxPublisher.java deleted file mode 100644 index a1c5ec7..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationOutboxPublisher.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.arch.policy.book.application; - -import java.util.List; - -/** 以至少一次语义发布 Outbox;OrderCreationSaga 必须使用 orderNo 幂等启动。 */ -public final class OrderCreationOutboxPublisher { - private final BookOrderStore store; - private final OrderCreationSaga creationSaga; - - public OrderCreationOutboxPublisher(BookOrderStore store, OrderCreationSaga creationSaga) { - this.store = store; - this.creationSaga = creationSaga; - } - - public void publish(OrderCreationOutboxMessage message) { - if (message.isPublished()) return; - creationSaga.start(message.getOrderNo(), message.getPromotionId()); - store.markCreationOutboxPublished(message.getMessageId()); - } - - public void publishPending(int limit) { - List messages = store.findUnpublishedCreationOutbox(limit); - for (OrderCreationOutboxMessage message : messages) publish(message); - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSaga.java b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSaga.java deleted file mode 100644 index 9f9637c..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSaga.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.arch.policy.book.application; - -public interface OrderCreationSaga { - - /** 使用 orderNo 作为业务键幂等启动;重复调用不得重复执行外部业务动作。 */ - SagaStartResult start(String orderNo, String promotionId); - - final class SagaStartResult { - private final String instanceId; - private final boolean running; - - public SagaStartResult(String instanceId, boolean running) { - this.instanceId = instanceId; - this.running = running; - } - - public String getInstanceId() { return instanceId; } - public boolean isRunning() { return running; } - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSagaStateServices.java b/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSagaStateServices.java deleted file mode 100644 index 366c613..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/OrderCreationSagaStateServices.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.BookOrder; -import com.arch.policy.common.book.OrderEventRequest; - -import java.util.Collections; - -public final class OrderCreationSagaStateServices { - private final BookOrderStore store; - private final GdsBookingGateway gdsGateway; - private final OrderEventApplicationService eventService; - private final CompensationTaskService compensationService; - private final OrderWorkflowTaskStore workflowTaskStore; - - public OrderCreationSagaStateServices(BookOrderStore store, GdsBookingGateway gdsGateway, - OrderEventApplicationService eventService, - CompensationTaskService compensationService, - OrderWorkflowTaskStore workflowTaskStore) { - this.store = store; - this.gdsGateway = gdsGateway; - this.eventService = eventService; - this.compensationService = compensationService; - this.workflowTaskStore = workflowTaskStore; - } - - public GdsBookingResult reservePnr(String orderNo) { return gdsGateway.createPnr(orderNo); } - - public boolean markCreateSuccess(String orderNo, String pnr) { - fire(orderNo, orderNo + ":CREATE_SUCCESS", "CREATE_SUCCEEDED", "gds-booking-saga", - Collections.singletonMap("bookingReference", value(pnr))); - workflowTaskStore.saveIfAbsent(new OrderWorkflowTask("WAIT_PAYMENT:" + orderNo, - orderNo, OrderWorkflowTask.TaskType.WAIT_PAYMENT)); - return true; - } - - public boolean markCreateFailure(String orderNo, String errorCode, String promotionId) { - fire(orderNo, orderNo + ":CREATE_FAIL", "CREATE_FAILED", "gds-booking-saga", - Collections.singletonMap("failureCode", value(errorCode))); - compensationService.returnPromotionStock(orderNo, value(promotionId)); - return true; - } - - public boolean markCreateUnknown(String orderNo, String errorCode) { - fire(orderNo, orderNo + ":CREATE_UNKNOWN", "START_VALIDATE", "gds-booking-saga", - Collections.singletonMap("failureCode", value(errorCode))); - workflowTaskStore.saveIfAbsent(new OrderWorkflowTask("MANUAL_GDS_VALIDATION:" + orderNo, - orderNo, OrderWorkflowTask.TaskType.MANUAL_GDS_VALIDATION)); - compensationService.validateGdsBooking(orderNo); - return true; - } - - public boolean enqueueCancelPnr(String orderNo, String childOrderNo, String pnr) { - compensationService.cancelPnr(orderNo, childOrderNo, pnr); - return true; - } - - public void markGdsValidationSuccess(String orderNo, String pnr) { - fire(orderNo, orderNo + ":RECONCILE_SUCCESS", "GDS_BOOKING_CONFIRMED", - "gds-reconciliation-job", Collections.singletonMap("bookingReference", value(pnr))); - workflowTaskStore.saveIfAbsent(new OrderWorkflowTask("WAIT_PAYMENT:" + orderNo, - orderNo, OrderWorkflowTask.TaskType.WAIT_PAYMENT)); - } - - public void markGdsValidationFailure(String orderNo, String errorCode) { - BookOrder order = store.findByOrderNo(orderNo); - fire(orderNo, orderNo + ":RECONCILE_FAIL", "GDS_BOOKING_REJECTED", - "gds-reconciliation-job", Collections.singletonMap("failureCode", value(errorCode))); - compensationService.returnPromotionStock(orderNo, value(order.getPromotionId())); - } - - private void fire(String orderNo, String eventId, String event, String operator, - java.util.Map attributes) { - BookOrder order = store.findByOrderNo(orderNo); - OrderEventRequest request = new OrderEventRequest(); - request.setOrderNo(orderNo); - request.setEventId(eventId); - request.setEvent(event); - request.setOperator(operator); - request.setExpectedVersion(order.getVersion()); - request.setAttributes(attributes); - eventService.fireEvent(request); - } - - private static String value(String value) { return value == null ? "" : value; } - -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderEventApplicationService.java b/architecture/src/main/java/com/arch/policy/book/application/OrderEventApplicationService.java deleted file mode 100644 index 4dc9c52..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/OrderEventApplicationService.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.BookOrder; -import com.arch.policy.book.domain.OrderEvent; -import com.arch.policy.book.domain.OrderStateMachine; -import com.arch.policy.book.domain.StateTransitionContext; -import com.arch.policy.book.domain.TransitionExecution; -import com.arch.policy.common.book.BookOrderResponse; -import com.arch.policy.common.book.OrderEventRequest; - -public final class OrderEventApplicationService { - private static final long CREATE_RESULT_TTL_MILLIS = 600_000L; - private final BookOrderStore store; - private final OrderStateMachine stateMachine; - private final PostTransitionExecutor postTransitionExecutor; - private final CompensationTaskService compensationTaskService; - private final CreateOrderResultCache resultCache; - - public OrderEventApplicationService(BookOrderStore store, OrderStateMachine stateMachine, - PostTransitionExecutor postTransitionExecutor, - CompensationTaskService compensationTaskService, - CreateOrderResultCache resultCache) { - this.store = store; - this.stateMachine = stateMachine; - this.postTransitionExecutor = postTransitionExecutor; - this.compensationTaskService = compensationTaskService; - this.resultCache = resultCache; - } - - public BookOrderResponse fireEvent(OrderEventRequest request) { - validate(request); - BookOrder processed = store.findByEventId(request.getEventId()); - if (processed != null) return response(processed); - BookOrder order = store.findByOrderNo(request.getOrderNo()); - if (order == null) throw new IllegalArgumentException("order not found: " + request.getOrderNo()); - if (order.getVersion() != request.getExpectedVersion()) { - throw new IllegalStateException("stale order version: " + request.getExpectedVersion()); - } - OrderEvent event = parseEvent(request.getEvent()); - TransitionExecution execution = stateMachine.fire(new StateTransitionContext( - request.getEventId(), event, order, request.getOperator(), request.getAttributes())); - TransitionCommitResult result = store.transit(order, execution.getPreviousVersion(), - history(execution), outbox(execution)); - if (!result.isDuplicateEvent()) { - enqueueCompensation(execution); - postTransitionExecutor.execute(stateMachine, execution); - } - BookOrderResponse response = response(result.getOrder()); - resultCache.put(result.getOrder().getRequestId(), response, CREATE_RESULT_TTL_MILLIS); - return response; - } - - private void enqueueCompensation(TransitionExecution execution) { - StateTransitionContext context = execution.getContext(); - if (context.getEvent() == OrderEvent.CANCEL && !isBlank(context.attribute("pnr"))) { - compensationTaskService.cancelPnr(context.getOrder().getOrderNo(), - value(context.attribute("childOrderNo")), context.attribute("pnr")); - } - if (context.getEvent() == OrderEvent.BOOK_FAILED && !isBlank(context.attribute("paymentNo"))) { - compensationTaskService.refundPayment(context.getOrder().getOrderNo(), - context.attribute("paymentNo")); - } - } - - private static OrderStateHistory history(TransitionExecution execution) { - return new OrderStateHistory(execution.getContext().getEventId(), - execution.getContext().getOrder().getOrderNo(), execution.getTransition().getFrom(), - execution.getTransition().getEvent(), execution.getTransition().getTo(), - execution.getContext().getOperator(), System.currentTimeMillis()); - } - - private static OrderOutboxMessage outbox(TransitionExecution execution) { - BookOrder order = execution.getContext().getOrder(); - return new OrderOutboxMessage(execution.getContext().getEventId(), order.getOrderNo(), - execution.getContext().getEvent(), order.getState(), order.getVersion()); - } - - private static void validate(OrderEventRequest request) { - if (request == null || isBlank(request.getEventId()) || isBlank(request.getOrderNo()) - || isBlank(request.getEvent()) || isBlank(request.getOperator())) { - throw new IllegalArgumentException("eventId, orderNo, event and operator are required"); - } - } - - private static OrderEvent parseEvent(String event) { - try { return OrderEvent.valueOf(event.trim().toUpperCase()); } - catch (IllegalArgumentException failure) { - throw new IllegalArgumentException("unknown order event: " + event, failure); - } - } - - private static boolean isBlank(String value) { return value == null || value.trim().isEmpty(); } - private static String value(String value) { return value == null ? "" : value; } - private static BookOrderResponse response(BookOrder order) { - return new BookOrderResponse(order.getOrderNo(), order.getState().name(), order.getVersion()); - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java b/architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java deleted file mode 100644 index 9e0b4c3..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/OrderOutboxMessage.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.OrderEvent; -import com.arch.policy.book.domain.OrderState; - -public final class OrderOutboxMessage { - private final String eventId; - private final String orderNo; - private final OrderEvent event; - private final OrderState state; - private final long orderVersion; - private boolean published; - - public OrderOutboxMessage(String eventId, String orderNo, OrderEvent event, - OrderState state, long orderVersion) { - this.eventId = eventId; - this.orderNo = orderNo; - this.event = event; - this.state = state; - this.orderVersion = orderVersion; - } - - public String getEventId() { return eventId; } - public String getOrderNo() { return orderNo; } - public OrderEvent getEvent() { return event; } - public OrderState getState() { return state; } - public long getOrderVersion() { return orderVersion; } - public boolean isPublished() { return published; } - public void markPublished() { published = true; } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderReconciler.java b/architecture/src/main/java/com/arch/policy/book/application/OrderReconciler.java deleted file mode 100644 index 2420f70..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/OrderReconciler.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.arch.policy.book.application; - -public interface OrderReconciler { - void reconcile(String orderNo); -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java b/architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java deleted file mode 100644 index 38e8fab..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/OrderStateHistory.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.OrderEvent; -import com.arch.policy.book.domain.OrderState; - -public final class OrderStateHistory { - private final String eventId; - private final String orderNo; - private final OrderState from; - private final OrderEvent event; - private final OrderState to; - private final String operator; - private final long occurredAtMillis; - - public OrderStateHistory(String eventId, String orderNo, OrderState from, OrderEvent event, - OrderState to, String operator, long occurredAtMillis) { - this.eventId = eventId; - this.orderNo = orderNo; - this.from = from; - this.event = event; - this.to = to; - this.operator = operator; - this.occurredAtMillis = occurredAtMillis; - } - - public String getEventId() { return eventId; } - public String getOrderNo() { return orderNo; } - public OrderState getFrom() { return from; } - public OrderEvent getEvent() { return event; } - public OrderState getTo() { return to; } - public String getOperator() { return operator; } - public long getOccurredAtMillis() { return occurredAtMillis; } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTask.java b/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTask.java deleted file mode 100644 index de6d653..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTask.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.arch.policy.book.application; - -public final class OrderWorkflowTask { - private final String uniqueKey; - private final String orderNo; - private final TaskType type; - - public OrderWorkflowTask(String uniqueKey, String orderNo, TaskType type) { - this.uniqueKey = uniqueKey; - this.orderNo = orderNo; - this.type = type; - } - - public String getUniqueKey() { return uniqueKey; } - public String getOrderNo() { return orderNo; } - public TaskType getType() { return type; } - - public enum TaskType { WAIT_PAYMENT, MANUAL_GDS_VALIDATION } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTaskStore.java b/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTaskStore.java deleted file mode 100644 index 88fb2d5..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/OrderWorkflowTaskStore.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.arch.policy.book.application; - -public interface OrderWorkflowTaskStore { - void saveIfAbsent(OrderWorkflowTask task); -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/PaymentGateway.java b/architecture/src/main/java/com/arch/policy/book/application/PaymentGateway.java deleted file mode 100644 index 6c828b2..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/PaymentGateway.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.arch.policy.book.application; - -public interface PaymentGateway { - void refund(String orderNo, String paymentNo); -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java b/architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java deleted file mode 100644 index 74e5d54..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/PostTransitionExecutor.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.OrderStateMachine; -import com.arch.policy.book.domain.TransitionExecution; - -public interface PostTransitionExecutor { - void execute(OrderStateMachine stateMachine, TransitionExecution execution); -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/PromotionStockGateway.java b/architecture/src/main/java/com/arch/policy/book/application/PromotionStockGateway.java deleted file mode 100644 index 23a9132..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/PromotionStockGateway.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.arch.policy.book.application; - -public interface PromotionStockGateway { - void returnStock(String orderNo, String promotionId); -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/RecoveryTask.java b/architecture/src/main/java/com/arch/policy/book/application/RecoveryTask.java deleted file mode 100644 index 0989144..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/RecoveryTask.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.arch.policy.book.application; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -public final class RecoveryTask { - private final String uniqueKey; - private final RecoveryType type; - private final String orderNo; - private final Map parameters; - private RecoveryStatus status; - private int attempts; - private long nextAttemptAtMillis; - private String lastError; - - public RecoveryTask(String uniqueKey, RecoveryType type, String orderNo, - Map parameters) { - this.uniqueKey = uniqueKey; - this.type = type; - this.orderNo = orderNo; - this.parameters = parameters == null ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap(parameters)); - this.status = RecoveryStatus.PENDING; - } - - public RecoveryTask copy() { - RecoveryTask copy = new RecoveryTask(uniqueKey, type, orderNo, parameters); - copy.status = status; - copy.attempts = attempts; - copy.nextAttemptAtMillis = nextAttemptAtMillis; - copy.lastError = lastError; - return copy; - } - - public void markSucceeded() { status = RecoveryStatus.SUCCEEDED; lastError = null; } - public void markRetry(long nextAttemptAtMillis, String error) { - attempts++; - status = RecoveryStatus.RETRYING; - this.nextAttemptAtMillis = nextAttemptAtMillis; - lastError = error; - } - public void markManualRequired(String error) { - attempts++; - status = RecoveryStatus.MANUAL_REQUIRED; - lastError = error; - } - - public String getUniqueKey() { return uniqueKey; } - public RecoveryType getType() { return type; } - public String getOrderNo() { return orderNo; } - public Map getParameters() { return parameters; } - public RecoveryStatus getStatus() { return status; } - public int getAttempts() { return attempts; } - public long getNextAttemptAtMillis() { return nextAttemptAtMillis; } - public String getLastError() { return lastError; } - - public enum RecoveryType { - RETURN_PROMOTION_STOCK, - CANCEL_PNR, - REFUND_PAYMENT, - VALIDATE_GDS_BOOKING - } - - public enum RecoveryStatus { PENDING, RETRYING, SUCCEEDED, MANUAL_REQUIRED } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskProcessor.java b/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskProcessor.java deleted file mode 100644 index c010fda..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskProcessor.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.arch.policy.book.application; - -import java.util.List; - -public final class RecoveryTaskProcessor { - private static final int MAX_ATTEMPTS = 5; - private final RecoveryTaskStore taskStore; - private final PromotionStockGateway promotionStockGateway; - private final GdsBookingGateway gdsGateway; - private final PaymentGateway paymentGateway; - private final OrderReconciler reconciliationService; - - public RecoveryTaskProcessor(RecoveryTaskStore taskStore, - PromotionStockGateway promotionStockGateway, - GdsBookingGateway gdsGateway, - PaymentGateway paymentGateway, - OrderReconciler reconciliationService) { - this.taskStore = taskStore; - this.promotionStockGateway = promotionStockGateway; - this.gdsGateway = gdsGateway; - this.paymentGateway = paymentGateway; - this.reconciliationService = reconciliationService; - } - - public void processBatch(long nowMillis, int limit) { - List tasks = taskStore.findExecutable(nowMillis, limit); - for (RecoveryTask task : tasks) process(task, nowMillis); - } - - private void process(RecoveryTask task, long nowMillis) { - try { - execute(task); - task.markSucceeded(); - } catch (RuntimeException failure) { - if (task.getAttempts() + 1 >= MAX_ATTEMPTS) { - task.markManualRequired(failure.getMessage()); - } else { - task.markRetry(nowMillis + retryDelayMillis(task.getAttempts()), failure.getMessage()); - } - } - taskStore.save(task); - } - - private void execute(RecoveryTask task) { - switch (task.getType()) { - case RETURN_PROMOTION_STOCK: - promotionStockGateway.returnStock(task.getOrderNo(), task.getParameters().get("promotionId")); - return; - case CANCEL_PNR: - gdsGateway.cancelPnr(task.getOrderNo(), task.getParameters().get("childOrderNo"), - task.getParameters().get("pnr")); - return; - case REFUND_PAYMENT: - paymentGateway.refund(task.getOrderNo(), task.getParameters().get("paymentNo")); - return; - case VALIDATE_GDS_BOOKING: - reconciliationService.reconcile(task.getOrderNo()); - return; - default: - throw new IllegalStateException("unsupported recovery type: " + task.getType()); - } - } - - private static long retryDelayMillis(int attempts) { - return Math.min(60_000L, 1_000L << Math.min(attempts, 6)); - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskStore.java b/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskStore.java deleted file mode 100644 index b8ac806..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/RecoveryTaskStore.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.arch.policy.book.application; - -import java.util.List; - -public interface RecoveryTaskStore { - RecoveryTask saveIfAbsent(RecoveryTask task); - - List findExecutable(long nowMillis, int limit); - - void save(RecoveryTask task); -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java b/architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java deleted file mode 100644 index 3043b16..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/RetryablePostTransitionExecutor.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.OrderStateMachine; -import com.arch.policy.book.domain.TransitionExecution; - -public final class RetryablePostTransitionExecutor implements PostTransitionExecutor { - private final FailedPostActionStore failedActionStore; - - public RetryablePostTransitionExecutor(FailedPostActionStore failedActionStore) { - this.failedActionStore = failedActionStore; - } - - @Override public void execute(OrderStateMachine stateMachine, TransitionExecution execution) { - try { - stateMachine.afterCommit(execution); - } catch (RuntimeException failure) { - failedActionStore.record(execution, failure); - } - } - - public interface FailedPostActionStore { - void record(TransitionExecution execution, RuntimeException failure); - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java b/architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java deleted file mode 100644 index 6564e33..0000000 --- a/architecture/src/main/java/com/arch/policy/book/application/TransitionCommitResult.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.arch.policy.book.application; - -import com.arch.policy.book.domain.BookOrder; - -public final class TransitionCommitResult { - private final BookOrder order; - private final boolean duplicateEvent; - - private TransitionCommitResult(BookOrder order, boolean duplicateEvent) { - this.order = order; - this.duplicateEvent = duplicateEvent; - } - - public static TransitionCommitResult committed(BookOrder order) { - return new TransitionCommitResult(order, false); - } - - public static TransitionCommitResult duplicate(BookOrder order) { - return new TransitionCommitResult(order, true); - } - - public BookOrder getOrder() { return order; } - public boolean isDuplicateEvent() { return duplicateEvent; } -} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java b/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java deleted file mode 100644 index 6507001..0000000 --- a/architecture/src/main/java/com/arch/policy/book/domain/BookOrder.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.arch.policy.book.domain; - -import java.math.BigDecimal; - -public final class BookOrder { - private final String orderNo; - private final String requestId; - private final String customerId; - private final String productId; - private final String promotionId; - private final int quantity; - private final BigDecimal amount; - private OrderState state; - private long version; - - private BookOrder(String orderNo, String requestId, String customerId, String productId, - String promotionId, int quantity, BigDecimal amount, OrderState state, long version) { - this.orderNo = orderNo; - this.requestId = requestId; - this.customerId = customerId; - this.productId = productId; - this.promotionId = promotionId; - this.quantity = quantity; - this.amount = amount; - this.state = state; - this.version = version; - } - - public static BookOrder create(String orderNo, String requestId, String customerId, - String productId, String promotionId, int quantity, BigDecimal amount) { - return new BookOrder(orderNo, requestId, customerId, productId, promotionId, quantity, amount, - OrderState.CREATE, 0L); - } - - public BookOrder copy() { - return new BookOrder(orderNo, requestId, customerId, productId, promotionId, - quantity, amount, state, version); - } - - void applyState(OrderState target) { - state = target; - version++; - } - - public String getOrderNo() { return orderNo; } - public String getRequestId() { return requestId; } - public String getCustomerId() { return customerId; } - public String getProductId() { return productId; } - public String getPromotionId() { return promotionId; } - public int getQuantity() { return quantity; } - public BigDecimal getAmount() { return amount; } - public OrderState getState() { return state; } - public long getVersion() { return version; } -} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java b/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java deleted file mode 100644 index 2d51f92..0000000 --- a/architecture/src/main/java/com/arch/policy/book/domain/DefaultOrderTransitions.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.arch.policy.book.domain; - -import java.util.ArrayList; -import java.util.List; - -public final class DefaultOrderTransitions { - private DefaultOrderTransitions() {} - - public static List definitions() { - List transitions = new ArrayList(); - add(transitions, OrderState.CREATE, OrderEvent.CREATE_SUCCEEDED, OrderState.WAIT_PAY); - add(transitions, OrderState.CREATE, OrderEvent.CREATE_FAILED, OrderState.CREATE_FAIL); - add(transitions, OrderState.CREATE, OrderEvent.START_VALIDATE, OrderState.VALIDATING); - add(transitions, OrderState.CREATE, OrderEvent.CANCEL, OrderState.CANCEL); - transitions.add(OrderTransition - .from(OrderState.WAIT_PAY, OrderEvent.PAY_SUCCEEDED, OrderState.BOOKING) - .guard(new RequiredAttributeGuard("paymentNo")).build()); - add(transitions, OrderState.WAIT_PAY, OrderEvent.CANCEL, OrderState.CANCEL); - transitions.add(OrderTransition - .from(OrderState.BOOKING, OrderEvent.BOOK_SUCCEEDED, OrderState.BOOKED) - .guard(new RequiredAttributeGuard("bookingReference")).build()); - transitions.add(OrderTransition - .from(OrderState.BOOKING, OrderEvent.BOOK_FAILED, OrderState.BOOK_FAIL) - .guard(new RequiredAttributeGuard("failureCode")).build()); - add(transitions, OrderState.BOOKING, OrderEvent.START_VALIDATE, OrderState.VALIDATING); - add(transitions, OrderState.BOOK_FAIL, OrderEvent.START_VALIDATE, OrderState.VALIDATING); - add(transitions, OrderState.BOOK_FAIL, OrderEvent.CANCEL, OrderState.CANCEL); - add(transitions, OrderState.BOOK_FAIL, OrderEvent.DELETE, OrderState.DELETED); - add(transitions, OrderState.BOOKED, OrderEvent.START_VALIDATE, OrderState.VALIDATING); - add(transitions, OrderState.BOOKED, OrderEvent.CANCEL, OrderState.CANCEL); - transitions.add(OrderTransition - .from(OrderState.BOOKED, OrderEvent.REFUND_SUCCEEDED, OrderState.REFUNDED) - .guard(new RequiredAttributeGuard("refundNo")).build()); - add(transitions, OrderState.VALIDATING, OrderEvent.VALIDATE_SUCCEEDED, OrderState.BOOKED); - add(transitions, OrderState.VALIDATING, OrderEvent.VALIDATE_FAILED, OrderState.VALIDATE_FAIL); - transitions.add(OrderTransition - .from(OrderState.VALIDATING, OrderEvent.GDS_BOOKING_CONFIRMED, OrderState.WAIT_PAY) - .guard(new RequiredAttributeGuard("bookingReference")).build()); - transitions.add(OrderTransition - .from(OrderState.VALIDATING, OrderEvent.GDS_BOOKING_REJECTED, OrderState.CREATE_FAIL) - .guard(new RequiredAttributeGuard("failureCode")).build()); - add(transitions, OrderState.VALIDATE_FAIL, OrderEvent.VALIDATE_SUCCEEDED, OrderState.BOOKED); - add(transitions, OrderState.VALIDATE_FAIL, OrderEvent.CANCEL, OrderState.CANCEL); - add(transitions, OrderState.CREATE_FAIL, OrderEvent.CANCEL, OrderState.CANCEL); - add(transitions, OrderState.CREATE_FAIL, OrderEvent.DELETE, OrderState.DELETED); - add(transitions, OrderState.CANCEL, OrderEvent.DELETE, OrderState.DELETED); - add(transitions, OrderState.REFUNDED, OrderEvent.DELETE, OrderState.DELETED); - return transitions; - } - - private static void add(List transitions, OrderState from, - OrderEvent event, OrderState to) { - transitions.add(OrderTransition.from(from, event, to).build()); - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java deleted file mode 100644 index 9bd36b7..0000000 --- a/architecture/src/main/java/com/arch/policy/book/domain/OrderEvent.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.arch.policy.book.domain; - -public enum OrderEvent { - CREATE_SUCCEEDED, - CREATE_FAILED, - PAY_SUCCEEDED, - BOOK_SUCCEEDED, - BOOK_FAILED, - START_VALIDATE, - VALIDATE_SUCCEEDED, - VALIDATE_FAILED, - GDS_BOOKING_CONFIRMED, - GDS_BOOKING_REJECTED, - CANCEL, - REFUND_SUCCEEDED, - DELETE -} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderState.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderState.java deleted file mode 100644 index a747609..0000000 --- a/architecture/src/main/java/com/arch/policy/book/domain/OrderState.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.arch.policy.book.domain; - -public enum OrderState { - CREATE(0, "创单"), - WAIT_PAY(1, "待支付"), - CREATE_FAIL(2, "创单失败"), - CANCEL(3, "已取消"), - BOOKING(4, "预定中"), - BOOKED(5, "已预定"), - BOOK_FAIL(6, "预定失败"), - DELETED(7, "已删除"), - REFUNDED(8, "已退订"), - VALIDATING(10, "验真中"), - VALIDATE_FAIL(11, "验真失败"); - - private final int code; - private final String description; - - OrderState(int code, String description) { - this.code = code; - this.description = description; - } - - public int getCode() { return code; } - public String getDescription() { return description; } -} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java deleted file mode 100644 index 7d5b19d..0000000 --- a/architecture/src/main/java/com/arch/policy/book/domain/OrderStateMachine.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.arch.policy.book.domain; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public final class OrderStateMachine { - private final Map transitions; - - public OrderStateMachine(List definitions) { - Map registry = new HashMap(); - for (OrderTransition transition : definitions) { - TransitionKey key = new TransitionKey(transition.getFrom(), transition.getEvent()); - if (registry.put(key, transition) != null) { - throw new IllegalStateException("duplicate order transition: " + key); - } - } - transitions = Collections.unmodifiableMap(registry); - } - - public TransitionExecution fire(StateTransitionContext context) { - OrderTransition transition = transitions.get( - new TransitionKey(context.getOrder().getState(), context.getEvent())); - if (transition == null) { - throw new IllegalStateException("unsupported order event " + context.getEvent() - + " in state " + context.getOrder().getState()); - } - executeGuards(transition, context); - executeActions(transition.getPreActions(), context); - long previousVersion = context.getOrder().getVersion(); - context.getOrder().applyState(transition.getTo()); - return new TransitionExecution(transition, context, previousVersion); - } - - public void afterCommit(TransitionExecution execution) { - executeActions(execution.getTransition().getPostActions(), execution.getContext()); - } - - public OrderState targetOf(OrderState state, OrderEvent event) { - OrderTransition transition = transitions.get(new TransitionKey(state, event)); - return transition == null ? null : transition.getTo(); - } - - private static void executeGuards(OrderTransition transition, StateTransitionContext context) { - for (TransitionGuard guard : transition.getGuards()) guard.check(context); - } - - private static void executeActions(List actions, StateTransitionContext context) { - for (TransitionAction action : actions) action.execute(context); - } - - private static final class TransitionKey { - private final OrderState state; - private final OrderEvent event; - - private TransitionKey(OrderState state, OrderEvent event) { - this.state = state; - this.event = event; - } - - @Override public boolean equals(Object other) { - if (this == other) return true; - if (!(other instanceof TransitionKey)) return false; - TransitionKey that = (TransitionKey) other; - return state == that.state && event == that.event; - } - - @Override public int hashCode() { return 31 * state.hashCode() + event.hashCode(); } - @Override public String toString() { return state + " + " + event; } - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java b/architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java deleted file mode 100644 index 4ae61df..0000000 --- a/architecture/src/main/java/com/arch/policy/book/domain/OrderTransition.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.arch.policy.book.domain; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public final class OrderTransition { - private final OrderState from; - private final OrderEvent event; - private final OrderState to; - private final List guards; - private final List preActions; - private final List postActions; - - private OrderTransition(Builder builder) { - from = builder.from; - event = builder.event; - to = builder.to; - guards = immutableCopy(builder.guards); - preActions = immutableCopy(builder.preActions); - postActions = immutableCopy(builder.postActions); - } - - public static Builder from(OrderState from, OrderEvent event, OrderState to) { - return new Builder(from, event, to); - } - - public OrderState getFrom() { return from; } - public OrderEvent getEvent() { return event; } - public OrderState getTo() { return to; } - public List getGuards() { return guards; } - public List getPreActions() { return preActions; } - public List getPostActions() { return postActions; } - - private static List immutableCopy(List values) { - return Collections.unmodifiableList(new ArrayList(values)); - } - - public static final class Builder { - private final OrderState from; - private final OrderEvent event; - private final OrderState to; - private final List guards = new ArrayList(); - private final List preActions = new ArrayList(); - private final List postActions = new ArrayList(); - - private Builder(OrderState from, OrderEvent event, OrderState to) { - if (from == null || event == null || to == null) { - throw new IllegalArgumentException("from, event and to are required"); - } - this.from = from; - this.event = event; - this.to = to; - } - - public Builder guard(TransitionGuard guard) { guards.add(guard); return this; } - public Builder before(TransitionAction action) { preActions.add(action); return this; } - public Builder after(TransitionAction action) { postActions.add(action); return this; } - public OrderTransition build() { return new OrderTransition(this); } - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java b/architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java deleted file mode 100644 index b636a00..0000000 --- a/architecture/src/main/java/com/arch/policy/book/domain/RequiredAttributeGuard.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.arch.policy.book.domain; - -public final class RequiredAttributeGuard implements TransitionGuard { - private final String attributeName; - - public RequiredAttributeGuard(String attributeName) { - this.attributeName = attributeName; - } - - @Override public void check(StateTransitionContext context) { - String value = context.attribute(attributeName); - if (value == null || value.trim().isEmpty()) { - throw new IllegalStateException(attributeName + " is required for event " + context.getEvent()); - } - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java b/architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java deleted file mode 100644 index 3269260..0000000 --- a/architecture/src/main/java/com/arch/policy/book/domain/StateTransitionContext.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.arch.policy.book.domain; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -public final class StateTransitionContext { - private final String eventId; - private final OrderEvent event; - private final BookOrder order; - private final String operator; - private final Map attributes; - - public StateTransitionContext(String eventId, OrderEvent event, BookOrder order, - String operator, Map attributes) { - this.eventId = eventId; - this.event = event; - this.order = order; - this.operator = operator; - this.attributes = attributes == null ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap(attributes)); - } - - public String getEventId() { return eventId; } - public OrderEvent getEvent() { return event; } - public BookOrder getOrder() { return order; } - public String getOperator() { return operator; } - public Map getAttributes() { return attributes; } - public String attribute(String name) { return attributes.get(name); } -} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java b/architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java deleted file mode 100644 index a035155..0000000 --- a/architecture/src/main/java/com/arch/policy/book/domain/TransitionAction.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.arch.policy.book.domain; - -public interface TransitionAction { - void execute(StateTransitionContext context); -} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java b/architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java deleted file mode 100644 index 1e4a0c8..0000000 --- a/architecture/src/main/java/com/arch/policy/book/domain/TransitionExecution.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.arch.policy.book.domain; - -public final class TransitionExecution { - private final OrderTransition transition; - private final StateTransitionContext context; - private final long previousVersion; - - public TransitionExecution(OrderTransition transition, StateTransitionContext context, - long previousVersion) { - this.transition = transition; - this.context = context; - this.previousVersion = previousVersion; - } - - public OrderTransition getTransition() { return transition; } - public StateTransitionContext getContext() { return context; } - public long getPreviousVersion() { return previousVersion; } -} diff --git a/architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java b/architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java deleted file mode 100644 index 5c88ebe..0000000 --- a/architecture/src/main/java/com/arch/policy/book/domain/TransitionGuard.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.arch.policy.book.domain; - -public interface TransitionGuard { - void check(StateTransitionContext context); -} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/cache/DefaultCreateOrderResultCache.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/cache/DefaultCreateOrderResultCache.java deleted file mode 100644 index fd6bf92..0000000 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/cache/DefaultCreateOrderResultCache.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.arch.policy.book.infrastructure.cache; - -import com.arch.policy.book.application.CreateOrderResultCache; -import com.arch.policy.common.book.BookOrderResponse; -import com.arch.policy.common.redis.RedisClient; -import com.arch.policy.common.redis.RedisClientException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.locks.LockSupport; - -public final class DefaultCreateOrderResultCache implements CreateOrderResultCache { - private static final Logger LOGGER = LoggerFactory.getLogger(DefaultCreateOrderResultCache.class); - private static final String KEY_PREFIX = "book:create:result:"; - private static final long POLL_INTERVAL_NANOS = 20_000_000L; - private final RedisClient redisClient; - - public DefaultCreateOrderResultCache(RedisClient redisClient) { - this.redisClient = redisClient; - } - - @Override public BookOrderResponse get(String requestId) { - try { - return redisClient.get(key(requestId)); - } catch (RedisClientException redisFailure) { - LOGGER.warn("Create-order result cache unavailable, requestId={}", requestId, - redisFailure); - return null; - } - } - - @Override public void put(String requestId, BookOrderResponse response, long ttlMillis) { - try { - redisClient.set(key(requestId), response, ttlMillis); - } catch (RedisClientException redisFailure) { - // 缓存写失败不回滚已经提交的订单,后续请求仍可在锁内查询数据库回填。 - LOGGER.warn("Failed to cache create-order result, requestId={}", requestId, - redisFailure); - } - } - - @Override public BookOrderResponse await(String requestId, long waitMillis) { - long deadlineNanos = System.nanoTime() + waitMillis * 1_000_000L; - do { - BookOrderResponse response = get(requestId); - if (response != null) return response; - if (Thread.currentThread().isInterrupted()) return null; - LockSupport.parkNanos(POLL_INTERVAL_NANOS); - } while (System.nanoTime() < deadlineNanos); - return get(requestId); - } - - private static String key(String requestId) { return KEY_PREFIX + requestId; } -} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/demo/DemoGdsBookingGateway.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/demo/DemoGdsBookingGateway.java deleted file mode 100644 index d965d60..0000000 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/demo/DemoGdsBookingGateway.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.arch.policy.book.infrastructure.demo; - -import com.arch.policy.book.application.GdsBookingGateway; -import com.arch.policy.book.application.GdsBookingResult; - -public final class DemoGdsBookingGateway implements GdsBookingGateway { - @Override public GdsBookingResult createPnr(String orderNo) { - return GdsBookingResult.success("PNR-" + orderNo.substring(Math.max(0, orderNo.length() - 6))); - } - - @Override public GdsBookingResult queryPnr(String orderNo) { - return createPnr(orderNo); - } - - @Override public void cancelPnr(String orderNo, String childOrderNo, String pnr) { - // Demo adapter: production uses a GDS RPC client with the same idempotency key. - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderCreationOutboxScheduler.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderCreationOutboxScheduler.java deleted file mode 100644 index 2094263..0000000 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderCreationOutboxScheduler.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.arch.policy.book.infrastructure.job; - -import com.arch.policy.book.application.OrderCreationOutboxPublisher; -import org.springframework.scheduling.annotation.Scheduled; - -public final class OrderCreationOutboxScheduler { - private final OrderCreationOutboxPublisher publisher; - - public OrderCreationOutboxScheduler(OrderCreationOutboxPublisher publisher) { - this.publisher = publisher; - } - - @Scheduled(fixedDelayString = "${book.creation.outbox-delay-ms:3000}") - public void publish() { publisher.publishPending(100); } -} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderRecoveryScheduler.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderRecoveryScheduler.java deleted file mode 100644 index c0c01ef..0000000 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/job/OrderRecoveryScheduler.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.arch.policy.book.infrastructure.job; - -import com.arch.policy.book.application.RecoveryTaskProcessor; -import org.springframework.scheduling.annotation.Scheduled; - -public final class OrderRecoveryScheduler { - private final RecoveryTaskProcessor processor; - - public OrderRecoveryScheduler(RecoveryTaskProcessor processor) { this.processor = processor; } - - @Scheduled(fixedDelayString = "${book.recovery.scan-delay-ms:5000}") - public void scan() { processor.processBatch(System.currentTimeMillis(), 100); } -} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java deleted file mode 100644 index 5895766..0000000 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryBookOrderStore.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.arch.policy.book.infrastructure.repository; - -import com.arch.policy.book.application.BookOrderStore; -import com.arch.policy.book.application.LocalCreateResult; -import com.arch.policy.book.application.OrderOutboxMessage; -import com.arch.policy.book.application.OrderCreationOutboxMessage; -import com.arch.policy.book.application.OrderStateHistory; -import com.arch.policy.book.application.TransitionCommitResult; -import com.arch.policy.book.domain.BookOrder; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public final class InMemoryBookOrderStore implements BookOrderStore { - private final Map orders = new HashMap(); - private final Map requestIndexes = new HashMap(); - private final Map eventResults = new HashMap(); - private final Map creationOutbox = - new HashMap(); - private final List histories = new ArrayList(); - private final List outboxMessages = new ArrayList(); - private int findByRequestIdCalls; - - @Override public synchronized BookOrder findByOrderNo(String orderNo) { - return copy(orders.get(orderNo)); - } - - @Override public synchronized BookOrder findByRequestId(String requestId) { - findByRequestIdCalls++; - String orderNo = requestIndexes.get(requestId); - return orderNo == null ? null : copy(orders.get(orderNo)); - } - - @Override public synchronized BookOrder findByEventId(String eventId) { - return copy(eventResults.get(eventId)); - } - - @Override public synchronized LocalCreateResult createOrder( - BookOrder order, OrderCreationOutboxMessage outboxMessage) { - String existingOrderNo = requestIndexes.get(order.getRequestId()); - if (existingOrderNo != null) { - return LocalCreateResult.duplicate(copy(orders.get(existingOrderNo))); - } - orders.put(order.getOrderNo(), order.copy()); - requestIndexes.put(order.getRequestId(), order.getOrderNo()); - creationOutbox.put(outboxMessage.getMessageId(), outboxMessage); - return LocalCreateResult.created(order.copy()); - } - - @Override public synchronized List findUnpublishedCreationOutbox( - int limit) { - List result = new ArrayList(); - for (OrderCreationOutboxMessage message : creationOutbox.values()) { - if (result.size() >= limit) break; - if (!message.isPublished()) result.add(message); - } - return result; - } - - @Override public synchronized void markCreationOutboxPublished(String messageId) { - OrderCreationOutboxMessage message = creationOutbox.get(messageId); - if (message != null) message.markPublished(); - } - - @Override public synchronized TransitionCommitResult transit( - BookOrder order, long expectedVersion, OrderStateHistory history, - OrderOutboxMessage outboxMessage) { - BookOrder processed = eventResults.get(history.getEventId()); - if (processed != null) return TransitionCommitResult.duplicate(processed.copy()); - - BookOrder current = orders.get(order.getOrderNo()); - if (current == null || current.getVersion() != expectedVersion - || order.getVersion() != expectedVersion + 1) { - throw new IllegalStateException("concurrent order update: " + order.getOrderNo()); - } - persist(order, history, outboxMessage); - return TransitionCommitResult.committed(order.copy()); - } - - public synchronized List historiesOf(String orderNo) { - List result = new ArrayList(); - for (OrderStateHistory history : histories) { - if (orderNo.equals(history.getOrderNo())) result.add(history); - } - return Collections.unmodifiableList(result); - } - - public synchronized int getFindByRequestIdCalls() { return findByRequestIdCalls; } - - public synchronized List unpublishedOutboxMessages() { - List result = new ArrayList(); - for (OrderOutboxMessage message : outboxMessages) { - if (!message.isPublished()) result.add(message); - } - return Collections.unmodifiableList(result); - } - - private void persist(BookOrder order, OrderStateHistory history, OrderOutboxMessage outboxMessage) { - BookOrder snapshot = order.copy(); - orders.put(order.getOrderNo(), snapshot); - eventResults.put(history.getEventId(), snapshot.copy()); - histories.add(history); - outboxMessages.add(outboxMessage); - } - - private static BookOrder copy(BookOrder order) { return order == null ? null : order.copy(); } - -} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java deleted file mode 100644 index 00c7db5..0000000 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryFailedPostActionStore.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.arch.policy.book.infrastructure.repository; - -import com.arch.policy.book.application.RetryablePostTransitionExecutor.FailedPostActionStore; -import com.arch.policy.book.domain.TransitionExecution; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public final class InMemoryFailedPostActionStore implements FailedPostActionStore { - private final List failures = new ArrayList(); - - @Override public synchronized void record(TransitionExecution execution, RuntimeException failure) { - failures.add(new FailedPostAction(execution.getContext().getEventId(), - execution.getContext().getOrder().getOrderNo(), failure.getMessage())); - } - - public synchronized List all() { - return Collections.unmodifiableList(new ArrayList(failures)); - } - - public static final class FailedPostAction { - private final String eventId; - private final String orderNo; - private final String reason; - - public FailedPostAction(String eventId, String orderNo, String reason) { - this.eventId = eventId; - this.orderNo = orderNo; - this.reason = reason; - } - - public String getEventId() { return eventId; } - public String getOrderNo() { return orderNo; } - public String getReason() { return reason; } - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryOrderWorkflowTaskStore.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryOrderWorkflowTaskStore.java deleted file mode 100644 index 25124df..0000000 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryOrderWorkflowTaskStore.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.arch.policy.book.infrastructure.repository; - -import com.arch.policy.book.application.OrderWorkflowTask; -import com.arch.policy.book.application.OrderWorkflowTaskStore; - -import java.util.LinkedHashMap; -import java.util.Map; - -public final class InMemoryOrderWorkflowTaskStore implements OrderWorkflowTaskStore { - private final Map tasks = new LinkedHashMap(); - - @Override public synchronized void saveIfAbsent(OrderWorkflowTask task) { - if (!tasks.containsKey(task.getUniqueKey())) tasks.put(task.getUniqueKey(), task); - } - - public synchronized int size() { return tasks.size(); } -} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryRecoveryTaskStore.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryRecoveryTaskStore.java deleted file mode 100644 index c50d22b..0000000 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/repository/InMemoryRecoveryTaskStore.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.arch.policy.book.infrastructure.repository; - -import com.arch.policy.book.application.RecoveryTask; -import com.arch.policy.book.application.RecoveryTaskStore; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -public final class InMemoryRecoveryTaskStore implements RecoveryTaskStore { - private final Map tasks = new LinkedHashMap(); - - @Override public synchronized RecoveryTask saveIfAbsent(RecoveryTask task) { - RecoveryTask existing = tasks.get(task.getUniqueKey()); - if (existing != null) return existing.copy(); - tasks.put(task.getUniqueKey(), task.copy()); - return task.copy(); - } - - @Override public synchronized List findExecutable(long nowMillis, int limit) { - List result = new ArrayList(); - for (RecoveryTask task : tasks.values()) { - if (result.size() >= limit) break; - if ((task.getStatus() == RecoveryTask.RecoveryStatus.PENDING - || task.getStatus() == RecoveryTask.RecoveryStatus.RETRYING) - && task.getNextAttemptAtMillis() <= nowMillis) result.add(task.copy()); - } - return result; - } - - @Override public synchronized void save(RecoveryTask task) { - if (!tasks.containsKey(task.getUniqueKey())) { - throw new IllegalArgumentException("recovery task not found: " + task.getUniqueKey()); - } - tasks.put(task.getUniqueKey(), task.copy()); - } - - public synchronized int size() { return tasks.size(); } - - public synchronized RecoveryTask find(String uniqueKey) { - RecoveryTask task = tasks.get(uniqueKey); - return task == null ? null : task.copy(); - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java deleted file mode 100644 index 869f952..0000000 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/rpc/DubboBookOrderService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.arch.policy.book.infrastructure.rpc; - -import com.arch.policy.api.book.BookOrderRpcService; -import com.arch.policy.book.application.BookOrderApplicationService; -import com.arch.policy.book.application.OrderEventApplicationService; -import com.arch.policy.common.book.BookOrderRequest; -import com.arch.policy.common.book.BookOrderResponse; -import com.arch.policy.common.book.OrderEventRequest; -import org.apache.dubbo.config.annotation.DubboService; - -@DubboService(version = "1.0.0", timeout = 3000) -public final class DubboBookOrderService implements BookOrderRpcService { - private final BookOrderApplicationService applicationService; - private final OrderEventApplicationService eventService; - - public DubboBookOrderService(BookOrderApplicationService applicationService, - OrderEventApplicationService eventService) { - this.applicationService = applicationService; - this.eventService = eventService; - } - - @Override public BookOrderResponse createOrder(BookOrderRequest request) { - return applicationService.createOrder(request); - } - - @Override public BookOrderResponse fireEvent(OrderEventRequest request) { - return eventService.fireEvent(request); - } -} diff --git a/architecture/src/main/java/com/arch/policy/book/infrastructure/seata/SeataOrderCreationSaga.java b/architecture/src/main/java/com/arch/policy/book/infrastructure/seata/SeataOrderCreationSaga.java deleted file mode 100644 index 1c22b60..0000000 --- a/architecture/src/main/java/com/arch/policy/book/infrastructure/seata/SeataOrderCreationSaga.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.arch.policy.book.infrastructure.seata; - -import com.arch.policy.book.application.OrderCreationSaga; -import io.seata.saga.engine.StateMachineEngine; -import io.seata.saga.statelang.domain.StateMachineInstance; - -import java.util.HashMap; -import java.util.Map; - -public final class SeataOrderCreationSaga implements OrderCreationSaga { - public static final String STATE_MACHINE_NAME = "BookOrderCreationSaga"; - private final StateMachineEngine stateMachineEngine; - private final String tenantId; - - public SeataOrderCreationSaga(StateMachineEngine stateMachineEngine, String tenantId) { - this.stateMachineEngine = stateMachineEngine; - this.tenantId = tenantId; - } - - /** - * 启动订单创建 Saga:调用 GDS 占编,并根据结果推进订单状态或登记补偿任务。 - */ - @Override public SagaStartResult start(String orderNo, String promotionId) { - // Saga 状态节点通过上下文读取订单号和促销活动 ID,作为服务方法的入参。 - Map context = new HashMap(); - context.put("orderNo", orderNo); - // Seata 表达式和服务参数统一使用空字符串,避免 null 在状态机流转中产生歧义。 - context.put("promotionId", promotionId == null ? "" : promotionId); - - // orderNo 作为业务键标识该订单的 Saga 实例,便于恢复任务关联同一笔订单流程。 - StateMachineInstance instance = stateMachineEngine.startWithBusinessKey( - STATE_MACHINE_NAME, tenantId, orderNo, context); - - // 向上层返回 Saga 实例 ID,以及状态机是否仍处于运行中。 - return new SagaStartResult(instance.getId(), instance.isRunning()); - } -} diff --git a/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java b/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java deleted file mode 100644 index 08b6962..0000000 --- a/architecture/src/main/java/com/arch/policy/common/book/BookOrderRequest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.arch.policy.common.book; - -import java.io.Serializable; -import java.math.BigDecimal; - -public final class BookOrderRequest implements Serializable { - private static final long serialVersionUID = 1L; - private String requestId; - private String customerId; - private String productId; - private int quantity; - private BigDecimal amount; - private String promotionId; - - public String getRequestId() { return requestId; } - public void setRequestId(String requestId) { this.requestId = requestId; } - public String getCustomerId() { return customerId; } - public void setCustomerId(String customerId) { this.customerId = customerId; } - public String getProductId() { return productId; } - public void setProductId(String productId) { this.productId = productId; } - public int getQuantity() { return quantity; } - public void setQuantity(int quantity) { this.quantity = quantity; } - public BigDecimal getAmount() { return amount; } - public void setAmount(BigDecimal amount) { this.amount = amount; } - public String getPromotionId() { return promotionId; } - public void setPromotionId(String promotionId) { this.promotionId = promotionId; } -} diff --git a/architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java b/architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java deleted file mode 100644 index 4845559..0000000 --- a/architecture/src/main/java/com/arch/policy/common/book/BookOrderResponse.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.arch.policy.common.book; - -import java.io.Serializable; - -public final class BookOrderResponse implements Serializable { - private static final long serialVersionUID = 1L; - private final String orderNo; - private final String state; - private final long version; - - public BookOrderResponse(String orderNo, String state, long version) { - this.orderNo = orderNo; - this.state = state; - this.version = version; - } - - public String getOrderNo() { return orderNo; } - public String getState() { return state; } - public long getVersion() { return version; } -} diff --git a/architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java b/architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java deleted file mode 100644 index 0e39d3a..0000000 --- a/architecture/src/main/java/com/arch/policy/common/book/OrderEventRequest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.arch.policy.common.book; - -import java.io.Serializable; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -public final class OrderEventRequest implements Serializable { - private static final long serialVersionUID = 1L; - private String eventId; - private String orderNo; - private String event; - private String operator; - private long expectedVersion; - private Map attributes = new HashMap(); - - public String getEventId() { return eventId; } - public void setEventId(String eventId) { this.eventId = eventId; } - public String getOrderNo() { return orderNo; } - public void setOrderNo(String orderNo) { this.orderNo = orderNo; } - public String getEvent() { return event; } - public void setEvent(String event) { this.event = event; } - public String getOperator() { return operator; } - public void setOperator(String operator) { this.operator = operator; } - public long getExpectedVersion() { return expectedVersion; } - public void setExpectedVersion(long expectedVersion) { this.expectedVersion = expectedVersion; } - public Map getAttributes() { return Collections.unmodifiableMap(attributes); } - public void setAttributes(Map attributes) { - this.attributes = attributes == null - ? new HashMap() : new HashMap(attributes); - } -} diff --git a/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java b/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java deleted file mode 100644 index 3dfe543..0000000 --- a/architecture/src/main/java/com/arch/policy/common/config/BookOrderConfiguration.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.arch.policy.common.config; - -import com.arch.policy.book.application.BookOrderApplicationService; -import com.arch.policy.book.application.BookOrderStore; -import com.arch.policy.book.application.CompensationTaskService; -import com.arch.policy.book.application.CreateOrderResultCache; -import com.arch.policy.book.application.DirectOrderCreationSaga; -import com.arch.policy.book.application.GdsBookingGateway; -import com.arch.policy.book.application.GdsReconciliationService; -import com.arch.policy.book.application.OrderCreationSaga; -import com.arch.policy.book.application.OrderCreationOutboxPublisher; -import com.arch.policy.book.application.OrderCreationSagaStateServices; -import com.arch.policy.book.application.OrderEventApplicationService; -import com.arch.policy.book.application.OrderWorkflowTaskStore; -import com.arch.policy.book.application.PaymentGateway; -import com.arch.policy.book.application.PostTransitionExecutor; -import com.arch.policy.book.application.PromotionStockGateway; -import com.arch.policy.book.application.RecoveryTaskProcessor; -import com.arch.policy.book.application.RecoveryTaskStore; -import com.arch.policy.book.application.RetryablePostTransitionExecutor; -import com.arch.policy.book.domain.DefaultOrderTransitions; -import com.arch.policy.book.domain.OrderStateMachine; -import com.arch.policy.book.infrastructure.demo.DemoGdsBookingGateway; -import com.arch.policy.book.infrastructure.job.OrderRecoveryScheduler; -import com.arch.policy.book.infrastructure.job.OrderCreationOutboxScheduler; -import com.arch.policy.book.infrastructure.repository.InMemoryBookOrderStore; -import com.arch.policy.book.infrastructure.repository.InMemoryFailedPostActionStore; -import com.arch.policy.book.infrastructure.repository.InMemoryOrderWorkflowTaskStore; -import com.arch.policy.book.infrastructure.repository.InMemoryRecoveryTaskStore; -import com.arch.policy.book.infrastructure.cache.DefaultCreateOrderResultCache; -import com.arch.policy.common.infrastructure.redis.RedissonDistributedLock; -import com.arch.policy.common.infrastructure.redis.RedissonRedisClient; -import com.arch.policy.common.lock.DistributedLock; -import com.arch.policy.common.redis.RedisClient; -import com.arch.policy.book.infrastructure.seata.SeataOrderCreationSaga; -import io.seata.saga.engine.StateMachineEngine; -import org.redisson.api.RedissonClient; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.annotation.EnableScheduling; - -@Configuration -@EnableScheduling -public class BookOrderConfiguration { - @Bean public OrderStateMachine orderStateMachine() { - return new OrderStateMachine(DefaultOrderTransitions.definitions()); - } - - @Bean public BookOrderStore bookOrderStore() { return new InMemoryBookOrderStore(); } - @Bean public DistributedLock distributedLock(RedissonClient redisson) { - return new RedissonDistributedLock(redisson); - } - @Bean public RedisClient redisClient(RedissonClient redisson) { - return new RedissonRedisClient(redisson); - } - @Bean public CreateOrderResultCache createOrderResultCache(RedisClient redisClient) { - return new DefaultCreateOrderResultCache(redisClient); - } - @Bean public RecoveryTaskStore recoveryTaskStore() { return new InMemoryRecoveryTaskStore(); } - @Bean public OrderWorkflowTaskStore orderWorkflowTaskStore() { - return new InMemoryOrderWorkflowTaskStore(); - } - @Bean public InMemoryFailedPostActionStore failedPostActionStore() { - return new InMemoryFailedPostActionStore(); - } - @Bean public PostTransitionExecutor postTransitionExecutor( - InMemoryFailedPostActionStore failedPostActionStore) { - return new RetryablePostTransitionExecutor(failedPostActionStore); - } - @Bean public OrderEventApplicationService orderEventApplicationService( - BookOrderStore store, OrderStateMachine stateMachine, - PostTransitionExecutor postTransitionExecutor, - CompensationTaskService compensationTaskService, - CreateOrderResultCache resultCache) { - return new OrderEventApplicationService(store, stateMachine, postTransitionExecutor, - compensationTaskService, resultCache); - } - @Bean public GdsBookingGateway gdsBookingGateway() { return new DemoGdsBookingGateway(); } - @Bean public PromotionStockGateway promotionStockGateway() { - return (orderNo, promotionId) -> { }; - } - @Bean public PaymentGateway paymentGateway() { return (orderNo, paymentNo) -> { }; } - @Bean public CompensationTaskService compensationTaskService(RecoveryTaskStore store) { - return new CompensationTaskService(store); - } - @Bean(name = "orderCreationSagaStateServices") - public OrderCreationSagaStateServices orderCreationSagaStateServices( - BookOrderStore store, GdsBookingGateway gdsGateway, - OrderEventApplicationService eventService, CompensationTaskService compensationService, - OrderWorkflowTaskStore workflowTaskStore) { - return new OrderCreationSagaStateServices(store, gdsGateway, eventService, - compensationService, workflowTaskStore); - } - @Bean public OrderCreationSaga orderCreationSaga( - ObjectProvider engineProvider, - OrderCreationSagaStateServices services, - @Value("${book.seata.tenant-id:book}") String tenantId) { - StateMachineEngine engine = engineProvider.getIfAvailable(); - return engine == null ? new DirectOrderCreationSaga(services) - : new SeataOrderCreationSaga(engine, tenantId); - } - @Bean public BookOrderApplicationService bookOrderApplicationService( - BookOrderStore store, OrderCreationOutboxPublisher outboxPublisher, - DistributedLock distributedLock, - CreateOrderResultCache resultCache) { - return new BookOrderApplicationService(store, outboxPublisher, distributedLock, - resultCache); - } - @Bean public OrderCreationOutboxPublisher orderCreationOutboxPublisher( - BookOrderStore store, OrderCreationSaga saga) { - return new OrderCreationOutboxPublisher(store, saga); - } - @Bean public OrderCreationOutboxScheduler orderCreationOutboxScheduler( - OrderCreationOutboxPublisher publisher) { - return new OrderCreationOutboxScheduler(publisher); - } - @Bean public GdsReconciliationService gdsReconciliationService( - GdsBookingGateway gdsGateway, OrderCreationSagaStateServices sagaStates) { - return new GdsReconciliationService(gdsGateway, sagaStates); - } - @Bean public RecoveryTaskProcessor recoveryTaskProcessor( - RecoveryTaskStore taskStore, PromotionStockGateway promotionGateway, - GdsBookingGateway gdsGateway, PaymentGateway paymentGateway, - GdsReconciliationService reconciliationService) { - return new RecoveryTaskProcessor(taskStore, promotionGateway, gdsGateway, - paymentGateway, reconciliationService); - } - @Bean public OrderRecoveryScheduler orderRecoveryScheduler(RecoveryTaskProcessor processor) { - return new OrderRecoveryScheduler(processor); - } -} diff --git a/architecture/src/main/java/com/arch/policy/common/config/SeataSagaConfiguration.java b/architecture/src/main/java/com/arch/policy/common/config/SeataSagaConfiguration.java index 2aa68e8..94cd985 100644 --- a/architecture/src/main/java/com/arch/policy/common/config/SeataSagaConfiguration.java +++ b/architecture/src/main/java/com/arch/policy/common/config/SeataSagaConfiguration.java @@ -28,8 +28,8 @@ public ThreadPoolExecutor seataSagaExecutor() { public DbStateMachineConfig dbStateMachineConfig( DataSource dataSource, ThreadPoolExecutor seataSagaExecutor, @Value("${spring.application.name:architecture}") String applicationId, - @Value("${book.seata.tx-service-group:book-saga-group}") String txServiceGroup, - @Value("${book.seata.tenant-id:book}") String tenantId) { + @Value("${order.seata.tx-service-group:order-saga-group}") String txServiceGroup, + @Value("${order.seata.tenant-id:order}") String tenantId) { DbStateMachineConfig config = new DbStateMachineConfig(); config.setDataSource(dataSource); config.setApplicationId(applicationId); @@ -37,7 +37,7 @@ public DbStateMachineConfig dbStateMachineConfig( config.setDefaultTenantId(tenantId); config.setThreadPoolExecutor(seataSagaExecutor); config.setAutoRegisterResources(true); - config.setResources(new String[] { "classpath*:statelang/book_order_creation_saga.json" }); + config.setResources(new String[] { "classpath*:statelang/order_creation_saga.json" }); config.setSagaJsonParser("jackson"); config.setSagaBranchRegisterEnable(true); return config; diff --git a/architecture/src/main/java/com/arch/policy/order/cache/OrderCacheUtil.java b/architecture/src/main/java/com/arch/policy/order/cache/OrderCacheUtil.java new file mode 100644 index 0000000..6a2e363 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/cache/OrderCacheUtil.java @@ -0,0 +1,47 @@ +package com.arch.policy.order.cache; + +import com.arch.policy.order.model.OrderResponse; +import org.redisson.api.RedissonClient; +import org.redisson.client.RedisException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @Author : haiyang.luo + * @Date : 2026/7/22 15:58 + * @Description : + */ +@Component +public class OrderCacheUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(OrderCacheUtil.class); + + @Autowired + private RedissonClient redissonClient; + + public OrderResponse getCache(String orderSerialNo) { + try { + return redissonClient.getBucket(cacheKey(orderSerialNo)).get(); + } catch (RedisException redisFailure) { + LOGGER.warn("Create-order result cache unavailable, orderSerialNo={}", orderSerialNo, + redisFailure); + return null; + } + } + + public void setCache(String orderSerialNo, OrderResponse response) { + try { + redissonClient.getBucket(cacheKey(orderSerialNo)).set(response, 600_000L, + java.util.concurrent.TimeUnit.MILLISECONDS); + } catch (RedisException redisFailure) { + LOGGER.warn("Create-order result cache unavailable, orderSerialNo={}", orderSerialNo, + redisFailure); + } + } + + private String cacheKey(String orderSerialNo) { + return "order:create:result:" + orderSerialNo; + } +} diff --git a/architecture/src/main/java/com/arch/policy/order/cache/OrderLockUtil.java b/architecture/src/main/java/com/arch/policy/order/cache/OrderLockUtil.java new file mode 100644 index 0000000..2f1bd36 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/cache/OrderLockUtil.java @@ -0,0 +1,45 @@ +package com.arch.policy.order.cache; + +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.redisson.client.RedisException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +@Component +public class OrderLockUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(OrderLockUtil.class); + + @Resource + private RedissonClient redissonClient; + + public boolean tryLock(String orderSerialNo) { + RLock lock = redissonClient.getLock("order:create:lock:" + orderSerialNo); + try { + // 不设置固定过期时间,使用Redisson watchdog自动续期;只锁本地建单,不锁三方调用。 + return lock.tryLock(300L, TimeUnit.MILLISECONDS); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + return false; + } catch (RedisException redisFailure) { + LOGGER.warn("Create-order lock unavailable, orderSerialNo={}", orderSerialNo, + redisFailure); + // Redis故障时继续执行,由订单库唯一索引保证最终幂等。 + return true; + } + } + + public void unlock(String orderSerialNo) { + RLock lock = redissonClient.getLock("order:create:lock:" + orderSerialNo); + try { + if (lock.isHeldByCurrentThread()) lock.unlock(); + } catch (RedisException redisFailure) { + LOGGER.warn("Release create-order lock failed, orderSerialNo={}", orderSerialNo, + redisFailure); + } + } +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/InventoryTransaction.java b/architecture/src/main/java/com/arch/policy/order/model/InventoryTransaction.java new file mode 100644 index 0000000..40231bc --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/InventoryTransaction.java @@ -0,0 +1,23 @@ +package com.arch.policy.order.model; + +/** 库存库中的幂等扣减流水。 */ +public class InventoryTransaction { + private final String tradeOrderSerialNo; + private final String skuId; + private final int quantity; + private InventoryTransactionStatus status; + + public InventoryTransaction(String tradeOrderSerialNo, String skuId, int quantity, + InventoryTransactionStatus status) { + this.tradeOrderSerialNo = tradeOrderSerialNo; + this.skuId = skuId; + this.quantity = quantity; + this.status = status; + } + + public String getTradeOrderSerialNo() { return tradeOrderSerialNo; } + public String getSkuId() { return skuId; } + public int getQuantity() { return quantity; } + public InventoryTransactionStatus getStatus() { return status; } + public void setStatus(InventoryTransactionStatus status) { this.status = status; } +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/InventoryTransactionStatus.java b/architecture/src/main/java/com/arch/policy/order/model/InventoryTransactionStatus.java new file mode 100644 index 0000000..e55c393 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/InventoryTransactionStatus.java @@ -0,0 +1,25 @@ +package com.arch.policy.order.model; + +/** 库存库流水状态;库存库是该状态的权威来源。 */ +public enum InventoryTransactionStatus { + /** 正在执行库存扣减,最终结果尚未确定。 */ + DEDUCTING, + + /** 库存已经扣减,等待订单创建结果决定确认或返还。 */ + DEDUCTED, + + /** 三方订单创建成功,库存扣减已经最终确认。 */ + CONFIRMED, + + /** 正在执行库存返还,最终结果尚未确定。 */ + RETURNING, + + /** 库存已经幂等返还。 */ + RETURNED, + + /** 可用库存不足,未执行库存扣减。 */ + INSUFFICIENT, + + /** 库存操作超时或异常,需要查询库存流水确认结果。 */ + UNKNOWN +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/OrderRequest.java b/architecture/src/main/java/com/arch/policy/order/model/OrderRequest.java new file mode 100644 index 0000000..5107143 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/OrderRequest.java @@ -0,0 +1,65 @@ +package com.arch.policy.order.model; + +import java.io.Serializable; + +/** + * @Author : haiyang.luo + * @Date : 2026/7/22 15:46 + * @Description : + */ +public class OrderRequest implements Serializable { + + private static final long serialVersionUID = 6809741055680415850L; + + /** + * 主订单 + */ + private String orderSerialNo; + + /** + * 供应商ID + */ + private String supplierId; + + /** + * 商品ID + */ + private String skuId; + + /** + * 下单数量 + */ + private int quantity; + + public String getOrderSerialNo() { + return orderSerialNo; + } + + public void setOrderSerialNo(String orderSerialNo) { + this.orderSerialNo = orderSerialNo; + } + + public String getSupplierId() { + return supplierId; + } + + public void setSupplierId(String supplierId) { + this.supplierId = supplierId; + } + + public String getSkuId() { + return skuId; + } + + public void setSkuId(String skuId) { + this.skuId = skuId; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/OrderResponse.java b/architecture/src/main/java/com/arch/policy/order/model/OrderResponse.java new file mode 100644 index 0000000..d4972da --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/OrderResponse.java @@ -0,0 +1,30 @@ +package com.arch.policy.order.model; + +import java.io.Serializable; + +/** + * @Author : haiyang.luo + * @Date : 2026/7/22 15:45 + * @Description : + */ +public class OrderResponse implements Serializable { + + private static final long serialVersionUID = -655337401948649968L; + + private String orderSerialNo; + private String tradeOrderSerialNo; + private String thirdPartyOrderNo; + private String status; + private String message; + + public String getOrderSerialNo() { return orderSerialNo; } + public void setOrderSerialNo(String orderSerialNo) { this.orderSerialNo = orderSerialNo; } + public String getTradeOrderSerialNo() { return tradeOrderSerialNo; } + public void setTradeOrderSerialNo(String tradeOrderSerialNo) { this.tradeOrderSerialNo = tradeOrderSerialNo; } + public String getThirdPartyOrderNo() { return thirdPartyOrderNo; } + public void setThirdPartyOrderNo(String thirdPartyOrderNo) { this.thirdPartyOrderNo = thirdPartyOrderNo; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/OrderStatus.java b/architecture/src/main/java/com/arch/policy/order/model/OrderStatus.java new file mode 100644 index 0000000..60716ca --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/OrderStatus.java @@ -0,0 +1,25 @@ +package com.arch.policy.order.model; + +/** 只表达订单业务生命周期,不承载库存、三方或 Saga 技术状态。 */ +public enum OrderStatus { + /** 本地订单已经创建,创建流程尚未得出最终业务结果。 */ + CREATE, + + /** 订单创建成功,正在等待用户完成支付。 */ + WAIT_PAY, + + /** 支付完成,正在向供应商执行预订。 */ + BOOKING, + + /** 供应商已经明确返回预订成功。 */ + BOOKED, + + /** 供应商明确返回预订失败。 */ + BOOK_FAIL, + + /** 订单支付失败。 */ + PAY_FAIL, + + /** 订单创建失败,例如库存不足或三方创建明确失败。 */ + CREATE_FAIL +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/SupplierOrder.java b/architecture/src/main/java/com/arch/policy/order/model/SupplierOrder.java new file mode 100644 index 0000000..a282589 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/SupplierOrder.java @@ -0,0 +1,26 @@ +package com.arch.policy.order.model; + +/** 订单库中的供应商订单关联记录,与 TradeOrder 通过子订单号关联。 */ +public class SupplierOrder { + private final String tradeOrderSerialNo; + private final String supplierId; + private final String requestNo; + private String thirdPartyOrderNo; + private SupplierOrderStatus status; + + public SupplierOrder(String tradeOrderSerialNo, String supplierId, String requestNo, + SupplierOrderStatus status) { + this.tradeOrderSerialNo = tradeOrderSerialNo; + this.supplierId = supplierId; + this.requestNo = requestNo; + this.status = status; + } + + public String getTradeOrderSerialNo() { return tradeOrderSerialNo; } + public String getSupplierId() { return supplierId; } + public String getRequestNo() { return requestNo; } + public String getThirdPartyOrderNo() { return thirdPartyOrderNo; } + public void setThirdPartyOrderNo(String thirdPartyOrderNo) { this.thirdPartyOrderNo = thirdPartyOrderNo; } + public SupplierOrderStatus getStatus() { return status; } + public void setStatus(SupplierOrderStatus status) { this.status = status; } +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/SupplierOrderStatus.java b/architecture/src/main/java/com/arch/policy/order/model/SupplierOrderStatus.java new file mode 100644 index 0000000..697e9ba --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/SupplierOrderStatus.java @@ -0,0 +1,28 @@ +package com.arch.policy.order.model; + +/** 本地记录的供应商订单协作状态。 */ +public enum SupplierOrderStatus { + /** 供应商订单关联记录已经创建,尚未调用三方接口。 */ + INIT, + + /** 正在使用本地子订单号作为幂等号调用三方创建接口。 */ + CREATING, + + /** 三方已经受理请求,但订单仍处于处理中。 */ + PENDING, + + /** 三方已经明确确认订单创建成功。 */ + SUCCESS, + + /** 三方已经明确确认订单创建失败。 */ + FAILED, + + /** 调用超时或响应不明确,需要主动查询三方最终状态。 */ + UNKNOWN, + + /** 正在调用三方订单取消接口。 */ + CANCELING, + + /** 三方已经明确确认订单取消成功。 */ + CANCELED +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/ThirdOrderResult.java b/architecture/src/main/java/com/arch/policy/order/model/ThirdOrderResult.java new file mode 100644 index 0000000..c861e4d --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/ThirdOrderResult.java @@ -0,0 +1,14 @@ +package com.arch.policy.order.model; + +public class ThirdOrderResult { + private final String thirdPartyOrderNo; + private final ThirdOrderStatus status; + + public ThirdOrderResult(String thirdPartyOrderNo, ThirdOrderStatus status) { + this.thirdPartyOrderNo = thirdPartyOrderNo; + this.status = status; + } + + public String getThirdPartyOrderNo() { return thirdPartyOrderNo; } + public ThirdOrderStatus getStatus() { return status; } +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/ThirdOrderStatus.java b/architecture/src/main/java/com/arch/policy/order/model/ThirdOrderStatus.java new file mode 100644 index 0000000..19e4275 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/ThirdOrderStatus.java @@ -0,0 +1,13 @@ +package com.arch.policy.order.model; + +/** 三方订单结果;超时和处理中统一按 UNKNOWN 处理,不能直接返还库存。 */ +public enum ThirdOrderStatus { + /** 三方接口明确返回业务处理成功。 */ + SUCCESS, + + /** 三方接口明确返回业务处理失败,且不会转为成功。 */ + FAILED, + + /** 三方处理中、调用超时或响应无法确认最终业务结果。 */ + UNKNOWN +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/TradeOrder.java b/architecture/src/main/java/com/arch/policy/order/model/TradeOrder.java new file mode 100644 index 0000000..65bbec8 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/TradeOrder.java @@ -0,0 +1,23 @@ +package com.arch.policy.order.model; + +/** 本地子订单的最小快照。 */ +public class TradeOrder { + private final String orderSerialNo; + private final String tradeOrderSerialNo; + private final String supplierId; + private OrderStatus status; + + public TradeOrder(String orderSerialNo, String tradeOrderSerialNo, String supplierId, + OrderStatus status) { + this.orderSerialNo = orderSerialNo; + this.tradeOrderSerialNo = tradeOrderSerialNo; + this.supplierId = supplierId; + this.status = status; + } + + public String getOrderSerialNo() { return orderSerialNo; } + public String getTradeOrderSerialNo() { return tradeOrderSerialNo; } + public String getSupplierId() { return supplierId; } + public OrderStatus getStatus() { return status; } + public void setStatus(OrderStatus status) { this.status = status; } +} diff --git a/architecture/src/main/java/com/arch/policy/order/service/InventoryTransactionService.java b/architecture/src/main/java/com/arch/policy/order/service/InventoryTransactionService.java new file mode 100644 index 0000000..7143c9b --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/service/InventoryTransactionService.java @@ -0,0 +1,80 @@ +package com.arch.policy.order.service; + +import com.arch.policy.order.model.OrderRequest; +import com.arch.policy.order.model.InventoryTransaction; +import com.arch.policy.order.model.InventoryTransactionStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** 只负责库存库事务:正常扣库存并记录幂等库存流水,不使用预占库存。 */ +@Service +public class InventoryTransactionService { + private static final Logger LOGGER = LoggerFactory.getLogger(InventoryTransactionService.class); + private final ConcurrentMap transactions = + new ConcurrentHashMap(); + + public boolean deduct(String tradeOrderSerialNo, OrderRequest request) { + LOGGER.info("[库存库事务-开始] 查询库存流水, tradeOrderSerialNo={}, skuId={}", + tradeOrderSerialNo, request.getSkuId()); + LOGGER.info("[库存库] 原子扣减库存: stock = stock - {}, 条件 stock >= {}, skuId={}", + request.getQuantity(), request.getQuantity(), request.getSkuId()); + InventoryTransaction created = new InventoryTransaction(tradeOrderSerialNo, + request.getSkuId(), request.getQuantity(), InventoryTransactionStatus.DEDUCTED); + InventoryTransaction existing = transactions.putIfAbsent(tradeOrderSerialNo, created); + InventoryTransaction transaction = existing == null ? created : existing; + LOGGER.info("[库存库] 幂等写库存流水, tradeOrderSerialNo={}, status={}", + tradeOrderSerialNo, transaction.getStatus()); + LOGGER.info("[库存库事务-提交] 库存扣减和DEDUCTED流水同时提交"); + return transaction.getStatus() == InventoryTransactionStatus.DEDUCTED + || transaction.getStatus() == InventoryTransactionStatus.CONFIRMED; + } + + public void confirm(String tradeOrderSerialNo) { + InventoryTransaction transaction = required(tradeOrderSerialNo); + synchronized (transaction) { + if (transaction.getStatus() == InventoryTransactionStatus.DEDUCTED) { + transaction.setStatus(InventoryTransactionStatus.CONFIRMED); + } else if (transaction.getStatus() != InventoryTransactionStatus.CONFIRMED) { + throw new IllegalStateException("inventory cannot be confirmed from status: " + + transaction.getStatus()); + } + } + LOGGER.info("[库存库事务] 库存流水 {} -> {}, tradeOrderSerialNo={}", + InventoryTransactionStatus.DEDUCTED, InventoryTransactionStatus.CONFIRMED, + tradeOrderSerialNo); + } + + public void returnStock(String tradeOrderSerialNo) { + InventoryTransaction transaction = required(tradeOrderSerialNo); + synchronized (transaction) { + if (transaction.getStatus() == InventoryTransactionStatus.DEDUCTED) { + transaction.setStatus(InventoryTransactionStatus.RETURNED); + } else if (transaction.getStatus() != InventoryTransactionStatus.RETURNED) { + throw new IllegalStateException("inventory cannot be returned from status: " + + transaction.getStatus()); + } + } + LOGGER.info("[库存库事务-开始] 库存流水仅允许 {} -> {}, tradeOrderSerialNo={}", + InventoryTransactionStatus.DEDUCTED, InventoryTransactionStatus.RETURNED, + tradeOrderSerialNo); + LOGGER.info("[库存库] 只有流水状态更新成功才执行 stock = stock + quantity,防止重复返还"); + LOGGER.info("[库存库事务-提交] 库存返还和RETURNED流水同时提交"); + } + + public InventoryTransaction findByTradeOrderSerialNo(String tradeOrderSerialNo) { + return transactions.get(tradeOrderSerialNo); + } + + private InventoryTransaction required(String tradeOrderSerialNo) { + InventoryTransaction transaction = transactions.get(tradeOrderSerialNo); + if (transaction == null) { + throw new IllegalStateException("inventory transaction not found: " + + tradeOrderSerialNo); + } + return transaction; + } +} diff --git a/architecture/src/main/java/com/arch/policy/order/service/OrderCreationSaga.java b/architecture/src/main/java/com/arch/policy/order/service/OrderCreationSaga.java new file mode 100644 index 0000000..8721d4d --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/service/OrderCreationSaga.java @@ -0,0 +1,9 @@ +package com.arch.policy.order.service; + +import com.arch.policy.order.model.OrderRequest; +import com.arch.policy.order.model.TradeOrder; + +/** 本地订单提交后的跨库、跨三方创建流程。 */ +public interface OrderCreationSaga { + void start(TradeOrder order, OrderRequest request); +} diff --git a/architecture/src/main/java/com/arch/policy/order/service/OrderCreationSagaStateServices.java b/architecture/src/main/java/com/arch/policy/order/service/OrderCreationSagaStateServices.java new file mode 100644 index 0000000..d464d57 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/service/OrderCreationSagaStateServices.java @@ -0,0 +1,74 @@ +package com.arch.policy.order.service; + +import com.arch.policy.order.model.OrderRequest; +import com.arch.policy.order.model.OrderStatus; +import com.arch.policy.order.model.SupplierOrderStatus; +import com.arch.policy.order.model.ThirdOrderResult; +import com.arch.policy.order.model.ThirdOrderStatus; +import com.arch.policy.order.model.TradeOrder; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** Seata 状态节点调用的短事务服务;未知三方结果不会错误返还库存。 */ +@Service("orderCreationSagaStateServices") +public final class OrderCreationSagaStateServices { + @Resource + private OrderTransactionService orderTransactionService; + @Resource + private InventoryTransactionService inventoryTransactionService; + @Resource + private SupplierOrderService supplierOrderService; + @Resource + private SupplierOrderTransactionService supplierOrderTransactionService; + @Resource + private OrderRecoveryService orderRecoveryService; + + public boolean deductStock(TradeOrder order, OrderRequest request) { + boolean deducted = inventoryTransactionService.deduct( + order.getTradeOrderSerialNo(), request); + return deducted; + } + + public ThirdOrderResult createThirdOrder(TradeOrder order, OrderRequest request) { + supplierOrderTransactionService.markCreating(order); + ThirdOrderResult result; + try { + result = supplierOrderService.createOrder(order.getTradeOrderSerialNo(), request); + } catch (RuntimeException uncertainFailure) { + // 网络异常只代表结果未知,不能当作三方明确失败并返还库存。 + result = new ThirdOrderResult(null, ThirdOrderStatus.UNKNOWN); + } + supplierOrderTransactionService.saveResult(order.getTradeOrderSerialNo(), + result.getThirdPartyOrderNo(), statusOf(result.getStatus())); + return result; + } + + public boolean completeSuccess(TradeOrder order) { + inventoryTransactionService.confirm(order.getTradeOrderSerialNo()); + orderTransactionService.updateStatus(order, OrderStatus.WAIT_PAY); + return true; + } + + public boolean completeFailure(TradeOrder order) { + inventoryTransactionService.returnStock(order.getTradeOrderSerialNo()); + orderTransactionService.updateStatus(order, OrderStatus.CREATE_FAIL); + return true; + } + + public boolean completeStockFailure(TradeOrder order) { + orderTransactionService.updateStatus(order, OrderStatus.CREATE_FAIL); + return true; + } + + public boolean waitForThirdResult(TradeOrder order) { + orderRecoveryService.createThirdOrderQueryTask(order); + return true; + } + + private static SupplierOrderStatus statusOf(ThirdOrderStatus status) { + if (status == ThirdOrderStatus.SUCCESS) return SupplierOrderStatus.SUCCESS; + if (status == ThirdOrderStatus.FAILED) return SupplierOrderStatus.FAILED; + return SupplierOrderStatus.UNKNOWN; + } +} diff --git a/architecture/src/main/java/com/arch/policy/order/service/OrderRecoveryService.java b/architecture/src/main/java/com/arch/policy/order/service/OrderRecoveryService.java new file mode 100644 index 0000000..610dadd --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/service/OrderRecoveryService.java @@ -0,0 +1,67 @@ +package com.arch.policy.order.service; + +import com.arch.policy.order.model.OrderStatus; +import com.arch.policy.order.model.SupplierOrder; +import com.arch.policy.order.model.SupplierOrderStatus; +import com.arch.policy.order.model.ThirdOrderResult; +import com.arch.policy.order.model.ThirdOrderStatus; +import com.arch.policy.order.model.TradeOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** 对三方超时、处理中等未知结果登记查询恢复任务。 */ +@Service +public class OrderRecoveryService { + private static final Logger LOGGER = LoggerFactory.getLogger(OrderRecoveryService.class); + + @Resource + private SupplierOrderService supplierOrderService; + @Resource + private InventoryTransactionService inventoryTransactionService; + @Resource + private OrderTransactionService orderTransactionService; + @Resource + private SupplierOrderTransactionService supplierOrderTransactionService; + + public void createThirdOrderQueryTask(TradeOrder order) { + SupplierOrder supplierOrder = supplierOrderTransactionService.findByTradeOrderSerialNo( + order.getTradeOrderSerialNo()); + LOGGER.info("[订单库事务] 创建三方订单查询任务,保持库存已扣状态,不立即返还, " + + "tradeOrderSerialNo={}, thirdPartyOrderNo={}", + order.getTradeOrderSerialNo(), thirdPartyOrderNo(supplierOrder)); + } + + /** 定时任务消费查询任务时执行;这里保留成普通方法突出恢复流程。 */ + public void reconcileThirdOrder(TradeOrder order) { + SupplierOrder supplierOrder = supplierOrderTransactionService.findByTradeOrderSerialNo( + order.getTradeOrderSerialNo()); + ThirdOrderResult result = supplierOrderService.queryOrder(order.getTradeOrderSerialNo(), + thirdPartyOrderNo(supplierOrder), order.getSupplierId()); + + if (result.getStatus() == ThirdOrderStatus.SUCCESS) { + supplierOrderTransactionService.saveResult(order.getTradeOrderSerialNo(), + result.getThirdPartyOrderNo(), SupplierOrderStatus.SUCCESS); + inventoryTransactionService.confirm(order.getTradeOrderSerialNo()); + orderTransactionService.updateStatus(order, OrderStatus.WAIT_PAY); + return; + } + + if (result.getStatus() == ThirdOrderStatus.FAILED) { + supplierOrderTransactionService.saveResult(order.getTradeOrderSerialNo(), + result.getThirdPartyOrderNo(), SupplierOrderStatus.FAILED); + inventoryTransactionService.returnStock(order.getTradeOrderSerialNo()); + orderTransactionService.updateStatus(order, OrderStatus.CREATE_FAIL); + return; + } + + LOGGER.info("[恢复任务] 三方状态仍未知,保留已扣库存并等待下次查询, tradeOrderSerialNo={}", + order.getTradeOrderSerialNo()); + } + + private static String thirdPartyOrderNo(SupplierOrder supplierOrder) { + return supplierOrder == null ? null : supplierOrder.getThirdPartyOrderNo(); + } +} diff --git a/architecture/src/main/java/com/arch/policy/order/service/OrderRpcServiceImpl.java b/architecture/src/main/java/com/arch/policy/order/service/OrderRpcServiceImpl.java new file mode 100644 index 0000000..47d40f0 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/service/OrderRpcServiceImpl.java @@ -0,0 +1,116 @@ +package com.arch.policy.order.service; + +import com.arch.policy.api.order.OrderRpcService; +import com.arch.policy.order.cache.OrderCacheUtil; +import com.arch.policy.order.cache.OrderLockUtil; +import com.arch.policy.order.model.OrderRequest; +import com.arch.policy.order.model.OrderResponse; +import com.arch.policy.order.model.OrderStatus; +import com.arch.policy.order.model.SupplierOrder; +import com.arch.policy.order.model.TradeOrder; +import org.apache.dubbo.config.annotation.DubboService; + +import javax.annotation.Resource; + +/** 创建本地子订单,并在本地事务提交后同步启动 Seata Saga。 */ +@DubboService(version = "1.0.0", timeout = 3000) +public class OrderRpcServiceImpl implements OrderRpcService { + @Resource + private OrderCacheUtil orderCacheUtil; + @Resource + private OrderLockUtil orderLockUtil; + @Resource + private OrderTransactionService orderTransactionService; + @Resource + private OrderCreationSaga orderCreationSaga; + @Resource + private SupplierOrderTransactionService supplierOrderTransactionService; + + @Override + public OrderResponse createOrder(OrderRequest request) { + validate(request); + String businessKey = businessKey(request); + + OrderResponse cached = orderCacheUtil.getCache(businessKey); + if (cached != null) return cached; + + if (!orderLockUtil.tryLock(businessKey)) { + return processing(request, "相同下单请求正在处理中"); + } + + try { + cached = orderCacheUtil.getCache(businessKey); + if (cached != null) return cached; + + TradeOrder order = orderTransactionService.findByRequest( + request.getOrderSerialNo(), request.getSupplierId()); + if (order != null) { + return cacheAndReturn(businessKey, order, "返回已存在的幂等订单"); + } else { + // 这里只提交订单库本地事务,不写 Outbox,也不在事务中调用库存或三方。 + order = orderTransactionService.createOrder(request); + } + + // 对同一幂等请求持锁启动,避免多个线程同时创建相同 businessKey 的 Saga。 + orderCreationSaga.start(order, request); + return cacheAndReturn(businessKey, order, message(order.getStatus())); + } finally { + orderLockUtil.unlock(businessKey); + } + } + + private OrderResponse cacheAndReturn(String businessKey, TradeOrder order, String message) { + OrderResponse result = response(order, message); + orderCacheUtil.setCache(businessKey, result); + return result; + } + + private static String message(OrderStatus status) { + if (status == OrderStatus.WAIT_PAY) return "下单成功,等待支付"; + if (status == OrderStatus.CREATE_FAIL) return "下单失败,库存已正确处理"; + return "Saga已启动,订单处理中"; + } + + private static OrderResponse processing(OrderRequest request, String message) { + OrderResponse response = new OrderResponse(); + response.setOrderSerialNo(request.getOrderSerialNo()); + response.setStatus(OrderStatus.CREATE.name()); + response.setMessage(message); + return response; + } + + private OrderResponse response(TradeOrder order, String message) { + SupplierOrder supplierOrder = supplierOrderTransactionService + .findByTradeOrderSerialNo(order.getTradeOrderSerialNo()); + OrderResponse response = new OrderResponse(); + response.setOrderSerialNo(order.getOrderSerialNo()); + response.setTradeOrderSerialNo(order.getTradeOrderSerialNo()); + response.setThirdPartyOrderNo(supplierOrder == null + ? null : supplierOrder.getThirdPartyOrderNo()); + response.setStatus(order.getStatus().name()); + response.setMessage(message); + return response; + } + + private static String businessKey(OrderRequest request) { + return request.getOrderSerialNo() + ":" + request.getSupplierId(); + } + + private static void validate(OrderRequest request) { + if (request == null) throw new IllegalArgumentException("orderRequest is required"); + if (isBlank(request.getOrderSerialNo())) { + throw new IllegalArgumentException("orderSerialNo is required"); + } + if (isBlank(request.getSupplierId())) { + throw new IllegalArgumentException("supplierId is required"); + } + if (isBlank(request.getSkuId())) throw new IllegalArgumentException("skuId is required"); + if (request.getQuantity() <= 0) { + throw new IllegalArgumentException("quantity must be positive"); + } + } + + private static boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } +} diff --git a/architecture/src/main/java/com/arch/policy/order/service/OrderTransactionService.java b/architecture/src/main/java/com/arch/policy/order/service/OrderTransactionService.java new file mode 100644 index 0000000..ea69784 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/service/OrderTransactionService.java @@ -0,0 +1,43 @@ +package com.arch.policy.order.service; + +import com.arch.policy.order.model.OrderRequest; +import com.arch.policy.order.model.OrderStatus; +import com.arch.policy.order.model.TradeOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +/** 只负责订单库的本地事务,示例使用日志代替真实DB操作。 */ +@Service +public class OrderTransactionService { + private static final Logger LOGGER = LoggerFactory.getLogger(OrderTransactionService.class); + + public TradeOrder findByRequest(String orderSerialNo, String supplierId) { + LOGGER.info("[订单库] 按 orderSerialNo + supplierId 查询幂等订单, orderSerialNo={}, supplierId={}", + orderSerialNo, supplierId); + return null; + } + + public TradeOrder createOrder(OrderRequest request) { + String tradeOrderSerialNo = newTradeOrderSerialNo(); + LOGGER.info("[订单库事务-开始] 创建子订单"); + LOGGER.info("[订单库] 插入子订单, orderSerialNo={}, tradeOrderSerialNo={}, supplierId={}, status={}", + request.getOrderSerialNo(), tradeOrderSerialNo, request.getSupplierId(), OrderStatus.CREATE); + LOGGER.info("[订单库事务-提交] 子订单提交;提交后由应用服务同步启动Seata Saga"); + return new TradeOrder(request.getOrderSerialNo(), tradeOrderSerialNo, + request.getSupplierId(), OrderStatus.CREATE); + } + + public void updateStatus(TradeOrder order, OrderStatus status) { + order.setStatus(status); + LOGGER.info("[订单库事务] CAS更新订单状态, tradeOrderSerialNo={}, status={}", + order.getTradeOrderSerialNo(), status); + } + + private static String newTradeOrderSerialNo() { + return "TO" + UUID.randomUUID().toString().replace("-", "") + .substring(0, 20).toUpperCase(); + } +} diff --git a/architecture/src/main/java/com/arch/policy/order/service/SeataOrderCreationSaga.java b/architecture/src/main/java/com/arch/policy/order/service/SeataOrderCreationSaga.java new file mode 100644 index 0000000..c2f3858 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/service/SeataOrderCreationSaga.java @@ -0,0 +1,31 @@ +package com.arch.policy.order.service; + +import com.arch.policy.order.model.OrderRequest; +import com.arch.policy.order.model.TradeOrder; +import io.seata.saga.engine.StateMachineEngine; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +/** 使用 tradeOrderSerialNo 作为业务幂等键启动持久化 Seata Saga。 */ +@Component +public final class SeataOrderCreationSaga implements OrderCreationSaga { + static final String STATE_MACHINE_NAME = "TradeOrderCreationSaga"; + + @Resource + private StateMachineEngine stateMachineEngine; + @Value("${order.seata.tenant-id:order}") + private String tenantId; + + @Override + public void start(TradeOrder order, OrderRequest request) { + Map context = new HashMap(); + context.put("order", order); + context.put("request", request); + stateMachineEngine.startWithBusinessKey(STATE_MACHINE_NAME, tenantId, + order.getTradeOrderSerialNo(), context); + } +} diff --git a/architecture/src/main/java/com/arch/policy/order/service/SupplierOrderService.java b/architecture/src/main/java/com/arch/policy/order/service/SupplierOrderService.java new file mode 100644 index 0000000..81b864e --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/service/SupplierOrderService.java @@ -0,0 +1,29 @@ +package com.arch.policy.order.service; + +import com.arch.policy.order.model.OrderRequest; +import com.arch.policy.order.model.ThirdOrderResult; +import com.arch.policy.order.model.ThirdOrderStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +/** 三方订单适配器伪实现;tradeOrderSerialNo 是三方请求的稳定幂等号。 */ +@Service +public class SupplierOrderService { + private static final Logger LOGGER = LoggerFactory.getLogger(SupplierOrderService.class); + + public ThirdOrderResult createOrder(String tradeOrderSerialNo, OrderRequest request) { + LOGGER.info("[三方接口] 幂等创建订单, tradeOrderSerialNo={}, supplierId={}", + tradeOrderSerialNo, request.getSupplierId()); + // 伪代码默认模拟明确成功;真实适配器需要映射 SUCCESS、FAILED、UNKNOWN 三种结果。 + return new ThirdOrderResult("TP" + tradeOrderSerialNo, ThirdOrderStatus.SUCCESS); + } + + public ThirdOrderResult queryOrder(String tradeOrderSerialNo, String thirdPartyOrderNo, + String supplierId) { + LOGGER.info("[三方查询接口] 查询最终订单状态, tradeOrderSerialNo={}, thirdPartyOrderNo={}, supplierId={}", + tradeOrderSerialNo, thirdPartyOrderNo, supplierId); + // 伪代码默认模拟查询后成功;长期UNKNOWN需要重试,超过阈值转人工处理。 + return new ThirdOrderResult(thirdPartyOrderNo, ThirdOrderStatus.SUCCESS); + } +} diff --git a/architecture/src/main/java/com/arch/policy/order/service/SupplierOrderTransactionService.java b/architecture/src/main/java/com/arch/policy/order/service/SupplierOrderTransactionService.java new file mode 100644 index 0000000..dd1ad7c --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/service/SupplierOrderTransactionService.java @@ -0,0 +1,52 @@ +package com.arch.policy.order.service; + +import com.arch.policy.order.model.SupplierOrder; +import com.arch.policy.order.model.SupplierOrderStatus; +import com.arch.policy.order.model.TradeOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** 只负责订单库中的供应商订单关联记录;生产实现应替换为数据库事务。 */ +@Service +public class SupplierOrderTransactionService { + private static final Logger LOGGER = LoggerFactory.getLogger(SupplierOrderTransactionService.class); + private final ConcurrentMap orders = + new ConcurrentHashMap(); + + public SupplierOrder markCreating(TradeOrder order) { + SupplierOrder created = new SupplierOrder(order.getTradeOrderSerialNo(), + order.getSupplierId(), order.getTradeOrderSerialNo(), SupplierOrderStatus.CREATING); + SupplierOrder existing = orders.putIfAbsent(order.getTradeOrderSerialNo(), created); + SupplierOrder supplierOrder = existing == null ? created : existing; + LOGGER.info("[订单库事务] 幂等创建供应商订单记录, tradeOrderSerialNo={}, status={}", + order.getTradeOrderSerialNo(), supplierOrder.getStatus()); + return supplierOrder; + } + + public void saveResult(String tradeOrderSerialNo, String thirdPartyOrderNo, + SupplierOrderStatus status) { + SupplierOrder supplierOrder = required(tradeOrderSerialNo); + synchronized (supplierOrder) { + supplierOrder.setThirdPartyOrderNo(thirdPartyOrderNo); + supplierOrder.setStatus(status); + } + LOGGER.info("[订单库事务] 保存供应商订单结果, tradeOrderSerialNo={}, thirdPartyOrderNo={}, status={}", + tradeOrderSerialNo, thirdPartyOrderNo, status); + } + + public SupplierOrder findByTradeOrderSerialNo(String tradeOrderSerialNo) { + return orders.get(tradeOrderSerialNo); + } + + private SupplierOrder required(String tradeOrderSerialNo) { + SupplierOrder supplierOrder = orders.get(tradeOrderSerialNo); + if (supplierOrder == null) { + throw new IllegalStateException("supplier order not found: " + tradeOrderSerialNo); + } + return supplierOrder; + } +} diff --git a/architecture/src/main/resources/statelang/book_order_creation_saga.json b/architecture/src/main/resources/statelang/book_order_creation_saga.json deleted file mode 100644 index c6284bd..0000000 --- a/architecture/src/main/resources/statelang/book_order_creation_saga.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "Name": "BookOrderCreationSaga", - "Comment": "本地订单CREATE提交后执行GDS占编,并按SUCCESS/FAIL/UNKNOWN分流", - "Version": "1.0.0", - "StartState": "ReservePnr", - "States": { - "ReservePnr": { - "Type": "ServiceTask", - "ServiceName": "orderCreationSagaStateServices", - "ServiceMethod": "reservePnr", - "Input": ["$.[orderNo]"], - "Output": {"gdsResult": "$#root"}, - "CompensateState": "CancelReservedPnr", - "Catch": [{"Exceptions": ["java.lang.Throwable"], "Next": "MarkUnknown"}], - "Next": "RouteGdsResult" - }, - "RouteGdsResult": { - "Type": "Choice", - "Choices": [ - {"Expression": "$.[gdsResult].status.name() == 'SUCCESS'", "Next": "MarkSuccess"}, - {"Expression": "$.[gdsResult].status.name() == 'FAIL'", "Next": "MarkFailure"} - ], - "Default": "MarkUnknown" - }, - "MarkSuccess": { - "Type": "ServiceTask", - "ServiceName": "orderCreationSagaStateServices", - "ServiceMethod": "markCreateSuccess", - "Input": ["$.[orderNo]", "$.[gdsResult].pnr"], - "Next": "Succeed" - }, - "MarkFailure": { - "Type": "ServiceTask", - "ServiceName": "orderCreationSagaStateServices", - "ServiceMethod": "markCreateFailure", - "Input": ["$.[orderNo]", "$.[gdsResult].errorCode", "$.[promotionId]"], - "Next": "Succeed" - }, - "MarkUnknown": { - "Type": "ServiceTask", - "ServiceName": "orderCreationSagaStateServices", - "ServiceMethod": "markCreateUnknown", - "Input": ["$.[orderNo]", "GDS_RESULT_UNKNOWN"], - "Next": "Succeed" - }, - "CancelReservedPnr": { - "Type": "ServiceTask", - "ServiceName": "orderCreationSagaStateServices", - "ServiceMethod": "enqueueCancelPnr", - "Input": ["$.[orderNo]", "", "$.[gdsResult].pnr"] - }, - "Succeed": {"Type": "Succeed"} - } -} diff --git a/architecture/src/main/resources/statelang/order_creation_saga.json b/architecture/src/main/resources/statelang/order_creation_saga.json new file mode 100644 index 0000000..4cf077f --- /dev/null +++ b/architecture/src/main/resources/statelang/order_creation_saga.json @@ -0,0 +1,68 @@ +{ + "Name": "TradeOrderCreationSaga", + "Comment": "本地子订单提交后扣库存、调用三方并按明确结果确认或返还库存", + "Version": "1.0.0", + "StartState": "DeductStock", + "States": { + "DeductStock": { + "Type": "ServiceTask", + "ServiceName": "orderCreationSagaStateServices", + "ServiceMethod": "deductStock", + "Input": ["$.[order]", "$.[request]"], + "Output": {"stockDeducted": "$#root"}, + "Next": "RouteStockResult" + }, + "RouteStockResult": { + "Type": "Choice", + "Choices": [ + {"Expression": "$.[stockDeducted] == true", "Next": "CreateThirdOrder"} + ], + "Default": "CompleteStockFailure" + }, + "CreateThirdOrder": { + "Type": "ServiceTask", + "ServiceName": "orderCreationSagaStateServices", + "ServiceMethod": "createThirdOrder", + "Input": ["$.[order]", "$.[request]"], + "Output": {"thirdResult": "$#root"}, + "Next": "RouteThirdResult" + }, + "RouteThirdResult": { + "Type": "Choice", + "Choices": [ + {"Expression": "$.[thirdResult].status.name() == 'SUCCESS'", "Next": "CompleteSuccess"}, + {"Expression": "$.[thirdResult].status.name() == 'FAILED'", "Next": "CompleteFailure"} + ], + "Default": "WaitForThirdResult" + }, + "CompleteSuccess": { + "Type": "ServiceTask", + "ServiceName": "orderCreationSagaStateServices", + "ServiceMethod": "completeSuccess", + "Input": ["$.[order]"], + "Next": "Succeed" + }, + "CompleteFailure": { + "Type": "ServiceTask", + "ServiceName": "orderCreationSagaStateServices", + "ServiceMethod": "completeFailure", + "Input": ["$.[order]"], + "Next": "Succeed" + }, + "CompleteStockFailure": { + "Type": "ServiceTask", + "ServiceName": "orderCreationSagaStateServices", + "ServiceMethod": "completeStockFailure", + "Input": ["$.[order]"], + "Next": "Succeed" + }, + "WaitForThirdResult": { + "Type": "ServiceTask", + "ServiceName": "orderCreationSagaStateServices", + "ServiceMethod": "waitForThirdResult", + "Input": ["$.[order]"], + "Next": "Succeed" + }, + "Succeed": {"Type": "Succeed"} + } +} From a2982104cdf834803555825fc7cb0aadc4434afa Mon Sep 17 00:00:00 2001 From: coderbruis Date: Wed, 22 Jul 2026 23:13:19 +0800 Subject: [PATCH 21/21] =?UTF-8?q?=E4=B8=8B=E5=8D=95=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E7=8A=B6=E6=80=81=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../arch/policy/order/model/OrderEvent.java | 22 ++++++++ .../policy/order/model/OrderStateMachine.java | 53 +++++++++++++++++++ .../arch/policy/order/model/TradeOrder.java | 4 +- .../OrderCreationSagaStateServices.java | 8 +-- .../order/service/OrderRecoveryService.java | 6 +-- .../service/OrderTransactionService.java | 14 +++-- 7 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 architecture/src/main/java/com/arch/policy/order/model/OrderEvent.java create mode 100644 architecture/src/main/java/com/arch/policy/order/model/OrderStateMachine.java diff --git a/.gitignore b/.gitignore index 3540bb2..0d80b28 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ Spring-Boot/target Spring-Netty/target Spring-Security/target rocketmqdemo/target +architecture/target # .DO_Store .DS_Store diff --git a/architecture/src/main/java/com/arch/policy/order/model/OrderEvent.java b/architecture/src/main/java/com/arch/policy/order/model/OrderEvent.java new file mode 100644 index 0000000..dc14615 --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/OrderEvent.java @@ -0,0 +1,22 @@ +package com.arch.policy.order.model; + +/** 驱动订单业务状态迁移的业务事件。 */ +public enum OrderEvent { + /** 库存和三方创建流程均已成功,订单进入待支付。 */ + CREATE_SUCCEEDED, + + /** 库存不足或三方创建明确失败,订单创建失败。 */ + CREATE_FAILED, + + /** 支付成功,订单开始执行供应商预订。 */ + PAY_SUCCEEDED, + + /** 支付明确失败。 */ + PAY_FAILED, + + /** 供应商明确返回预订成功。 */ + BOOK_SUCCEEDED, + + /** 供应商明确返回预订失败。 */ + BOOK_FAILED +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/OrderStateMachine.java b/architecture/src/main/java/com/arch/policy/order/model/OrderStateMachine.java new file mode 100644 index 0000000..63a51ea --- /dev/null +++ b/architecture/src/main/java/com/arch/policy/order/model/OrderStateMachine.java @@ -0,0 +1,53 @@ +package com.arch.policy.order.model; + +import org.springframework.stereotype.Component; + +/** 集中维护订单业务状态迁移规则,禁止应用服务直接指定目标状态。 */ +@Component +public final class OrderStateMachine { + + public OrderStatus fire(TradeOrder order, OrderEvent event) { + if (order == null) throw new IllegalArgumentException("order is required"); + if (event == null) throw new IllegalArgumentException("order event is required"); + + OrderStatus current = order.getStatus(); + OrderStatus target = target(current, event); + order.applyStatus(target); + return target; + } + + private static OrderStatus target(OrderStatus current, OrderEvent event) { + // Saga和消息可能至少投递一次;相同事件到达目标状态后按幂等成功处理。 + if (current == targetOf(event)) return current; + + switch (current) { + case CREATE: + if (event == OrderEvent.CREATE_SUCCEEDED) return OrderStatus.WAIT_PAY; + if (event == OrderEvent.CREATE_FAILED) return OrderStatus.CREATE_FAIL; + break; + case WAIT_PAY: + if (event == OrderEvent.PAY_SUCCEEDED) return OrderStatus.BOOKING; + if (event == OrderEvent.PAY_FAILED) return OrderStatus.PAY_FAIL; + break; + case BOOKING: + if (event == OrderEvent.BOOK_SUCCEEDED) return OrderStatus.BOOKED; + if (event == OrderEvent.BOOK_FAILED) return OrderStatus.BOOK_FAIL; + break; + default: + break; + } + throw new IllegalStateException("invalid order transition: " + current + " + " + event); + } + + private static OrderStatus targetOf(OrderEvent event) { + switch (event) { + case CREATE_SUCCEEDED: return OrderStatus.WAIT_PAY; + case CREATE_FAILED: return OrderStatus.CREATE_FAIL; + case PAY_SUCCEEDED: return OrderStatus.BOOKING; + case PAY_FAILED: return OrderStatus.PAY_FAIL; + case BOOK_SUCCEEDED: return OrderStatus.BOOKED; + case BOOK_FAILED: return OrderStatus.BOOK_FAIL; + default: throw new IllegalArgumentException("unsupported order event: " + event); + } + } +} diff --git a/architecture/src/main/java/com/arch/policy/order/model/TradeOrder.java b/architecture/src/main/java/com/arch/policy/order/model/TradeOrder.java index 65bbec8..405a5ba 100644 --- a/architecture/src/main/java/com/arch/policy/order/model/TradeOrder.java +++ b/architecture/src/main/java/com/arch/policy/order/model/TradeOrder.java @@ -19,5 +19,7 @@ public TradeOrder(String orderSerialNo, String tradeOrderSerialNo, String suppli public String getTradeOrderSerialNo() { return tradeOrderSerialNo; } public String getSupplierId() { return supplierId; } public OrderStatus getStatus() { return status; } - public void setStatus(OrderStatus status) { this.status = status; } + + /** 仅供订单状态机应用已经校验通过的目标状态。 */ + void applyStatus(OrderStatus status) { this.status = status; } } diff --git a/architecture/src/main/java/com/arch/policy/order/service/OrderCreationSagaStateServices.java b/architecture/src/main/java/com/arch/policy/order/service/OrderCreationSagaStateServices.java index d464d57..8a41aeb 100644 --- a/architecture/src/main/java/com/arch/policy/order/service/OrderCreationSagaStateServices.java +++ b/architecture/src/main/java/com/arch/policy/order/service/OrderCreationSagaStateServices.java @@ -1,7 +1,7 @@ package com.arch.policy.order.service; import com.arch.policy.order.model.OrderRequest; -import com.arch.policy.order.model.OrderStatus; +import com.arch.policy.order.model.OrderEvent; import com.arch.policy.order.model.SupplierOrderStatus; import com.arch.policy.order.model.ThirdOrderResult; import com.arch.policy.order.model.ThirdOrderStatus; @@ -46,18 +46,18 @@ public ThirdOrderResult createThirdOrder(TradeOrder order, OrderRequest request) public boolean completeSuccess(TradeOrder order) { inventoryTransactionService.confirm(order.getTradeOrderSerialNo()); - orderTransactionService.updateStatus(order, OrderStatus.WAIT_PAY); + orderTransactionService.fireEvent(order, OrderEvent.CREATE_SUCCEEDED); return true; } public boolean completeFailure(TradeOrder order) { inventoryTransactionService.returnStock(order.getTradeOrderSerialNo()); - orderTransactionService.updateStatus(order, OrderStatus.CREATE_FAIL); + orderTransactionService.fireEvent(order, OrderEvent.CREATE_FAILED); return true; } public boolean completeStockFailure(TradeOrder order) { - orderTransactionService.updateStatus(order, OrderStatus.CREATE_FAIL); + orderTransactionService.fireEvent(order, OrderEvent.CREATE_FAILED); return true; } diff --git a/architecture/src/main/java/com/arch/policy/order/service/OrderRecoveryService.java b/architecture/src/main/java/com/arch/policy/order/service/OrderRecoveryService.java index 610dadd..3dfe8eb 100644 --- a/architecture/src/main/java/com/arch/policy/order/service/OrderRecoveryService.java +++ b/architecture/src/main/java/com/arch/policy/order/service/OrderRecoveryService.java @@ -1,6 +1,6 @@ package com.arch.policy.order.service; -import com.arch.policy.order.model.OrderStatus; +import com.arch.policy.order.model.OrderEvent; import com.arch.policy.order.model.SupplierOrder; import com.arch.policy.order.model.SupplierOrderStatus; import com.arch.policy.order.model.ThirdOrderResult; @@ -45,7 +45,7 @@ public void reconcileThirdOrder(TradeOrder order) { supplierOrderTransactionService.saveResult(order.getTradeOrderSerialNo(), result.getThirdPartyOrderNo(), SupplierOrderStatus.SUCCESS); inventoryTransactionService.confirm(order.getTradeOrderSerialNo()); - orderTransactionService.updateStatus(order, OrderStatus.WAIT_PAY); + orderTransactionService.fireEvent(order, OrderEvent.CREATE_SUCCEEDED); return; } @@ -53,7 +53,7 @@ public void reconcileThirdOrder(TradeOrder order) { supplierOrderTransactionService.saveResult(order.getTradeOrderSerialNo(), result.getThirdPartyOrderNo(), SupplierOrderStatus.FAILED); inventoryTransactionService.returnStock(order.getTradeOrderSerialNo()); - orderTransactionService.updateStatus(order, OrderStatus.CREATE_FAIL); + orderTransactionService.fireEvent(order, OrderEvent.CREATE_FAILED); return; } diff --git a/architecture/src/main/java/com/arch/policy/order/service/OrderTransactionService.java b/architecture/src/main/java/com/arch/policy/order/service/OrderTransactionService.java index ea69784..9948092 100644 --- a/architecture/src/main/java/com/arch/policy/order/service/OrderTransactionService.java +++ b/architecture/src/main/java/com/arch/policy/order/service/OrderTransactionService.java @@ -1,18 +1,23 @@ package com.arch.policy.order.service; import com.arch.policy.order.model.OrderRequest; +import com.arch.policy.order.model.OrderEvent; +import com.arch.policy.order.model.OrderStateMachine; import com.arch.policy.order.model.OrderStatus; import com.arch.policy.order.model.TradeOrder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import javax.annotation.Resource; import java.util.UUID; /** 只负责订单库的本地事务,示例使用日志代替真实DB操作。 */ @Service public class OrderTransactionService { private static final Logger LOGGER = LoggerFactory.getLogger(OrderTransactionService.class); + @Resource + private OrderStateMachine orderStateMachine; public TradeOrder findByRequest(String orderSerialNo, String supplierId) { LOGGER.info("[订单库] 按 orderSerialNo + supplierId 查询幂等订单, orderSerialNo={}, supplierId={}", @@ -30,10 +35,11 @@ public TradeOrder createOrder(OrderRequest request) { request.getSupplierId(), OrderStatus.CREATE); } - public void updateStatus(TradeOrder order, OrderStatus status) { - order.setStatus(status); - LOGGER.info("[订单库事务] CAS更新订单状态, tradeOrderSerialNo={}, status={}", - order.getTradeOrderSerialNo(), status); + public void fireEvent(TradeOrder order, OrderEvent event) { + OrderStatus source = order.getStatus(); + OrderStatus target = orderStateMachine.fire(order, event); + LOGGER.info("[订单库事务] 状态机迁移并CAS更新订单, tradeOrderSerialNo={}, event={}, from={}, to={}", + order.getTradeOrderSerialNo(), event, source, target); } private static String newTradeOrderSerialNo() {