<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>架构 on 追风笔记</title><link>https://f91dba72.wangpeng.pages.dev/tags/%E6%9E%B6%E6%9E%84/</link><description>Recent content in 架构 on 追风笔记</description><generator>Hugo</generator><language>zh</language><lastBuildDate>Sun, 02 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://f91dba72.wangpeng.pages.dev/tags/%E6%9E%B6%E6%9E%84/index.xml" rel="self" type="application/rss+xml"/><item><title>CQRS 与 Saga：复杂业务的读写分离与最终一致</title><link>https://f91dba72.wangpeng.pages.dev/columns/architecture/cqrs-saga/</link><pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/columns/architecture/cqrs-saga/</guid><description>&lt;p&gt;很多团队把 CQRS 当成「加个 MQ 同步数据」就完事，结果读模型对了、写模型乱了，长事务更无从下手。CQRS 解决「读写诉求不同」，Saga 解决「业务跨多个聚合却不能开大事务」，两者常一起出现但职责不同。&lt;/p&gt;&#10;&lt;h2 id="读写为什么要分家"&gt;读写为什么要分家&#10;&lt;/h2&gt;&#10;&lt;p&gt;写模型关心一致性与不变量，是一组小而强的聚合；读模型关心展示与组装，最好能直接查出前端要的 DTO。诉求冲突时强行共用一个模型，两边都不讨好。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-72f7da03-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="3"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-72f7da03-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withdraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;money&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 写侧：只做业务&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;findByCardNo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cardNo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;读侧&lt;/span&gt;&lt;span class="err"&gt;：&lt;/span&gt;&lt;span class="n"&gt;直接投影&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;无需聚合&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;不要为了 CQRS 而 CQRS。单表就能满足读写、流量不大的模块，分家只会制造延迟和负担。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="saga-管理长事务"&gt;Saga 管理长事务&#10;&lt;/h2&gt;&#10;&lt;p&gt;转账、跨境汇款天然跨账户跨系统，不可能用本地事务锁住。Saga 把大事务拆成一串本地事务，每步都有补偿。&lt;/p&gt;&#10;&lt;div class="td-table-scroll td-table-scroll--static"&gt;&#10;&lt;table&gt;&#10; &lt;thead&gt;&#10; &lt;tr&gt;&#10; &lt;th scope="col"&gt;模式&lt;/th&gt;&#10; &lt;th scope="col"&gt;思路&lt;/th&gt;&#10; &lt;th scope="col"&gt;适用&lt;/th&gt;&#10; &lt;/tr&gt;&#10; &lt;/thead&gt;&#10; &lt;tbody&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;编排&lt;/td&gt;&#10; &lt;td&gt;中心协调器逐步调用并补偿&lt;/td&gt;&#10; &lt;td&gt;需强管控&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;协同&lt;/td&gt;&#10; &lt;td&gt;各服务靠事件触发&lt;/td&gt;&#10; &lt;td&gt;去中心低耦合&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;h2 id="补偿与幂等"&gt;补偿与幂等&#10;&lt;/h2&gt;&#10;&lt;p&gt;Saga 最怕「补偿自己也失败」，所以每步都要幂等：用业务流水号去重，重试不重复扣款。每步记录状态机 &lt;code&gt;PENDING -&amp;gt; DONE / COMPENSATED&lt;/code&gt;，补偿顺序与正向相反，对账做兜底而非第一道防线。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;最终一致不是「不管了」，而是把一致性从「即时」换成「可验证」：允许短暂中间态，但必须能收敛到正确态。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;落地上，CQRS 让读写各取所需，Saga 让长流程可回退，再加独立对账，复杂业务才算立得住。&lt;/p&gt;</description></item><item><title>银行核心系统建模随笔</title><link>https://f91dba72.wangpeng.pages.dev/blog/banking-core-model/</link><pubDate>Sun, 02 Aug 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/banking-core-model/</guid><description>&lt;p&gt;核心系统的模型看起来很朴素：账户、余额、交易、分录。真正做进去才发现，每一个概念背后都有一堆不能妥协的约束，而这些约束在需求文档里往往一个字都没写。这篇是零散的建模笔记，按我认为重要性排的顺序。&lt;/p&gt;&#10;&lt;h2 id="核心系统到底在建模什么"&gt;核心系统到底在建模什么&#10;&lt;/h2&gt;&#10;&lt;p&gt;先把定位说清楚。核心系统建模的对象不是&amp;quot;业务流程&amp;quot;，而是&lt;strong&gt;会计事实&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;客户在手机上点一下转账，这是渠道行为；反洗钱要不要拦、限额够不够，这是风控与协议判断；而核心真正负责的只有一件事：&lt;strong&gt;在正确的会计日，把一组借贷平衡的分录准确地记下来，并保证余额永远等于分录累计的结果&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;p&gt;想清楚这一点，很多设计争论会自动消失。比如&amp;quot;转账的营销活动规则要不要放核心&amp;quot;——不放，那不是会计事实。核心的表越少越好，能力越单一越好。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;判断一个需求该不该进核心，只问一句：它会改变账务事实吗？不会，就放外围。核心系统膨胀的过程，就是这条边界一次次让步的过程。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="账户模型一棵不能长歪的树"&gt;账户模型：一棵不能长歪的树&#10;&lt;/h2&gt;&#10;&lt;h3 id="账户号不是主键"&gt;账户号不是主键&#10;&lt;/h3&gt;&#10;&lt;p&gt;这是我见过最多的建模错误。账号是&lt;strong&gt;业务标识&lt;/strong&gt;，有格式规则、会变更、会重开、可能因迁移换号段。用它做物理主键，等到某天要做账号升位或者跨行并库，整个系统的外键关系会一起爆炸。&lt;/p&gt;&#10;&lt;p&gt;正确做法是内部 ID 与业务账号分离：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-b9acdbec-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="18"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-b9acdbec-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AccountId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 内部标识，永不变更，不对外暴露&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AccountNo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;accountNo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 业务账号，可变更，对外展示&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ProductCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AccountStatus&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LocalDate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;openDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/** 账号变更保留历史，因为凭证和流水引用的是历史账号 */&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;changeAccountNo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AccountNo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;newNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AccountStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ACTIVE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DomainException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;非正常状态账户不允许换号&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;registerEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AccountNoChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;accountNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;newNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accountNo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;newNo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;code&gt;AccountNoChanged&lt;/code&gt; 事件必须落库。半年后有人拿旧账号来查流水，你要能翻译过去。&lt;/p&gt;</description></item><item><title>缓存设计的那些坑</title><link>https://f91dba72.wangpeng.pages.dev/blog/redis-cache-pitfall/</link><pubDate>Wed, 08 Jul 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/redis-cache-pitfall/</guid><description>&lt;p&gt;缓存是最容易加的优化，也是最容易埋雷的优化。加的时候只要几行代码，出问题时往往是深夜的数据不一致或者数据库被打穿。&lt;/p&gt;&#10;&lt;h2 id="一致性先更库还是先删缓存"&gt;一致性：先更库还是先删缓存&#10;&lt;/h2&gt;&#10;&lt;p&gt;这个问题的正确答案是&lt;strong&gt;先更新数据库，再删除缓存&lt;/strong&gt;（Cache Aside）。但要理解它为什么仍然不完美。&lt;/p&gt;&#10;&lt;p&gt;先删缓存再更库的问题很直接：删完缓存到更库完成之间，另一个请求读到旧数据并把它写回缓存，之后缓存里就一直是脏数据。&lt;/p&gt;&#10;&lt;p&gt;先更库再删缓存也有极小概率出问题：读请求恰好在缓存失效后读到旧库值，且回写发生在删除动作之后。概率很低，但在高并发下不等于零。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-8d7ae05e-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="11"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-8d7ae05e-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;updateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;accountNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Money&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;newLimit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;limitRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;newLimit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 1. 先落库&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 2. 事务提交后再删缓存，避免删除后事务回滚导致缓存空档&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TransactionSynchronizationManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerSynchronization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TransactionSynchronization&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;afterCommit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limitKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountNo&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;code&gt;afterCommit&lt;/code&gt; 这一步很关键。在事务内删缓存，如果事务随后回滚，缓存已经被清掉，下一次读会把旧值再加载回来——看起来是对的，但实际上你丢失了一次删除的语义保证。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;更重要的判断是：这个数据到底能不能容忍短暂不一致。限额、开关、风控规则这类直接影响资金准入的配置，我们干脆不缓存或者只做几秒的本地缓存，宁可多查一次库。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="三大经典故障"&gt;三大经典故障&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;穿透&lt;/strong&gt;：查一个根本不存在的 key，每次都落库。恶意刷不存在的账号就是这么打穿数据库的。做法是缓存空值并设短过期（比如 60 秒），或者前置布隆过滤器。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;击穿&lt;/strong&gt;：热点 key 过期瞬间大量请求同时落库。做法是加互斥锁，只让一个线程去加载，其余等待或返回旧值。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;雪崩&lt;/strong&gt;：大批 key 同时过期，或者 Redis 整体不可用。前者靠给过期时间加随机抖动解决；后者必须有降级预案——限流加返回兜底数据，不能让流量全量透传到数据库。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-8d7ae05e-fence-1" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="6"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-8d7ae05e-fence-1-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 过期时间加抖动，避免批量同时失效&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ttlWithJitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;jitter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ThreadLocalRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextLong&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toSeconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 上下浮动约 10%&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;plusSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="热-key-与大-key"&gt;热 key 与大 key&#10;&lt;/h2&gt;&#10;&lt;p&gt;这两个是运维阶段最常见的问题，开发时基本不会注意到。&lt;/p&gt;</description></item><item><title>分库分表不是银弹</title><link>https://f91dba72.wangpeng.pages.dev/blog/mysql-sharding/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/mysql-sharding/</guid><description>&lt;p&gt;分库分表在很多团队是一种默认操作：数据量一大就分。但它换来的收益很窄，付出的代价很宽，而且代价大多在上线半年后才开始显现。&lt;/p&gt;&#10;&lt;h2 id="先问要不要分"&gt;先问要不要分&#10;&lt;/h2&gt;&#10;&lt;p&gt;分库分表真正解决的问题只有两个：单表数据量导致的索引效率下降，以及单库写入的 IOPS 瓶颈。除此之外的问题它都解决不了，甚至会恶化。&lt;/p&gt;&#10;&lt;p&gt;在决定分之前，这几件事的性价比更高：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;加索引或改查询&lt;/strong&gt;。慢查询里相当大一部分是索引缺失或者写法导致索引失效，跟数据量无关。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;冷热分离&lt;/strong&gt;。交易流水表里九成查询集中在最近三个月。把历史数据归档到历史库，主表立刻瘦身，成本远低于分表。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;读写分离&lt;/strong&gt;。读多写少的场景加从库就够了。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;升配&lt;/strong&gt;。听起来不体面，但一台高配实例能撑到的量往往超出预期，而且省下的是人力。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;我的经验阈值：单表超过两千万行且增长稳定、或者写入已经打满 IO，才考虑分表。仅仅因为&amp;quot;数据量看着挺大&amp;quot;就分，是给自己找活干。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="分片键决定生死"&gt;分片键决定生死&#10;&lt;/h2&gt;&#10;&lt;p&gt;分片键选错基本没法补救——除非停机重新迁移全量数据。&lt;/p&gt;&#10;&lt;p&gt;选择原则是&lt;strong&gt;让高频查询都能带上分片键&lt;/strong&gt;。交易流水表用账号分片，那么&amp;quot;查某账号最近流水&amp;quot;就是单分片查询，性能很好；但&amp;quot;查某商户当天全部流水&amp;quot;就变成了全分片扫描加内存聚合。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-35cd25d1-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="sql" data-td-line-count="10"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-35cd25d1-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 单分片，走得很好&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;txn_flow&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;account_no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;6222...1234&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;txn_date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2026-06-01&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;txn_date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- 跨全部分片，聚合在应用层做，分片多了就是灾难&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;merchant_no&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;txn_flow&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;txn_date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2026-06-14&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;GROUP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;merchant_no&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;第二类需求不要靠分片库硬扛，应该走另一条路：同步一份到 OLAP 引擎或数仓。&lt;strong&gt;一份数据满足所有查询模式的想法本身就是问题的根源。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;另外一定要&lt;strong&gt;一次性把逻辑分片数定足&lt;/strong&gt;。我们定了 1024 个逻辑分片映射到 8 个物理库，扩容时只搬分片区间，不用重新哈希。如果直接按物理库取模，第一次扩容就要迁移全量数据。&lt;/p&gt;</description></item><item><title>Kafka 在交易系统的削峰与解耦</title><link>https://f91dba72.wangpeng.pages.dev/blog/kafka-practice/</link><pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/kafka-practice/</guid><description>&lt;p&gt;交易系统引入 Kafka 通常出于两个动机：扛住流量尖峰，以及把非核心逻辑从主链路上摘下来。这两件事它都能做，但做法和注意点完全不同。&lt;/p&gt;&#10;&lt;h2 id="削峰把洪峰变成队列"&gt;削峰：把洪峰变成队列&#10;&lt;/h2&gt;&#10;&lt;p&gt;代发工资、批量还款、营销活动这些场景的流量特征是极不均匀——平时每秒几百笔，活动开始瞬间冲到几万笔。数据库扛不住的不是总量，是瞬时并发。&lt;/p&gt;&#10;&lt;p&gt;削峰的本质是&lt;strong&gt;用延迟换稳定&lt;/strong&gt;：请求先落 Kafka，下游按自己的处理能力匀速消费。关键是消费端要限速，不能拿到消息就火力全开打数据库。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-9fc028c5-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="yaml" data-td-line-count="11"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-9fc028c5-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;kafka&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;group-id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;txn-processor&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enable-auto-commit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 必须手动提交&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;max-poll-records&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;max.poll.interval.ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;300000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 留足单批处理时间，避免误判掉线&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ack-mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;manual&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;concurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 与分区数匹配，不要超过&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;code&gt;concurrency&lt;/code&gt; 超过分区数没有任何收益，多出来的线程只会空转。要提升并行度得先加分区，而分区数一旦加了就减不回去，前期要按峰值容量规划好。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;削峰只对&amp;quot;可以延后处理&amp;quot;的业务成立。用户在 App 里点转账等结果的场景不能这么做——那是同步链路，塞消息队列只是把等待转移到了别处。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="解耦从同步调用到事件"&gt;解耦：从同步调用到事件&#10;&lt;/h2&gt;&#10;&lt;p&gt;主链路上挂着一堆下游是很常见的技术债：转账成功后要发短信、更新积分、推送风控特征、写数仓。每加一个下游，主链路的失败面就大一分。&lt;/p&gt;&#10;&lt;p&gt;改成发一条 &lt;code&gt;transfer.posted&lt;/code&gt; 事件，各下游自己订阅。主链路只负责记账和发事件，下游挂了不影响交易成功。&lt;/p&gt;&#10;&lt;p&gt;需要划清界限的是：&lt;strong&gt;哪些下游能异步&lt;/strong&gt;。判断标准是这个下游失败后，业务上能不能接受&amp;quot;稍后补上&amp;quot;。更新积分可以，扣减额度不行——额度是交易准入条件，必须同步。&lt;/p&gt;&#10;&lt;h2 id="几个必须守住的细节"&gt;几个必须守住的细节&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;顺序只在分区内保证&lt;/strong&gt;。同一账户的消息必须用账户号做 key，否则先扣款后入账的顺序可能颠倒。跨账户不需要全局顺序，别为了顺序把分区数设成 1。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;消费端必须幂等&lt;/strong&gt;。Kafka 是至少一次投递，重复是常态。用业务唯一键加唯一索引兜住，不要指望不重复。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;先处理成功再提交 offset&lt;/strong&gt;。自动提交会在处理失败时丢消息。手动提交虽然会带来重复，但重复有幂等兜着，丢了就找不回来了。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;积压要能定位到分区&lt;/strong&gt;。整体 lag 正常但单分区堆积，通常是某个 key 数据倾斜或者某条消息反复失败阻塞了分区。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-9fc028c5-fence-1" data-td-code data-td-code-auto-id&#10; data-td-language="bash" data-td-line-count="7"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-9fc028c5-fence-1-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 按分区查看消费延迟，定位倾斜与阻塞&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kafka-consumer-groups.sh --bootstrap-server &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$BROKERS&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --group txn-processor --describe&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 排查反复失败的毒消息：从指定 offset 读一条看内容&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;kafka-console-consumer.sh --bootstrap-server &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$BROKERS&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --topic txn-events --partition &lt;span class="m"&gt;3&lt;/span&gt; --offset &lt;span class="m"&gt;88213&lt;/span&gt; --max-messages &lt;span class="m"&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;毒消息一定要有出路。处理失败超过阈值就转到死信 topic，让分区继续往下走，否则一条脏数据能把整个分区卡死几个小时。&lt;/p&gt;</description></item><item><title>分布式事务在金融场景的取舍</title><link>https://f91dba72.wangpeng.pages.dev/blog/distributed-transaction/</link><pubDate>Mon, 23 Feb 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/distributed-transaction/</guid><description>&lt;p&gt;金融系统聊分布式事务，很容易陷入&amp;quot;选哪个框架&amp;quot;的讨论。但真实的约束是：钱不能错，且必须能查清为什么错。这两条决定了方案选择的顺序和别的行业不太一样。&lt;/p&gt;&#10;&lt;h2 id="金融场景的真实约束"&gt;金融场景的真实约束&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;不允许静默不一致&lt;/strong&gt;。可以暂时不一致，但必须有机制发现并修复，不能出现谁也不知道差在哪的状态。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;必须能人工干预&lt;/strong&gt;。任何自动补偿机制都有失效的可能，最终要留一条人工处理通道，且要有审计留痕。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;联机响应时间有硬指标&lt;/strong&gt;。核心交易通常要求 100ms 内，多阶段协调的额外网络往返基本吃不下。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;补偿不总是可行&lt;/strong&gt;。已经出账到人民银行的报文没法回滚，只能发反向报文，业务语义完全不同。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;最后一条最容易被忽略：很多补偿型方案假设操作可逆，但金融业务里大量操作在真实世界里是不可逆的。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="几种方案的适用边界"&gt;几种方案的适用边界&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;2PC / XA&lt;/strong&gt;：强一致，但同步阻塞、协调者单点、锁持有时间长。只在同库多表或同一数据库集群内使用。跨服务用它，第一次网络抖动就会看到大批悬挂事务。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;TCC&lt;/strong&gt;：一致性和性能都不错，代价是每个参与方要写三份逻辑（try / confirm / cancel），且必须自己处理空回滚、悬挂、幂等。开发量大约是普通接口的三倍，适合参与方少且业务确定的核心链路。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Saga&lt;/strong&gt;：适合长流程、多参与方，比如开户流程里的证件核验、账户开立、卡片制发。要求每步都有语义补偿，且能接受中间态对外可见。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;本地消息表 + 可靠投递&lt;/strong&gt;：把消息写入和业务操作放在同一个本地事务，再由后台任务投递。实现简单、无外部依赖、可靠性靠数据库保证。缺点是只能做最终一致。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="我们的默认选择本地消息表--对账"&gt;我们的默认选择：本地消息表 + 对账&#10;&lt;/h2&gt;&#10;&lt;p&gt;绝大多数场景我们用本地消息表。理由很朴素：它没有引入新的中间件，所有状态都在业务库里，出问题时排查路径最短。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-310a34a3-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="15"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-310a34a3-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TransferCmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 1. 本地账务操作&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Voucher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;voucher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;accountingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toVoucher&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 2. 同一事务内写待发消息，保证原子性&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OutboxMessage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OutboxMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;transfer.posted&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bizNo&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 业务唯一键，下游据此幂等&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;JsonUtil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voucher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toEvent&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;outboxRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 独立线程扫描待发消息，投递成功后标记；失败按退避重试&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;超过最大重试次数转入人工处理队列&lt;/span&gt;&lt;span class="err"&gt;，&lt;/span&gt;&lt;span class="n"&gt;不允许静默丢弃&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;配套三件事一个都不能省：&lt;/p&gt;</description></item><item><title>DDD 在银行核心系统的落地实践</title><link>https://f91dba72.wangpeng.pages.dev/blog/ddd-banking-core/</link><pubDate>Mon, 12 Jan 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/ddd-banking-core/</guid><description>&lt;p&gt;银行核心系统是一类很特殊的软件：它的业务规则几十年没怎么变，但承载它的代码每隔七八年就要重写一次。我参与过一次核心的部分重构，也旁观过一次彻底失败的重写。DDD 在这两次里都被提过，但只有一次真的起了作用。这篇把我认为有效的部分和纯属自我感动的部分分开写。&lt;/p&gt;&#10;&lt;h2 id="为什么银行核心需要-ddd"&gt;为什么银行核心需要 DDD&#10;&lt;/h2&gt;&#10;&lt;p&gt;老核心的问题从来不是技术栈老。COBOL 写的联机交易照样能扛住每秒几千笔。真正的问题是：业务知识只存在于少数几个人的脑子里，代码里找不到对应的表达。&lt;/p&gt;&#10;&lt;p&gt;一段典型的老代码长这样：把交易码、账户类型、产品编号、机构号混在一个几百行的过程里，用几十个 &lt;code&gt;IF&lt;/code&gt; 分支区分场景。你想知道&amp;quot;活期账户计息到底怎么算&amp;quot;，只能从头读到尾，然后祈祷没漏掉某个补丁分支。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;代码可以重写，业务知识不能。重构核心系统的第一目标不是换语言，而是把散落在分支里的规则重新变成可以被讨论的概念。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;这正是 DDD 唯一真正值得投入的地方：它提供了一套让业务专家和工程师用同一套词汇讨论问题的方法。别的都是附加品。&lt;/p&gt;&#10;&lt;h2 id="战略设计先切限界上下文"&gt;战略设计：先切限界上下文&#10;&lt;/h2&gt;&#10;&lt;p&gt;如果只能做 DDD 的一件事，就做上下文切分。这一步做错，后面所有战术模式都是白费。&lt;/p&gt;&#10;&lt;h3 id="以业务能力而非表结构切分"&gt;以业务能力而非表结构切分&#10;&lt;/h3&gt;&#10;&lt;p&gt;最常见的错误是按数据库表切：账户表归账户服务，交易表归交易服务。结果是每笔转账都要跨三个服务改数据，分布式事务满天飞。&lt;/p&gt;&#10;&lt;p&gt;我们最后落在这几个上下文上：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;客户与关系&lt;/strong&gt;：客户主体、关系人、证件、KYC 状态&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;账户与协议&lt;/strong&gt;：账号、账户属性、签约关系、限额协议&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;交易处理&lt;/strong&gt;：交易受理、要素校验、路由、冲正&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;账务核算&lt;/strong&gt;：分录、余额、总账、日终结转&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;产品参数&lt;/strong&gt;：利率、费率、期限、计息规则&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;关键判据是：&lt;strong&gt;一次业务动作的强一致性要求是否落在同一个上下文内&lt;/strong&gt;。转账的&amp;quot;扣款成功且分录平衡&amp;quot;必须强一致，所以账务核算不能再往下拆；而&amp;quot;转账成功后更新客户活跃度&amp;quot;完全可以最终一致，那就是两个上下文。&lt;/p&gt;&#10;&lt;h3 id="上下文映射的三种常见关系"&gt;上下文映射的三种常见关系&#10;&lt;/h3&gt;&#10;&lt;div class="td-table-scroll td-table-scroll--static"&gt;&#10;&lt;table&gt;&#10; &lt;thead&gt;&#10; &lt;tr&gt;&#10; &lt;th scope="col"&gt;关系类型&lt;/th&gt;&#10; &lt;th scope="col"&gt;银行场景举例&lt;/th&gt;&#10; &lt;th scope="col"&gt;集成方式&lt;/th&gt;&#10; &lt;th scope="col"&gt;注意点&lt;/th&gt;&#10; &lt;/tr&gt;&#10; &lt;/thead&gt;&#10; &lt;tbody&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;共享内核&lt;/td&gt;&#10; &lt;td&gt;账户上下文与账务上下文共用币种、金额模型&lt;/td&gt;&#10; &lt;td&gt;公共 jar，严格版本管理&lt;/td&gt;&#10; &lt;td&gt;只放值对象，禁止放业务逻辑&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;客户方-供应方&lt;/td&gt;&#10; &lt;td&gt;交易处理调用账务记账&lt;/td&gt;&#10; &lt;td&gt;同步 RPC，供应方定契约&lt;/td&gt;&#10; &lt;td&gt;契约变更必须双版本并行&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;防腐层&lt;/td&gt;&#10; &lt;td&gt;新模块访问老核心账户查询&lt;/td&gt;&#10; &lt;td&gt;适配器 + 模型转换&lt;/td&gt;&#10; &lt;td&gt;老模型绝不允许穿透进来&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;h3 id="一个反例"&gt;一个反例&#10;&lt;/h3&gt;&#10;&lt;p&gt;我们曾经把&amp;quot;限额&amp;quot;单独切成一个上下文。听起来很干净，实际上限额校验必须和交易受理在同一个事务里完成，跨服务调用一下就把联机响应时间从 40ms 拖到 90ms，还引入了新的失败分支。半年后合并回交易处理上下文。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;上下文边界的正确性，用一致性要求和调用频次验证，不用概念优雅度验证。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="战术落地聚合实体与值对象"&gt;战术落地：聚合、实体与值对象&#10;&lt;/h2&gt;&#10;&lt;p&gt;战术模式在核心系统里的价值远低于战略设计，但用对了确实能减少一批低级 bug。&lt;/p&gt;&#10;&lt;h3 id="聚合的粒度取决于一致性边界"&gt;聚合的粒度取决于一致性边界&#10;&lt;/h3&gt;&#10;&lt;p&gt;账务这块我们的聚合是&amp;quot;分录凭证&amp;quot;（Voucher），不是&amp;quot;账户&amp;quot;。一张凭证包含多条分录，借贷必须平衡，这是不可分割的一致性单元。账户余额是凭证记账的结果，通过事件更新。&lt;/p&gt;</description></item></channel></rss>