<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>博客 on 追风笔记</title><link>https://f91dba72.wangpeng.pages.dev/blog/</link><description>Recent content in 博客 on 追风笔记</description><generator>Hugo</generator><language>zh</language><lastBuildDate>Sat, 15 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://f91dba72.wangpeng.pages.dev/blog/index.xml" rel="self" type="application/rss+xml"/><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>我的 AI 工作流：从 Cubox 到 Claude</title><link>https://f91dba72.wangpeng.pages.dev/blog/ai-workflow/</link><pubDate>Sun, 24 May 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/ai-workflow/</guid><description>&lt;p&gt;试过很多工具组合，绝大多数最后都废了。留下来的这套很简单，只有三个环节：收口、加工、归档。工具会换，环节不会。&lt;/p&gt;&#10;&lt;h2 id="输入cubox-负责收口"&gt;输入：Cubox 负责收口&#10;&lt;/h2&gt;&#10;&lt;p&gt;信息源太散是最大的问题——微信文章、技术博客、内部文档、群里的链接。以前我在不同地方存不同来源，结果就是想找时哪儿都找不到。&lt;/p&gt;&#10;&lt;p&gt;现在的规则只有一条：&lt;strong&gt;所有想读的东西只进一个入口&lt;/strong&gt;。Cubox 的价值不在功能多，而在它在手机、浏览器、微信里都能一键收藏。&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;：架构 / 银行业务 / AI / 其他&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;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;收藏不等于阅读，阅读不等于沉淀。我给自己定的比例是每收藏十篇，至少有一篇要走完到沉淀环节。剩下九篇没读完也不内疚——收口的意义就是让它们不再占用注意力。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="加工claude-负责结构化"&gt;加工：Claude 负责结构化&#10;&lt;/h2&gt;&#10;&lt;p&gt;这一环是变化最大的。以前读完一篇长文要自己做摘要，成本高到经常跳过。现在的做法是把原文丢给 Claude，用固定的提示词做第一轮结构化：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-0037c5b6-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="text" data-td-line-count="9"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-0037c5b6-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. 核心论点（不超过三句）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 作者的关键假设与适用前提&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 论证中的证据类型（生产数据 / 基准测试 / 个人经验 / 无证据）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. 与我已有认知可能冲突的点&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. 值得进一步查证的三个问题&#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;要求：不要复述细节，不要评价文笔。若某项在原文中缺失，直接写&amp;#34;原文未提供&amp;#34;。&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;第 3 项和第 5 项是我加上去之后价值最大的两条。技术文章里很多结论是没有证据的个人经验，明确标出来能避免我把别人的经验当事实用。&lt;/p&gt;&#10;&lt;p&gt;对于代码类的材料，我会让它先跑一遍可行性判断，而不是直接接受：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-0037c5b6-fence-1" data-td-code data-td-code-auto-id&#10; data-td-language="bash" data-td-line-count="4"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-0037c5b6-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;# 让 Claude Code 直接在本地验证文章里的写法能不能跑通&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cbc -p &lt;span class="s2"&gt;&amp;#34;读取 notes/inbox/kafka-idempotent.md 里的示例代码，&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;在 sandbox/ 下建最小可运行工程验证结论是否成立，&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;把不成立的地方列出来并说明原因&amp;#34;&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;能跑通的写法才进笔记，跑不通的记录成&amp;quot;待查证&amp;quot;。这一步过滤掉了不少看起来很对的博客结论。&lt;/p&gt;</description></item><item><title>备考系统架构设计师：我的学习路径</title><link>https://f91dba72.wangpeng.pages.dev/blog/career-architect/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/career-architect/</guid><description>&lt;p&gt;考这个证的动机说实话不纯粹：一部分是行内职级评定需要，一部分是想系统补一遍自己知识体系里的窟窿。考完回头看，第二个收获比第一个大。&lt;/p&gt;&#10;&lt;h2 id="考什么"&gt;考什么&#10;&lt;/h2&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;/ul&gt;&#10;&lt;p&gt;三门里论文的通过率最低，也是唯一没法靠刷题突破的一门。&lt;/p&gt;&#10;&lt;h2 id="我的时间分配"&gt;我的时间分配&#10;&lt;/h2&gt;&#10;&lt;p&gt;总共投入大约四个月，每天一到两小时，周末多一点。分配比例大致是：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;综合知识 30%&lt;/strong&gt;。这门的正确策略是刷题而不是读教材。教材几百页，考点分布极散，直接刷历年真题，错题回教材定位。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;案例分析 30%&lt;/strong&gt;。真题必须动手写，不能只在脑子里过。写完对照答案，重点看答题的&lt;strong&gt;结构&lt;/strong&gt;而不是内容——阅卷看的是有没有踩到关键词。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;论文 40%&lt;/strong&gt;。这个比例是我调整过的，前两个月低估了论文难度。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-2254d37f-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="text" data-td-line-count="5"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-2254d37f-fence-0-viewport" data-td-code-viewport&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&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;第 1 月：通读教材建立地图 + 每天 30 题&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;第 2 月：真题选择题两轮 + 案例真题每周 2 套&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;第 3 月：论文四大方向各写一篇 + 案例真题每周 3 套&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;第 4 月：论文模板背熟 + 全真模拟 3 次（严格计时）&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;strong&gt;没写完&lt;/strong&gt;而不是写得差。&lt;/p&gt;&#10;&lt;h2 id="论文是分水岭"&gt;论文是分水岭&#10;&lt;/h2&gt;&#10;&lt;p&gt;论文的核心不是文采，是能不能在限定时间内把一个真实项目按固定结构讲清楚。我的模板是：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;项目背景与我的角色（约 400 字）&lt;/li&gt;&#10;&lt;li&gt;相关技术概念简述（约 400 字）&lt;/li&gt;&#10;&lt;li&gt;我在项目中如何应用（约 1300 字，这是主体）&lt;/li&gt;&#10;&lt;li&gt;遇到的问题与改进（约 400 字）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;主体部分要给出&lt;strong&gt;具体的数字和取舍&lt;/strong&gt;：为什么选 A 不选 B，上线后指标变化多少。空谈&amp;quot;提升了系统性能&amp;quot;是低分答案。&lt;/p&gt;</description></item><item><title>《领域驱动设计》读书笔记</title><link>https://f91dba72.wangpeng.pages.dev/blog/reading-notes-ddd/</link><pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/reading-notes-ddd/</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;那一节抄个定义就开始设计。但 Evans 花了整本书篇幅想说的其实是一件事：&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;一个团队如果没有和业务专家持续对话的机制，那么它用不用 DDD 的模式都无所谓——反正模型里没有真正的领域知识。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="我划重点的三处"&gt;我划重点的三处&#10;&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;第一是统一语言。&lt;/strong&gt; 这是全书性价比最高的概念，也最容易被跳过。如果业务方说&amp;quot;客户&amp;quot;指的是签约主体，而代码里的 &lt;code&gt;Customer&lt;/code&gt; 指的是自然人，那么所有后续设计都建立在一个错位的基础上。我们后来的做法很土：维护一份术语表，中英文对照，明确写出每个词的边界和不包含什么。上线一年下来，它减少的沟通成本比任何架构决策都多。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;第二是模型驱动设计的&amp;quot;绑定&amp;quot;要求。&lt;/strong&gt; 书里强调模型和实现必须绑死——模型改了代码就得改，代码改了模型文档也得改。做不到这一点，模型就退化成挂在墙上的装饰。这也是为什么我不再维护单独的模型图，而是让代码本身成为模型的唯一表达：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-04f985f8-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="10"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-04f985f8-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="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;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;RuleProcessor&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;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BigDecimal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BigDecimal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BigDecimal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;type&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; &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="c1"&gt;// accrual（计提）不简化成 calc，basis（计息基础）不省略&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InterestAccrualRule&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;public&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="nf"&gt;accrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DailyBalance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Rate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DayCountBasis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;basis&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; &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;strong&gt;第三是&amp;quot;深层模型&amp;quot;和重构的关系。&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;。不再让业务方念文档，而是当场画概念、当场确认术语。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;术语表进代码仓库&lt;/strong&gt;。放在 &lt;code&gt;docs/glossary.md&lt;/code&gt;，跟代码一起评审，改术语要走 PR。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;不再追求完整套用模式&lt;/strong&gt;。战术模式里我们只常用值对象和聚合，事件溯源、CQRS 一律不用，除非有明确的读写比例失衡问题。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;接受模型会被重写&lt;/strong&gt;。第一版模型的目标是能跑并暴露问题，不是一次做对。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;如果只有两小时读这本书，我建议读第一部分和第三部分的第 8 章（突破），跳过所有模式定义。模式的定义随便搜一下都有，而&amp;quot;如何通过对话获得领域知识&amp;quot;这件事，只有认真读原文才能体会到。&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>企业级 Java AI 框架选型：Spring AI vs LangChain4j</title><link>https://f91dba72.wangpeng.pages.dev/blog/java-ai-framework/</link><pubDate>Wed, 11 Mar 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/java-ai-framework/</guid><description>&lt;p&gt;去年团队要在 Java 技术栈上落地一批 AI 应用，绕不开这两个框架的选型。当时网上的对比文章基本都是把两边 README 抄一遍，看完还是不知道该选哪个。这篇写我们实际用下来的差异，包括最后为什么两个都在用。&lt;/p&gt;&#10;&lt;h2 id="选型的前提先定义需求"&gt;选型的前提：先定义需求&#10;&lt;/h2&gt;&#10;&lt;p&gt;框架选型没有绝对答案，只有对不对得上需求。我们的实际需求是这五条：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;必须能接私有化部署的模型&lt;/strong&gt;。行内模型走内网网关，协议兼容 OpenAI 但不完全一致，鉴权是自研的。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;必须能接入现有 Spring Boot 体系&lt;/strong&gt;。配置中心、链路追踪、指标上报、统一异常处理都已经是现成的。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;需要工具调用（function calling）&lt;/strong&gt;，且工具要以真实用户身份调用下游服务。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;需要 RAG，但向量库是行内统一提供的&lt;/strong&gt;，不是 Pinecone 这类云服务。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;要能做提示词版本管理和灰度&lt;/strong&gt;。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;注意第 4、5 条：这两条决定了框架自带的&amp;quot;开箱即用&amp;quot;能力对我们价值有限，我们更看重扩展点是否好写。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;选型时最该问的不是&amp;quot;哪个功能多&amp;quot;，而是&amp;quot;我要改的那部分，改起来疼不疼&amp;quot;。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="spring-ai-的取舍"&gt;Spring AI 的取舍&#10;&lt;/h2&gt;&#10;&lt;h3 id="优势"&gt;优势&#10;&lt;/h3&gt;&#10;&lt;p&gt;抽象设计非常 Spring。&lt;code&gt;ChatClient&lt;/code&gt; 的流式 API 写起来舒服，和 Spring Boot 的自动配置、&lt;code&gt;RestClient&lt;/code&gt;、Micrometer 打通得彻底。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-9058c652-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="23"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-9058c652-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;@Service&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductAdvisor&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;&#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;ChatClient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;chatClient&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="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ProductAdvisor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;VectorStore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;vectorStore&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatClient&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;builder&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;defaultSystem&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="na"&gt;defaultAdvisors&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;QuestionAnswerAdvisor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectorStore&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;MessageChatMemoryAdvisor&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;InMemoryChatMemory&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;build&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;&#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="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;consult&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;question&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;sessionId&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;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&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;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&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;advisors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&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="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;param&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;ChatMemory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CONVERSATION_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;sessionId&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;call&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;content&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;Advisor&lt;/code&gt; 这个扩展点设计得不错，本质是拦截器链。我们的护栏、审计留痕、数字接地校验全部实现成 Advisor，一次编写全局生效：&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>对客 AI Agent 平台建设思路</title><link>https://f91dba72.wangpeng.pages.dev/blog/ai-agent-platform/</link><pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/ai-agent-platform/</guid><description>&lt;p&gt;内部效率类的 AI 应用怎么做都不算太难，做错了也就是员工少用几次。对客不一样：一句话说错可能就是投诉、监管问询甚至赔付。这篇记录我们做对客 Agent 平台时的思路，重点不在模型，在约束。&lt;/p&gt;&#10;&lt;h2 id="对客场景的特殊约束"&gt;对客场景的特殊约束&#10;&lt;/h2&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;。查余额要认证到本人，代客操作要有明确授权链路。Agent 调用工具时的身份必须是真实用户身份，不是服务账号。&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;。用户在 App 里等超过三秒基本就退出了。多轮反思、多次检索这些提升准确率的手段，成本是延迟。&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;对客 Agent 的技术难点不是让它更聪明，而是让它在不确定的地方老实承认不知道，并且把用户交给正确的下一环节。&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;h3 id="网关层"&gt;网关层&#10;&lt;/h3&gt;&#10;&lt;p&gt;统一的模型接入层，屏蔽底层模型差异。做三件事：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;多模型路由与降级（主模型超时自动切备用模型）&lt;/li&gt;&#10;&lt;li&gt;配额与限流（按业务方、按用户维度双重限流）&lt;/li&gt;&#10;&lt;li&gt;计费与用量统计（按 token 归集到业务方成本中心）&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;这一层不允许有任何业务逻辑。它唯一的职责是让上层调用模型像调用一个稳定的内部服务。&lt;/p&gt;&#10;&lt;h3 id="编排层"&gt;编排层&#10;&lt;/h3&gt;&#10;&lt;p&gt;Agent 的运行时。定义一个 Agent 需要：系统提示词、可用工具集、知识库范围、护栏规则、降级策略。我们用配置描述而不是写代码，业务方在控制台就能改提示词并灰度发布。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-16b844fc-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="yaml" data-td-line-count="26"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-16b844fc-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;agent&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;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;retail-product-consult&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&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;model&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;primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;qwen-max&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;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;internal-7b&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;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0.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="nt"&gt;timeout_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;2500&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;autonomy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;tool_calling &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 可选 qa_only / tool_calling / multi_step&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_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&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;knowledge&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;collections&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="l"&gt;product-manual, faq-retail]&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;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&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;min_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0.62&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;tools&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;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;query_product_rate&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;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;user_token &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;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;query_my_balance&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;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;user_token&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;require_mfa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;guardrail&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;input&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="l"&gt;prompt_injection, sensitive_topic]&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;output&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="l"&gt;pii_mask, number_grounding, disclaimer_append]&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;fallback&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;on_model_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;rule_based_faq&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;on_low_confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;transfer_human&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;h3 id="工具层"&gt;工具层&#10;&lt;/h3&gt;&#10;&lt;p&gt;工具就是被包装成模型可调用形式的业务接口。我们的规范是：工具必须是幂等查询，或者是带确认步骤的操作。&lt;strong&gt;没有确认步骤的写操作一律不开放给 Agent。&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>单元化架构：银行核心的容灾与扩展底座</title><link>https://f91dba72.wangpeng.pages.dev/blog/unitization-deploy/</link><pubDate>Mon, 26 Jan 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/unitization-deploy/</guid><description>&lt;p&gt;单元化被讲成&amp;quot;异地多活的银弹&amp;quot;，但它真正解决的问题只有两个：把故障爆炸半径收进一个单元，以及让容量可以按单元横向叠加。理解不了这两点，做出来的往往只是一套更复杂的分库分表。&lt;/p&gt;&#10;&lt;h2 id="为什么核心要做单元化"&gt;为什么核心要做单元化&#10;&lt;/h2&gt;&#10;&lt;p&gt;传统主备架构的困境是：备机平时不承载流量，切换时没人敢按那个按钮。因为没人验证过备机在真实压力下能不能扛住。&lt;/p&gt;&#10;&lt;p&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;p&gt;切分维度只有一个原则：&lt;strong&gt;保证绝大多数交易在单元内闭环&lt;/strong&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;：适合零售，单元内闭环率通常能到 90% 以上&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;/p&gt;&#10;&lt;h2 id="路由与数据一致性"&gt;路由与数据一致性&#10;&lt;/h2&gt;&#10;&lt;p&gt;路由要在尽可能靠前的位置完成。我们的做法是接入层解析出客户号后直接算单元，错误路由的请求由应用层做二次校验并拒绝，绝不允许&amp;quot;就近处理&amp;quot;。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-e106a432-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-e106a432-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;routing&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;shard_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;customer_no&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;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;mod &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 客户号后四位取模，共 64 个逻辑单元&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;logical_units&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;64&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;mapping&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;unit-sh-01&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="m"&gt;0-15&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;unit-sh-02&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="m"&gt;16-31&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;unit-sz-01&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="m"&gt;32-47&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;unit-sz-02&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="m"&gt;48-63&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;global_unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unit-global&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;strict_check&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;逻辑单元数要一次性定足（我们定了 64），物理单元可以少。扩容时把逻辑单元区间搬到新物理单元，不用改路由算法，也不用重新哈希全量数据。&lt;/p&gt;&#10;&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;。有个模块直接按账号查库，没带客户号，单元化后查不到数据。所有 DAO 必须强制要求分片键，缺失就报错，不要默认广播查询。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;广播查询滥用&lt;/strong&gt;。运营后台要查&amp;quot;全行某状态的账户&amp;quot;，图省事写成遍历所有单元。单元多了之后这类查询直接拖垮全部单元。这类需求应该走离线数仓。&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;/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><item><title>开博第一篇：为什么写「追风笔记」</title><link>https://f91dba72.wangpeng.pages.dev/blog/welcome/</link><pubDate>Sat, 15 Aug 2026 00:00:00 +0000</pubDate><guid>https://f91dba72.wangpeng.pages.dev/blog/welcome/</guid><description>&lt;p&gt;做银行系统架构这些年，踩过的坑、想明白的事，大多散落在笔记本和聊天记录里。开这个站点，是想把它们系统地沉淀下来。&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;：客户信息系统（ECIF）、核心系统、分布式架构，以及领域驱动设计（DDD）在银行场景下的落地。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;银行业务专栏&lt;/strong&gt;：支付清算、计息与限额、账户/卡、反洗钱（AML）/KYC/CRS 等核心业务梳理。&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;blockquote&gt;&#10;&lt;p&gt;这个站点是示例内容，框架搭好后，真实文章会逐步替换进来。&lt;/p&gt;&#10;&lt;/blockquote&gt;</description></item></channel></rss>