企业应用架构23:综合模式选择与架构评审
一份合同实收 1000 分,客户申请退款 300 分。请求可能重复,退款提交后进程可能退出,结算方收到通知并入账后也可能来不及确认。架构评审需要回答这些具体窗口会留下什么数据,而不是检查设计图中是否出现足够多的模式名称。 本篇将“租户范围内的部分退款”作为独立变化,使用两个 SQLite 文件分别表示租赁与结算持久状态。实验验证单库事务、并发重复请求、持久待发记录及接收方去重;本地函数调用承担传递工作,没有消息中间件、公网、真实支付或新页面。对应能力保留 NOT_RUN,综合章节的发布不改变前章证据边界。 业务契约先于模式选择 退款金额必须为正,累计退款不能超过实收;同一个租户内,同一个退款键重复提交相同金额应返回已有结果,改成不同金额必须拒绝;不同租户可以使用相同合同编号。合法退款在租赁库提交后,最终应在结算库形成同额入账,重复投递不能增加结算金额。 样例以 A、B 两个租户,各自拥有合同 1,实收均为 1000 分。主体 alice 只属于 A,bob 只属于 B。此映射沿用教学边界,不代替真实认证。所有金额固定 CNY 整数分,不涉及部分履约、支付通道费用、税务冲销或跨币...
企业应用架构22:Strangler 与 Branch by Abstraction
旧租赁系统用 cents 保存合同金额,新实现准备采用 amount_minor 与 currency。替换一个字段看似简单,实际存在多个同时运行的事实:旧版本还在写,历史行尚未回填,新版本可能只接管部分租户,而回退按钮只能切换代码,无法撤销已经提交的数据。 本篇用 SQLite 文件数据库演练一个受限迁移:保留旧字段,增加兼容字段,通过稳定写入入口按租户路由;回填中途退出,再从检查点恢复;最后切回旧实现并继续写入。实验中的副作用是数据库 effects 记录,没有访问外部支付或通知服务,线上网关、跨机器复制和真实流量迁移均为 NOT_RUN。 先确定回退以后谁读什么 迁移的第一份契约应描述旧版本仍然需要的列、类型和含义。旧读取 cents,固定解释为 CNY;新读取优先使用 amount_minor 和 currency,尚未回填时退回旧字段及 CNY。两条读取路径对同一合同必须返回相同金额与币种,比较应在实际数据上执行,不能只比较两份接口定义。 因此,这次变化只允许 CNY。若新版本开始写 USD,旧版本把同一个数字解释为人民币,即使 SQL 能执行,业务也已经不兼容。新增...
企业应用架构21:插件、业务扩展点与产品变化
设备租赁的客户差异有时只是一项费率,有时改变整个计费算法。把所有差异塞进配置,配置会逐渐拥有条件与循环;把每个字段都做成插件,简单产品又需要维护加载器、版本协议和异常边界。扩展点应由实际变化决定,并且留下普通条件分支可以解决问题的对照。 本篇保留日计费和整周优惠两种策略,再增加一个受限封顶插件。实验使用 Python 3.14 的标准库从批准的本地文件加载插件,不使用 Java ServiceLoader,也不把进程内插件说成安全沙箱。相同八天、一百分日费率,三种行为分别产生 800、700、500 分,差异来自明确规则,不来自名称不同的空实现。 参数、策略与插件分别改变什么 日计费按天数乘单价。整周优惠将每七天计为六天,剩余天数照常计费;八天对应七个计费日,因此是 700 分。封顶规则取普通日租总额与 500 分的较小值,八天得到 500 分。金额统一使用整数分,输入只描述受限报价,不包含税率、押金、跨币种转换和合同变更历史。 若客户差异只是每日单价,保留同一函数并传不同参数足够。整周优惠改变运算规则,适合独立策略函数。封顶规则由另一个批准模块提供,需要加载时验证能力与接口版...
企业应用架构20:多租户隔离与配置
两家租赁公司都可能有编号为 1 的合同。公司甲的金额为 100,公司乙的金额为 200,合同编号相同并不表示同一业务对象。多租户系统需要把公司身份一直保留到查询、缓存、任务与幂等记录,任何一层只留下合同编号,都可能把另一家公司的结果交给当前请求。 本篇采用合成数据演示一个受限方案:H2 2.1.214 中每个租户一个 schema,每个 schema 配一个应用账号。管理员负责建表和授权,业务查询使用 APP_A、APP_B。实验验证数据库权限与应用上下文的组合,消息部分只运行消费函数和持久 Inbox,没有启动消息中间件;线上身份认证、密钥轮换和跨机器攻击测试均为 NOT_RUN。 租户身份从哪里来 租户标识可以出现在请求头、路径、消息和配置项中,但这些位置提供的是请求声明。服务必须先依据已认证主体的授权关系确定可访问公司,再比较声明。代码以固定映射 alice→A、bob→B 代替真实身份系统,输入为空或 alice 声明 B 都在建立数据库连接前失败。这个映射证明检查顺序,不能证明真实令牌不可伪造。 一种常见错误是把请求头直接转成连接池键。攻击者修改标识后,应用会用自己的...
企业应用架构19:工作流人任务与版本演进
租赁申请需要店员审核,审核期间服务可能重启。后来流程增加主管复核,已经提交的旧申请是否也要补一道审核,不能只靠替换一个流程文件回答。等待中的实例、任务归属和流程版本都必须有持久记录。 本篇将两份 BPMN 模型部署到真实 Camunda 7.23 引擎,以 H2 文件数据库保存等待状态。第一次创建人任务后直接终止 JVM,再用新进程恢复,随后部署第二版,比较新旧实例的路径及普通代码的同一转换规则。 引擎承担等待和继续的执行 普通业务代码可以计算下一步是审核还是结束,但跨越数小时的人工等待还涉及任务持久化、恢复、查询和历史记录。工作流引擎把这些执行机制集中起来,领域规则仍需由应用明确提供。流程节点存在,不表示审批权限或租赁条件已经正确。 BPMN 是流程模型与表示规范;具体引擎只执行自己支持的元素和扩展。本实验使用开始事件、顺序流、人任务和结束事件,没有覆盖整个 BPMN 标准,也没有把能够解析 XML 等同于全部语义通过。OMG BPMN 2.0.2 Camunda 的 User Task 在流程到达时创建等待处理的任务,应用可以查询并完成任务。样本直接调用引擎 API 完成它...
企业应用架构18:Saga补偿与持久化恢复
设备预留已经提交,扣费却被拒绝,合同需要释放预留。释放也可能失败,甚至已经释放成功但协调器还没记录结果就退出。把这段逻辑写成一个 try-catch,只能处理仍在运行的调用栈,无法回答新进程该从哪一步继续。 本篇用三个独立 H2 数据库分别保存协调状态、库存操作和收费结果,主动在参与者提交后终止 JVM。恢复进程根据持久记录继续补偿,检查重复释放、迟到成功事件以及交付以后不能自动撤销的边界。 补偿是另一次业务操作 Saga 将长事务分成可分别提交的事务,并为需要撤销的步骤安排补偿。前面已经提交的结果不会因为后续某个进程抛异常而自动回滚,补偿也可能需要新的尝试。原始Sagas论文;Saga模式 设备预留的补偿可以是释放预留,但这并不等于时间倒流。预留期间其他合同可能已被拒绝,业务系统也可能产生审计或通知。实验只检验资源持有状态与释放次数,不声称所有外部观察都恢复到预留前。 协调器保存 R1、R2 的当前状态和每次迁移原因。库存数据库保存资源 held、releases 及操作编号;收费数据库保存操作编号和整数金额。三者通过各自 JDBC 连接提交,不共享一个能够整体回滚的数据库事...
企业应用架构17:Request-Reply关联与超时恢复
两个合同同时等待预留结果,第二个请求先收到答复,第一个请求在等待期限以后仍没有答复。按队列接收顺序配对,会把设备预留结果交给错误合同;把超时直接写成失败,则可能在设备已经预留以后再次发起预留。 本篇保留两条尚未完成的请求,使用真实消息通道逆序返回结果,并在请求进程退出后恢复关联状态。服务端已经提交而回复迟到的场景,通过查询持久化结果恢复,超时本身不承担判断业务成功与否的职责。 三种标识对应三种问题 Request-Reply 以一条请求和对应答复组织交互,Correlation Identifier 则让接收者判断答复属于哪次请求。这不要求答复按请求发送顺序到达,也不要求调用方始终占用一个线程等待。Request-Reply;Correlation Identifier 实验的合同编号为 R1、R2,请求关联编号为 C1、C2,发送消息编号为 M1、M2。合同编号用于业务查询,关联编号区分一次请求,消息编号识别一次业务报文。三者在这个最小样本里一一对应,却不应因此合并:同一合同可能经历多次查询或不同阶段的请求。 JMSCorrelationID 携带 C1 或 C2,消息正文携...
企业应用架构16:拆单路由与交付结果聚合
一份租赁合同包含相机与投影仪,两种设备由不同端点处理。相机完成消息到了两次,投影仪的结果一直没有到。如果聚合器只计算收到了几条消息,合同就可能在缺少一件设备时被标为全部完成。 本篇用真实 ActiveMQ 通道传递拆分任务和完成结果,H2 保存批次与明细。完成条件按合同、交付批次和设备任务判断,实验依次观察乱序、重复、缺项、超时及上一批次的迟到结果。 拆分之前固定完成条件 Splitter 把复合消息拆成可以独立处理的部分;Content-Based Router 根据内容选择通道;Aggregator 再按关联标识与完成条件收集结果。这里需要先定义“全部”,再决定如何拆分,否则最后一条到达的消息没有资格宣布整份合同完成。Splitter;Content-Based Router 实验在批次创建时保存预期任务数。合同 R1 的 B1 批次包含相机 E1 和投影仪 P1,预期为两项;合同 R2 的 B1 批次只有相机 E2,预期为一项。批次号可以在不同合同内重复,因此数据库主键由合同号和批次号共同组成。 每项任务也有独立标识,例如 R1/B1/E1。它既出现在持久化明细表,也作为...
企业应用架构15:Channel、Endpoint与Translator的真实消息边界
租赁合同引用设备 E1,维修系统识别的资产却叫 ASSET-E1。直接把合同对象序列化后发送,既暴露内部字段,也没有解决两套标识之间的关系。消息能够送达,只是集成的一部分;接收方还需要知道它属于哪种报文、该交给哪个端点,以及不能处理时保留在哪里。 本篇将设备维修申请放进真实 ActiveMQ 通道,经 TCP 交给独立消费进程,再将翻译结果写入 H2。消费者离线、broker 停启、未知版本、错误通道和毒消息分别运行,业务拒绝与基础设施重试保留不同证据。 通道与端点承担不同职责 EIP 的 Message Channel 讨论应用之间传输消息的通路;Message Endpoint 是应用与消息系统连接的边界;Message Translator 则处理报文格式差异。它们可以组合,但不能仅根据一条队列名称就认定转换规则已经存在。Message Channel;Message Translator 实验固定 maintenance.request 为维修申请通道。租赁方发送外部合同编号与设备引用,维修端点将 E1 翻译成 ASSET-E1,并生成维修申请文本。这个前缀规则仅服务于...
企业应用架构14:幂等、Outbox与Inbox的退出恢复
合同提交成功以后,发布进程可能尚未发送消息就退出。接收方已经记账以后,也可能尚未确认消息就退出。前一种情况可能漏发,后一种情况会引发重复投递。只在正常流程里调用一次发送函数,无法说明这两个故障窗口怎样恢复。 本实验把业务、待发记录、传输队列和接收方结果分别写入真实文件数据库。业务事务原子写合同与 Outbox,独立发布 JVM 把消息写进持久 SQL 队列,独立消费 JVM 用 Inbox 与记账更新组成另一个事务。驱动在三个已提交位置调用进程级强制退出,再启动新进程读取和恢复,每一步保留实际 SQL 与退出码。 同一事务能够解决的范围 Transactional Outbox 模式要求业务更新与消息记录在同一个数据库事务中持久化,再由另外的发布器发送。这样避免了“业务已提交但没有任何待发依据”的空隙。发布器本身仍可能重复发送,因此消费者需要幂等处理。Transactional Outbox 原文 本例 sender 数据库中有合同表与 Outbox 表。合同编号 R1 对应整数金额二十五,Outbox 的消息编号 M1 携带相同金额。一次事务插入两条记录并提交,然后进程立即退...










