<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>追风笔记</title><link>https://88ok.github.io/</link><description>Recent content on 追风笔记</description><generator>Hugo</generator><language>zh</language><lastBuildDate>Tue, 25 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://88ok.github.io/index.xml" rel="self" type="application/rss+xml"/><item><title>Dubbo 总体架构与核心概念</title><link>https://88ok.github.io/tech/dubbo/architecture/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/tech/dubbo/architecture/</guid><description>&lt;p&gt;Dubbo 的核心竞争力在于清晰的职责分层与高度可插拔的扩展机制。理解分层与 Invoker 抽象，是读懂 Dubbo 源码与排查线上问题的基础。&lt;/p&gt;&#10;&lt;h2 id="分层架构"&gt;分层架构&#10;&lt;/h2&gt;&#10;&lt;p&gt;Dubbo 自顶向下可分为若干逻辑层，每一层只依赖其下层的接口而非实现：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;service / config 层&lt;/strong&gt;：面向用户的 API 与配置层，&lt;code&gt;@DubboService&lt;/code&gt;、&lt;code&gt;@DubboReference&lt;/code&gt; 以及 &lt;code&gt;ReferenceConfig&lt;/code&gt;、&lt;code&gt;ServiceConfig&lt;/code&gt; 在此落地。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;proxy 层&lt;/strong&gt;：生成服务接口的动态代理，让远程调用对业务代码透明。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;registry 层&lt;/strong&gt;：封装服务注册与订阅，感知 provider/consumer 的上线下线。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;cluster 层&lt;/strong&gt;：将多个 Invoker 封装为「集群 Invoker」，负责负载均衡、容错与路由。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;protocol 层&lt;/strong&gt;：封装 RPC 调用，是 Invoker 暴露与引用的核心。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;filter 链&lt;/strong&gt;：贯穿调用的拦截器，可用于日志、鉴权、限流等横切逻辑。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="invoker-与-spi"&gt;Invoker 与 SPI&#10;&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;Invoker&lt;/code&gt; 是 Dubbo 的通用领域模型，代表「一个可执行且可描述的调用」，抽象了本地、远程、集群等不同类型的调用。&lt;code&gt;URL&lt;/code&gt; 则作为贯穿各层的配置总线，几乎所有扩展点都通过 URL 传递参数。&lt;/p&gt;&#10;&lt;p&gt;Dubbo 的扩展机制基于改进版的 JDK SPI，通过 &lt;code&gt;@SPI&lt;/code&gt; 注解声明扩展接口，&lt;code&gt;@Adaptive&lt;/code&gt; 生成自适应实现，&lt;code&gt;META-INF/dubbo/&lt;/code&gt; 下以接口全限定名命名的文件登记实现类：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-63727b2b-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="7"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-63727b2b-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;@SPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;dubbo&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="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Protocol&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;@Adaptive&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&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;Exporter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Invoker&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&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;invoker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;throws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RpcException&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;@Adaptive&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&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;Invoker&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;refer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Class&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&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;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;throws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RpcException&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="一次同步调用的链路"&gt;一次同步调用的链路&#10;&lt;/h2&gt;&#10;&lt;p&gt;当 consumer 发起一次同步调用时，请求大致经历以下步骤：&lt;/p&gt;</description></item><item><title>Kafka 存储与消费模型</title><link>https://88ok.github.io/tech/kafka/storage-consumer/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/tech/kafka/storage-consumer/</guid><description>&lt;p&gt;Kafka 的高性能依赖于「分区日志 + 顺序写 + 批量拉取」的设计。理解存储与消费模型，才能合理配置并行度与避免重复消费。&lt;/p&gt;&#10;&lt;h2 id="topic-与-partition"&gt;Topic 与 Partition&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;topic&lt;/strong&gt; 是逻辑主题，&lt;strong&gt;partition&lt;/strong&gt; 是物理并行单位，消息只追加写入分区末尾。&lt;/li&gt;&#10;&lt;li&gt;每个分区是一个有序、不可变的日志，由多个 segment 文件组成，并维护 offset（分区内唯一递增）。&lt;/li&gt;&#10;&lt;li&gt;分区数决定了消费的并行上限：一个分区同一时刻只被 group 内一个消费者消费。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="offset-与消费位移"&gt;Offset 与消费位移&#10;&lt;/h2&gt;&#10;&lt;p&gt;消费者处理完消息后提交的位移称为 &lt;strong&gt;offset&lt;/strong&gt;，Kafka 将其保存在内部 topic &lt;code&gt;__consumer_offsets&lt;/code&gt; 中：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-de365f5c-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="text" data-td-line-count="4"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-de365f5c-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;Producer -&amp;gt; [partition-0: 0,1,2,3 ...]&#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; | committed offset (group A)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Consumer group A 从 offset=4 继续拉取&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;&#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;/ul&gt;&#10;&lt;h2 id="consumer-group-与-rebalance"&gt;Consumer Group 与 Rebalance&#10;&lt;/h2&gt;&#10;&lt;p&gt;同一 group 内的消费者共同消费 topic 的全部分区，分区在成员间均衡分配。当成员加入/退出时触发 &lt;strong&gt;rebalance&lt;/strong&gt;，重新分配分区。频繁 rebalance 会暂停消费，应控制 &lt;code&gt;session.timeout&lt;/code&gt; 与 &lt;code&gt;heartbeat.interval&lt;/code&gt; 并避免消费耗时过长。&lt;/p&gt;</description></item><item><title>MySQL 索引原理与最佳实践</title><link>https://88ok.github.io/tech/mysql/indexing/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/tech/mysql/indexing/</guid><description>&lt;p&gt;索引是 MySQL 查询性能的决定性因素。理解 InnoDB 的 B+Tree 索引结构，才能写出真正命中索引的 SQL。&lt;/p&gt;&#10;&lt;h2 id="btree-与聚簇索引"&gt;B+Tree 与聚簇索引&#10;&lt;/h2&gt;&#10;&lt;p&gt;InnoDB 使用 B+Tree 组织索引，其特点是非叶子节点只存索引键用于导航，所有数据都落在叶子节点，且叶子节点通过双向链表串联，非常适合范围扫描。&lt;/p&gt;&#10;&lt;p&gt;InnoDB 的主键索引是&lt;strong&gt;聚簇索引&lt;/strong&gt;，叶子节点直接存放整行数据；二级索引的叶子节点存放「索引键 + 主键值」。因此通过二级索引查询非索引列时，需要再用主键回表查聚簇索引。&lt;/p&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;div class="td-code td-code--untitled" id="td-code-5f7f444b-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="sql" data-td-line-count="3"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-5f7f444b-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;-- idx_user_age 为 (name, age) 的联合索引&#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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;age&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="k"&gt;user&lt;/span&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;name&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;tom&amp;#39;&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;若 &lt;code&gt;SELECT *&lt;/code&gt; 还需要 &lt;code&gt;phone&lt;/code&gt; 等额外列，则会触发回表。高频查询应优先设计为覆盖索引。&lt;/p&gt;&#10;&lt;h2 id="最左前缀原则"&gt;最左前缀原则&#10;&lt;/h2&gt;&#10;&lt;p&gt;联合索引 &lt;code&gt;(a, b, c)&lt;/code&gt; 能生效的条件是查询从最左列开始连续匹配：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;WHERE a = ?&lt;/code&gt; 可用&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;WHERE a = ? AND b = ?&lt;/code&gt; 可用&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;WHERE b = ?&lt;/code&gt; 无法使用该联合索引（断开了最左前缀）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="避免索引失效"&gt;避免索引失效&#10;&lt;/h2&gt;&#10;&lt;p&gt;常见导致索引失效的写法：&lt;/p&gt;</description></item><item><title>Redis 数据结构与底层实现</title><link>https://88ok.github.io/tech/redis/data-structures/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/tech/redis/data-structures/</guid><description>&lt;p&gt;Redis 对外暴露五种常用类型，但底层有多种编码（encoding）。根据数据规模自动在不同编码间转换，是它兼顾内存与性能的关键。&lt;/p&gt;&#10;&lt;h2 id="核心底层结构"&gt;核心底层结构&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;SDS（简单动态字符串）&lt;/strong&gt;：相比 C 字符串记录长度、预分配空间，避免缓冲区溢出并支持二进制安全。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;ziplist / listpack&lt;/strong&gt;：连续内存的紧凑列表，节省空间，适合小数据量。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;linkedlist&lt;/strong&gt;：双向链表，元素多时替代 ziplist。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;dict（字典）&lt;/strong&gt;：哈希表，使用链地址法解决冲突，渐进式 rehash 避免阻塞。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;skiplist（跳表）&lt;/strong&gt;：多层有序链表，结合 dict 实现 zset，范围查询高效。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="各类型的编码与转换"&gt;各类型的编码与转换&#10;&lt;/h2&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;string&lt;/td&gt;&#10; &lt;td&gt;int / embstr&lt;/td&gt;&#10; &lt;td&gt;raw&lt;/td&gt;&#10; &lt;td&gt;长度 &amp;gt; 44 字节转 raw&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;hash&lt;/td&gt;&#10; &lt;td&gt;ziplist/listpack&lt;/td&gt;&#10; &lt;td&gt;hashtable&lt;/td&gt;&#10; &lt;td&gt;元素数或单个值超阈值&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;list&lt;/td&gt;&#10; &lt;td&gt;quicklist（ziplist 片段）&lt;/td&gt;&#10; &lt;td&gt;quicklist&lt;/td&gt;&#10; &lt;td&gt;—&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;set&lt;/td&gt;&#10; &lt;td&gt;intset&lt;/td&gt;&#10; &lt;td&gt;hashtable&lt;/td&gt;&#10; &lt;td&gt;含非整数或元素过多&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td&gt;zset&lt;/td&gt;&#10; &lt;td&gt;ziplist&lt;/td&gt;&#10; &lt;td&gt;skiplist+dict&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;p&gt;以 &lt;code&gt;zset&lt;/code&gt; 为例，小数据使用 ziplist 紧凑存储（member 与 score 相邻），当元素数量或单个 member 长度超过阈值时转为 &lt;code&gt;skiplist + dict&lt;/code&gt;，保证有序遍历与按 member 查找都为 O(1)/O(log n)。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-d3cd55a5-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="text" data-td-line-count="2"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-d3cd55a5-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;zset 小: [member1, score1, member2, score2, ...] (ziplist)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zset 大: dict(member-&amp;gt;score) + skiplist(score-&amp;gt;member)&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;理解这些阈值有助于解释「为什么小哈希比单独 key 省内存」，也提醒我们避免让单个 key 堆积过多元素，否则编码升级或操作耗时都会陡增。&lt;/p&gt;</description></item><item><title>ZooKeeper 数据模型与 ZAB 协议</title><link>https://88ok.github.io/tech/zookeeper/model-zab/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/tech/zookeeper/model-zab/</guid><description>&lt;p&gt;ZooKeeper 的可靠性来源于其简洁的数据模型与强一致的 ZAB 协议。理解二者是正确使用 ZK 的前提。&lt;/p&gt;&#10;&lt;h2 id="数据模型-znode"&gt;数据模型 znode&#10;&lt;/h2&gt;&#10;&lt;p&gt;ZK 维护一棵类似文件系统的层级树，每个节点称为 &lt;strong&gt;znode&lt;/strong&gt;，可同时承载数据与子节点。其类型决定了生命周期与并发语义：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;持久节点 / 临时节点&lt;/strong&gt;：临时节点随 session 断开自动删除，是分布式锁、选主的基础。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;普通节点 / 顺序节点&lt;/strong&gt;：顺序节点创建时自动追加单调递增序号。&lt;/li&gt;&#10;&lt;li&gt;每个 znode 带有 &lt;code&gt;version&lt;/code&gt;、&lt;code&gt;cversion&lt;/code&gt;、&lt;code&gt;aversion&lt;/code&gt;，修改时版本自增，提供乐观锁语义。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="session-与-watcher"&gt;Session 与 Watcher&#10;&lt;/h2&gt;&#10;&lt;p&gt;客户端与服务器建立 &lt;strong&gt;session&lt;/strong&gt;，通过心跳保活，session 超时则服务器清理其临时节点。客户端可对节点注册 &lt;strong&gt;Watcher&lt;/strong&gt;，节点发生变化（数据写、子节点变）时收到一次性通知，需重新注册才能继续监听。&lt;/p&gt;&#10;&lt;h2 id="zab-协议"&gt;ZAB 协议&#10;&lt;/h2&gt;&#10;&lt;p&gt;ZAB（ZooKeeper Atomic Broadcast）是为 ZK 设计的崩溃恢复原子广播协议，保证所有事务以相同顺序被所有副本应用。&lt;/p&gt;&#10;&lt;h3 id="崩溃恢复与选主"&gt;崩溃恢复与选主&#10;&lt;/h3&gt;&#10;&lt;p&gt;集群启动时或 Leader 宕机进入恢复阶段：选举产生新 Leader，并让各 Follower 补齐到与 Leader 相同的状态。选主依赖 &lt;code&gt;zxid&lt;/code&gt;（事务 id）与 &lt;code&gt;myid&lt;/code&gt;，zxid 越大代表数据越新，优先当选。&lt;/p&gt;&#10;&lt;h3 id="消息广播"&gt;消息广播&#10;&lt;/h3&gt;&#10;&lt;p&gt;正常阶段采用类似 2PC 的广播：Leader 为每个写请求分配递增 &lt;code&gt;zxid&lt;/code&gt; 并发起提议，Follower 写本地日志后 ACK，Leader 收到多数派 ACK 即提交并通知应用。&lt;/p&gt;</description></item><item><title>数据治理：从资产目录到口径统一</title><link>https://88ok.github.io/columns/data/data-governance/</link><pubDate>Sat, 08 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/data/data-governance/</guid><description>&lt;p&gt;很多银行的数据治理项目，最后都沦为「补元数据、填责任表、交汇报 PPT」。运动一过，元数据过期、口径继续打架、下游照样不敢用这份数据。治理之所以失效，是因为它被当成了一份额外的「填表工作」，而不是数据生产流水线本身的属性。真正有效的治理，是让目录、血缘、口径和质量规则&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;blockquote&gt;&#10;&lt;p&gt;治理的目标不是「漂亮的报告」，而是让下游系统敢用这份数据。如果一份数据没人敢用来做决策，它就算被打了满分标签，也还是负债。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;要扭转这个局面，得从三件最实在的事入手：盘清家底（资产目录）、追清来路（元数据血缘）、对齐说法（口径统一）。&lt;/p&gt;&#10;&lt;h2 id="资产目录先盘清家底"&gt;资产目录：先盘清家底&#10;&lt;/h2&gt;&#10;&lt;p&gt;资产目录回答的是「我们到底有哪些数据、归谁管、能不能用」。它不是 Excel 清单，而是带权属和分级的结构化注册表。&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;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;p&gt;目录必须和真实的元数据打通，否则就会出现「目录里说有、库里早就删了」的尴尬。一个可行的做法是：目录条目由采集任务自动生成，人工只补充业务语义，而不是反过来手工录入。&lt;/p&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;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;静态解析&lt;/strong&gt;：扫描 SQL、ETL 脚本、Spark/DAG 定义，提取表与字段级的输入输出关系。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;运行时采集&lt;/strong&gt;：在任务执行时记录实际读写的上下游，准确率最高但侵入性较强。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="影响分析"&gt;影响分析&#10;&lt;/h3&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;h3 id="口径冲突的例子"&gt;口径冲突的例子&#10;&lt;/h3&gt;&#10;&lt;p&gt;「月活客户」可能被定义为：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;渠道侧：当月登录 App 即算活跃；&lt;/li&gt;&#10;&lt;li&gt;零售侧：当月有动账交易才算活跃；&lt;/li&gt;&#10;&lt;li&gt;监管侧：按监管文件口径，且需去重。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;三个数字放在一起对比，结论自然矛盾。更糟的是，没人说得清哪个是「官方版本」。&lt;/p&gt;&#10;&lt;h3 id="指标字典与单一来源"&gt;指标字典与单一来源&#10;&lt;/h3&gt;&#10;&lt;p&gt;解决思路是建立指标字典：每个指标一条定义，包含口径 SQL、维度、过滤条件、负责人和生效时间。下游统一从字典取数，禁止各自重写逻辑。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-e85092e4-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="sql" data-td-line-count="7"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-e85092e4-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="n"&gt;cust_id&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;SUM&lt;/span&gt;&lt;span class="p"&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="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cal_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;avg_daily_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="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dwd_account_daily&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="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cal_date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BETWEEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;start&lt;/span&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="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;end&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;balance_type&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;SAVING&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="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;cust_id&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;口径变更要走版本管理，旧口径保留可追溯，新口径标注生效日，避免历史报表被悄悄改义。&lt;/p&gt;</description></item><item><title>聚合根与领域事件：DDD 战术建模</title><link>https://88ok.github.io/columns/architecture/ddd-aggregates/</link><pubDate>Sun, 15 Mar 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/architecture/ddd-aggregates/</guid><description>&lt;p&gt;银行核心系统里最容易被低估的概念，是「聚合」。很多人把 DDD 战术设计理解成「给实体加几个注解」，结果聚合越写越大，事务越来越长，最后把数据库和团队都拖垮。本文把聚合根和领域事件放回它们本来的位置：一致性边界的设计工具，以及领域向外界发声的窗口。&lt;/p&gt;&#10;&lt;h2 id="为什么需要聚合"&gt;为什么需要聚合&#10;&lt;/h2&gt;&#10;&lt;p&gt;领域模型不是一张 ER 图。ER 图关心「数据怎么存」，聚合关心「哪些数据必须在同一个事务里保持一致」。这两件事经常错位，而错位的代价在金融场景里格外昂贵——账务不平、状态错乱，往往不是因为算法错，而是因为一致性边界划错了。&lt;/p&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;blockquote&gt;&#10;&lt;p&gt;经验法则：如果两条业务规则必须同时为真，它们大概率属于同一个聚合；如果可以接受短暂的不一致，它们就分属不同聚合。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h3 id="聚合不是大对象"&gt;聚合不是「大对象」&#10;&lt;/h3&gt;&#10;&lt;p&gt;最常见的反模式，是把「客户」做成包含地址、联系人、合同、账户、画像的超级对象。它在概念上很完整，但在工程上是个灾难：每次改一个手机号，都要加载半个数据库，还要锁住一堆根本无关的数据。&lt;/p&gt;&#10;&lt;p&gt;聚合的边界应以&lt;strong&gt;事务一致性&lt;/strong&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;h3 id="引用靠-id不靠对象"&gt;引用靠 ID，不靠对象&#10;&lt;/h3&gt;&#10;&lt;p&gt;跨聚合的关联，永远用标识符，而不是对象引用。聚合 A 不应直接持有聚合 B 的对象，否则两个聚合会被悄悄绑进同一事务。&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-8481552c-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="20"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-8481552c-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="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;ownerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 跨聚合引用：只存 ID&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;Money&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;&#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;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DomainEvent&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;pendingEvents&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;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;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="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&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="w"&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="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;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isLessThan&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="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;InsufficientBalanceException&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;&#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;balance&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;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subtract&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;&#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;pendingEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&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;MoneyWithdrawn&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;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;now&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;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DomainEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;drainEvents&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="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;copy&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;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pendingEvents&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;pendingEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&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;copy&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;h3 id="工厂与仓储"&gt;工厂与仓储&#10;&lt;/h3&gt;&#10;&lt;p&gt;复杂聚合的创建交给工厂，避免调用方掌握内部构造细节；聚合的持久化交给仓储，且&lt;strong&gt;仓储只按聚合根 ID 加载整个聚合&lt;/strong&gt;，不存在「只查一半」的仓储方法。&lt;/p&gt;</description></item><item><title>支付清算全流程拆解</title><link>https://88ok.github.io/columns/banking/payment-clearing/</link><pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/banking/payment-clearing/</guid><description>&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;当你在 App 上给朋友转 1000 元，这笔指令从你的银行出发，要穿越自己的核心系统、跨行清算通道、对方银行的核心系统，最后落到对方账户。中间任何一环的状态错位，都会让这笔钱「卡在半路」。所以支付系统设计的第一个原则，是&lt;strong&gt;把每个阶段的状态显式建模&lt;/strong&gt;，而不是用一个「已转账」含糊带过。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;支付系统最危险的 bug，不是算错金额，而是状态机说不清一笔钱现在到底在哪：是已扣、已发、已清算、已结算，还是已退回。&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;h3 id="清分轧差与净额"&gt;清分：轧差与净额&#10;&lt;/h3&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;：把一段时间内的多笔借贷相互抵扣，只交差额。比如 A 欠 B 100 万、B 欠 A 80 万，净额就是 A 再付 B 20 万。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="结算资金的真实位移"&gt;结算：资金的真实位移&#10;&lt;/h3&gt;&#10;&lt;p&gt;结算是资金在央行或清算机构的账户上真正转移，借贷最终平衡。只有结算完成，这笔钱才算「落地」。在此之前，它只是账面上的债权。&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;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;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;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;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;/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;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;大额支付系统（HVPS）&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;tr&gt;&#10; &lt;td&gt;小额支付系统（BEPS）&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;tr&gt;&#10; &lt;td&gt;网上支付跨行清算&lt;/td&gt;&#10; &lt;td&gt;逐笔/批量&lt;/td&gt;&#10; &lt;td&gt;7x24、准实时&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;p&gt;大额走全额是为了安全和实时，小额走净额是为了降低流动性占用和通道成本。设计路由时，按金额和时效要求分流，而不是一刀切全走大额。&lt;/p&gt;</description></item><item><title>PageFly</title><link>https://88ok.github.io/projects/pagefly/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://88ok.github.io/projects/pagefly/</guid><description>&lt;p&gt;项目主页见上方卡片链接，点击直接跳转至 &lt;a href="https://fly.882680.xyz"&gt;https://fly.882680.xyz&lt;/a&gt;。&lt;/p&gt;</description></item><item><title>Kafka 可靠性与精确一次</title><link>https://88ok.github.io/tech/kafka/reliability/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/tech/kafka/reliability/</guid><description>&lt;p&gt;Kafka 的可靠性需要在 producer、broker、consumer 三端协同配置。下面从「不丢、不重、有序」三个目标展开。&lt;/p&gt;&#10;&lt;h2 id="producer-端的可靠性"&gt;Producer 端的可靠性&#10;&lt;/h2&gt;&#10;&lt;p&gt;通过 &lt;code&gt;acks&lt;/code&gt; 控制写入持久化级别：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;acks=0&lt;/code&gt;：发完即认为成功，可能丢消息，吞吐最高。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;acks=1&lt;/code&gt;：leader 写入即成功，leader 宕机可能丢。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;acks=all&lt;/code&gt;：ISR 全部同步才成功，最安全，配合 &lt;code&gt;min.insync.replicas&lt;/code&gt; 防单点。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;开启重试（&lt;code&gt;retries&lt;/code&gt;）可应对瞬时失败，但会带来&lt;strong&gt;重复&lt;/strong&gt;：网络抖动导致 producer 未收到 ack 而重试，同一条消息被写两次。&lt;/p&gt;&#10;&lt;h2 id="幂等-producer-与事务"&gt;幂等 Producer 与事务&#10;&lt;/h2&gt;&#10;&lt;p&gt;Kafka 提供 &lt;strong&gt;幂等 producer&lt;/strong&gt;（&lt;code&gt;enable.idempotence=true&lt;/code&gt;），broker 用 &lt;code&gt;producerId + 序列号&lt;/code&gt; 去重，保证单分区内不重不漏。需要跨分区、跨系统的原子写入时，使用&lt;strong&gt;事务&lt;/strong&gt;（&lt;code&gt;transactional.id&lt;/code&gt;）将多次 produce 与 offset 提交纳入一个事务：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-5a2edc9c-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="5"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-5a2edc9c-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;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initTransactions&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;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;beginTransaction&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;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record1&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;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record2&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;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;commitTransaction&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="消费端去重与幂等"&gt;消费端去重与幂等&#10;&lt;/h2&gt;&#10;&lt;p&gt;即使 producer 幂等，consumer 在 rebalance 或位移提交时机不当时仍可能重复消费。消费端应保证&lt;strong&gt;业务幂等&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;用唯一键（订单号等）做去重表/唯一索引。&lt;/li&gt;&#10;&lt;li&gt;将「处理 + 提交位移」放在同一事务（如消费写 DB 同时记录 offset）。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="分区顺序性"&gt;分区顺序性&#10;&lt;/h2&gt;&#10;&lt;p&gt;Kafka 只保证&lt;strong&gt;单分区内有序&lt;/strong&gt;。需要全局顺序只能单分区（牺牲并行）；需要业务顺序则按 key（如用户 id）分区，使同一 key 落到同一分区，从而在该 key 维度保持顺序。精确一次（EOS）正是幂等 + 事务 + 消费幂等三者叠加的结果。&lt;/p&gt;</description></item><item><title>MySQL 事务与锁机制</title><link>https://88ok.github.io/tech/mysql/transaction-lock/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/tech/mysql/transaction-lock/</guid><description>&lt;p&gt;并发场景下，事务与锁共同保证了数据的一致性。InnoDB 通过 MVCC 与锁的协同，在性能与隔离性之间取得平衡。&lt;/p&gt;&#10;&lt;h2 id="acid-与隔离级别"&gt;ACID 与隔离级别&#10;&lt;/h2&gt;&#10;&lt;p&gt;事务的 ACID 由不同机制保障：原子性靠 undo log，持久性靠 redo log，隔离性靠 MVCC 与锁，一致性是最终目标。&lt;/p&gt;&#10;&lt;p&gt;SQL 标准定义四种隔离级别，InnoDB 默认 &lt;code&gt;REPEATABLE READ&lt;/code&gt;：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;READ UNCOMMITTED：可能脏读&lt;/li&gt;&#10;&lt;li&gt;READ COMMITTED：避免脏读，可能不可重复读&lt;/li&gt;&#10;&lt;li&gt;REPEATABLE READ：避免不可重复读（InnoDB 额外避免幻读）&lt;/li&gt;&#10;&lt;li&gt;SERIALIZABLE：完全串行，性能最低&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="mvcc-与日志"&gt;MVCC 与日志&#10;&lt;/h2&gt;&#10;&lt;p&gt;MVCC（多版本并发控制）让读不加锁、读写不阻塞。每行记录隐含 &lt;code&gt;trx_id&lt;/code&gt; 与 &lt;code&gt;roll_pointer&lt;/code&gt;，通过 undo log 构建历史版本，配合 ReadView 判断某版本对当前事务是否可见。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;undo log&lt;/strong&gt;：记录数据修改前的镜像，用于回滚与构建旧版本。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;redo log&lt;/strong&gt;：记录物理页修改，保证崩溃后已提交事务不丢失（WAL 机制）。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="行锁间隙锁与-next-key-lock"&gt;行锁、间隙锁与 Next-Key Lock&#10;&lt;/h2&gt;&#10;&lt;p&gt;InnoDB 默认使用行锁，但「锁的是索引记录」而非行本身。在 &lt;code&gt;REPEATABLE READ&lt;/code&gt; 下，为解决幻读引入：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;行锁（Record Lock）&lt;/strong&gt;：锁住具体索引记录。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;间隙锁（Gap Lock）&lt;/strong&gt;：锁住索引记录之间的间隙，阻止插入。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Next-Key Lock&lt;/strong&gt;：行锁 + 间隙锁，锁定左开右闭的区间。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-634a084d-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="sql" data-td-line-count="2"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-634a084d-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, 20] 区间加 Next-Key Lock，阻止其他事务插入 id=15&#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;t&lt;/span&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;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BETWEEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&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="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FOR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UPDATE&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;h2 id="死锁成因与排查"&gt;死锁成因与排查&#10;&lt;/h2&gt;&#10;&lt;p&gt;死锁通常由两个事务以相反顺序获取锁引起。排查手段：&lt;/p&gt;</description></item><item><title>Redis 缓存设计</title><link>https://88ok.github.io/tech/redis/cache-design/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/tech/redis/cache-design/</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;：查询不存在的数据，缓存与数据库都查不到，请求每次都打到 DB。应对：缓存空值（短过期）、布隆过滤器拦截非法 key。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;缓存击穿&lt;/strong&gt;：某个热点 key 过期瞬间，大量并发同时回源 DB。应对：互斥锁（只放一个线程重建）、逻辑过期（异步刷新）、热点 key 不过期。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;缓存雪崩&lt;/strong&gt;：大量 key 同一时刻集中失效，或 Redis 整体不可用，DB 被冲垮。应对：过期时间加随机抖动、多级缓存、Redis 高可用（哨兵/集群）。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="cache-aside-与一致性"&gt;Cache-Aside 与一致性&#10;&lt;/h2&gt;&#10;&lt;p&gt;最常用的是 &lt;strong&gt;Cache-Aside（旁路缓存）&lt;/strong&gt;：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-9a055417-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="text" data-td-line-count="2"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-9a055417-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;读: 先读缓存 -&amp;gt; 命中返回; 未命中读 DB -&amp;gt; 写入缓存&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;写: 先更新 DB -&amp;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;「先更新 DB 再删缓存」可降低并发下读到旧值的概率；极端不一致可通过延迟双删或订阅 binlog（如 Canal）补偿。注意：不要盲目「更新缓存」，否则并发写可能产生脏数据。&lt;/p&gt;&#10;&lt;h2 id="大-key-与热-key"&gt;大 key 与热 key&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;大 key&lt;/strong&gt;：如包含百万元素的 hash/list，删除或序列化会阻塞主线程。应对：拆分、分批删除（&lt;code&gt;unlink&lt;/code&gt;）、定期清理。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;热 key&lt;/strong&gt;：单个 key 访问极高，单节点成为瓶颈。应对：本地缓存副本、key 打散到多个副本、Redis 集群分片。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="过期与淘汰策略"&gt;过期与淘汰策略&#10;&lt;/h2&gt;&#10;&lt;p&gt;Redis 提供主动 + 惰性两种过期删除，内存不足时按 &lt;code&gt;maxmemory-policy&lt;/code&gt; 淘汰：&lt;/p&gt;</description></item><item><title>ZooKeeper 分布式协调实战</title><link>https://88ok.github.io/tech/zookeeper/coordination/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/tech/zookeeper/coordination/</guid><description>&lt;p&gt;ZooKeeper 提供原语级的协调能力，几乎所有分布式协调需求都可基于「临时节点 + 顺序节点 + Watcher」组合实现。&lt;/p&gt;&#10;&lt;h2 id="选主master-election"&gt;选主（Master Election）&#10;&lt;/h2&gt;&#10;&lt;p&gt;每个候选者在 &lt;code&gt;/election&lt;/code&gt; 下创建临时顺序节点，序号最小的即成为 Master。非最小节点监听其前一个节点，前驱消失时重新判断自己是否最小，从而实现故障自动转移：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-96c7e587-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="text" data-td-line-count="4"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-96c7e587-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;/election&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── /n_0000000001 (master)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── /n_0000000002 (watch 001)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── /n_0000000003 (watch 002)&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="配置中心"&gt;配置中心&#10;&lt;/h2&gt;&#10;&lt;p&gt;将配置写入持久节点，客户端读取并注册 Watcher，配置变更时收到通知后热更新，无需重启应用。注意配置节点应控制体积，避免单次推送过大。&lt;/p&gt;&#10;&lt;h2 id="分布式锁"&gt;分布式锁&#10;&lt;/h2&gt;&#10;&lt;p&gt;基于「临时顺序节点 + 监听前驱」实现公平排他锁，比简单 &lt;code&gt;create&lt;/code&gt; 抢锁更健壮、可避免惊群。&lt;/p&gt;&#10;&lt;h3 id="羊群效应herd-effect"&gt;羊群效应（Herd Effect）&#10;&lt;/h3&gt;&#10;&lt;p&gt;若所有客户端都监听同一个锁节点，释放时会被同时唤醒并争抢，产生大量无效请求。正确做法是&lt;strong&gt;只监听比自己序号小的前一个节点&lt;/strong&gt;，形成链式唤醒，将 O(N) 通知降为 O(1)。&lt;/p&gt;&#10;&lt;h3 id="临时节点的坑"&gt;临时节点的坑&#10;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;客户端 GC 停顿或网络抖动可能导致 session 超时，临时节点被误删而锁提前释放。需结合合理的 &lt;code&gt;sessionTimeout&lt;/code&gt; 与心跳。&lt;/li&gt;&#10;&lt;li&gt;持锁方崩溃后锁自动释放，但业务未提交，需保证操作幂等。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="与-etcd-的取舍"&gt;与 etcd 的取舍&#10;&lt;/h2&gt;&#10;&lt;p&gt;etcd 基于 Raft，提供更强一致与租约（lease）、Watch 增量推送，API 更简单，云原生场景（Kubernetes）更主流；ZK 生态成熟、客户端丰富，但在超大规模 Watch 下存在性能瓶颈。新项目可优先考虑 etcd，存量 Dubbo/Hadoop 体系仍常用 ZK。&lt;/p&gt;</description></item><item><title>服务注册与发现实战</title><link>https://88ok.github.io/tech/dubbo/registry-discovery/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/tech/dubbo/registry-discovery/</guid><description>&lt;p&gt;服务注册与发现是微服务架构的基石。Dubbo 将注册中心抽象为 &lt;code&gt;Registry&lt;/code&gt; 接口，因此可以在 ZooKeeper、Nacos、Consul 等实现之间平滑切换，业务代码无需改动。&lt;/p&gt;&#10;&lt;h2 id="注册中心接入"&gt;注册中心接入&#10;&lt;/h2&gt;&#10;&lt;p&gt;以 Nacos 为例，只需在配置中声明注册中心地址与协议即可。Dubbo 在启动时自动完成服务导出（export）与订阅（subscribe）：&lt;/p&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-404ebe0c-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="yaml" data-td-line-count="8"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-404ebe0c-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;dubbo&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;application&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;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;order-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="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;registry&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;address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nacos://127.0.0.1:8848&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;protocol&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;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;dubbo&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;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;20880&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;Provider 启动后会在注册中心写入自身元数据（接口、IP、端口、权重等）；Consumer 启动后订阅接口节点，拿到可用 Provider 列表并缓存到本地，后续调用直接走本地缓存，降低注册中心压力。&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;：基于临时节点（ZooKeeper）或心跳（Nacos）感知进程存活，进程宕机时节点被摘除，Consumer 收到 &lt;code&gt;unregister&lt;/code&gt; 通知后剔除实例。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;应用侧优雅下线&lt;/strong&gt;：收到 &lt;code&gt;SIGTERM&lt;/code&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;ol&gt;&#10;&lt;li&gt;下线前通过注册中心将实例权重置 0，使其不再接收新流量。&lt;/li&gt;&#10;&lt;li&gt;等待预热期与在途请求排空（如 10s）。&lt;/li&gt;&#10;&lt;li&gt;再执行实际进程关闭。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;配合 Consumer 端的失败重试与 &lt;code&gt;Cluster&lt;/code&gt; 容错（如 &lt;code&gt;Failover&lt;/code&gt;），可基本实现调用方无感知的版本迭代。需要注意：注册中心通知存在网络延迟，Consumer 本地缓存与重试机制是兜底关键。&lt;/p&gt;</description></item><item><title>计息与限额：算得清才能管得住</title><link>https://88ok.github.io/columns/banking/interest-limit/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/banking/interest-limit/</guid><description>&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;code&gt;本金 × 利率 × 天数 / 基数&lt;/code&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;：一年按 365 还是 360？结息日遇节假日顺延还是提前？&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;利率变动&lt;/strong&gt;：固定利率、浮动利率重定价日如何衔接，分段计息怎么切。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-3621cf47-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="4"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-3621cf47-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;BigDecimal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;interest&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;dailyBalance&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;multiply&lt;/span&gt;&lt;span class="p"&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;&#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;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BigDecimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&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;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BigDecimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;360&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RoundingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;HALF_UP&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;blockquote&gt;&#10;&lt;p&gt;计息 bug 最可怕的结局，不是算错一笔，而是「每天错一点点，半年后对不上，却找不到从哪天开始错」。所以计息必须有可复现的逐日明细。&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;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;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;ul&gt;&#10;&lt;li&gt;限额规则要可配置，且变更留痕、可追溯生效时间。&lt;/li&gt;&#10;&lt;li&gt;累计类限额必须考虑并发：用原子计数或数据库约束，别靠「先查后扣」。&lt;/li&gt;&#10;&lt;li&gt;超限处理要明确：拒绝、转人工、还是降级，不能悄悄放行。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-3621cf47-fence-1" 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-3621cf47-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="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;dailyUsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&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="na"&gt;compareTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dayLimit&lt;/span&gt;&lt;span class="p"&gt;)&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;0&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;LimitExceededException&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="n"&gt;dailyUsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dayLimit&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;blockquote&gt;&#10;&lt;p&gt;限额系统宁可「误杀一笔正常交易」，也不能「放过一笔该拦的」。前者是体验问题，后者是风险事件。&lt;/p&gt;</description></item><item><title>CQRS 与 Saga：复杂业务的读写分离与最终一致</title><link>https://88ok.github.io/columns/architecture/cqrs-saga/</link><pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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>消息队列选型：Kafka vs Pulsar vs RabbitMQ</title><link>https://88ok.github.io/columns/data/message-queue/</link><pubDate>Sat, 28 Feb 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/data/message-queue/</guid><description>&lt;p&gt;消息队列是分布式系统的神经系统，但 Kafka、Pulsar、RabbitMQ 三者的设计哲学差异极大。选错不是「换个客户端」的事，而是架构重做。下面从几个工程最关心的维度拆开看。&lt;/p&gt;&#10;&lt;h2 id="模型差异是根本"&gt;模型差异是根本&#10;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;：经典消息代理，面向「消息」和「队列」，支持丰富路由（直连、主题、头部、扇出）。适合任务分发、低延迟小消息。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Kafka&lt;/strong&gt;：日志型、分区有序、拉取消费，面向「流」和「事件溯源」。适合高吞吐、可重放。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Pulsar&lt;/strong&gt;：计算存储分离，分层架构，原生多租户和跨地域复制。适合既要 Kafka 的流、又要 RabbitMQ 的队列语义的统一平台。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-e7471357-fence-0" 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-e7471357-fence-0-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;# Kafka 顺序消费依赖分区&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bin/kafka-console-consumer.sh &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 orders --bootstrap-server b1:9092 &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; --partition &lt;span class="m"&gt;0&lt;/span&gt; --offset earliest&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="关键维度对比"&gt;关键维度对比&#10;&lt;/h2&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;Kafka&lt;/th&gt;&#10; &lt;th scope="col"&gt;Pulsar&lt;/th&gt;&#10; &lt;th scope="col"&gt;RabbitMQ&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;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;tr&gt;&#10; &lt;td&gt;顺序性&lt;/td&gt;&#10; &lt;td&gt;分区内有序&lt;/td&gt;&#10; &lt;td&gt;分区/Key 有序&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;高（含 BookKeeper）&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;h2 id="选型思路"&gt;选型思路&#10;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;事件流、日志、可重放&lt;/strong&gt; → Kafka 稳。审计、CDC、行为埋点这类场景它的重放能力几乎是刚需。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;统一消息平台、多租户、跨地域&lt;/strong&gt; → Pulsar 更合适，但团队要扛得住运维。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;任务队列、复杂路由、低延迟&lt;/strong&gt; → RabbitMQ 更直接。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-e7471357-fence-1" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="4"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-e7471357-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;// 用消息头做复杂路由（RabbitMQ 风格）&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;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;basicPublish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;orders.topic&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="s"&gt;&amp;#34;order.created.vip&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// routingKey&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;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;body&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;blockquote&gt;&#10;&lt;p&gt;别用 RabbitMQ 扛日均百亿事件流，也别用 Kafka 做需要复杂路由的任务分发——用对的工具，比用最强的工具重要。&lt;/p&gt;</description></item><item><title>投资顾问系统</title><link>https://88ok.github.io/projects/investment-advisor/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://88ok.github.io/projects/investment-advisor/</guid><description>&lt;p&gt;项目主页见上方卡片链接。&lt;/p&gt;</description></item><item><title>AML/KYC：合规不是绊脚石</title><link>https://88ok.github.io/columns/banking/aml-kyc/</link><pubDate>Tue, 14 Jul 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/banking/aml-kyc/</guid><description>&lt;p&gt;一提 AML/KYC，业务侧往往皱眉：开户变慢、交易被拦。但合规不是外挂的「麻烦」，而是银行必须内建的能力。把它当系统一等公民来设计，风控和体验才能双赢。&lt;/p&gt;&#10;&lt;h2 id="kyc先搞清楚你是谁"&gt;KYC：先搞清楚你是谁&#10;&lt;/h2&gt;&#10;&lt;p&gt;KYC 是起点，不是收集证件复印件，而是建立&lt;strong&gt;持续可验证的客户画像&lt;/strong&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;div class="td-code td-code--untitled" id="td-code-979c12bf-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="yaml" data-td-line-count="3"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-979c12bf-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;customer_risk&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;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;high&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;factors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;[virtual_asset, high_risk_region, pep&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="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="aml盯着钱往哪走"&gt;AML：盯着钱往哪走&#10;&lt;/h2&gt;&#10;&lt;p&gt;反洗钱看交易行为与资金路径，而非单次金额大小：名单监控、交易监测（规则+模型打分）、可疑报告（STR）走调查上报流程。&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;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;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;/p&gt;</description></item><item><title>六边形架构在银行核心系统的落地</title><link>https://88ok.github.io/columns/architecture/hex-architecture/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/architecture/hex-architecture/</guid><description>&lt;p&gt;银行核心最痛的不是业务复杂，而是&lt;strong&gt;业务逻辑和周边技术死死绑在一起&lt;/strong&gt;：换连接池要改核心，接新渠道要动账务，单测要起一整套中间件。六边形架构（Ports &amp;amp; Adapters）给出干净解法。&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;：纯业务规则，不依赖任何框架、数据库或 HTTP 客户端。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;端口&lt;/strong&gt;：核心暴露（入站）和需要（出站）的接口，用 Java 接口表达意图。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;适配器&lt;/strong&gt;：把端口接到具体技术——REST 控制器是入站适配器，JDBC 仓储是出站适配器。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-89492c2e-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="java" data-td-line-count="5"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-89492c2e-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;interface&lt;/span&gt; &lt;span class="nc"&gt;AccountRepository&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="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;Account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&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;&#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;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&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="w"&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="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="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;JdbcAccountRepository&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AccountRepository&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="cm"&gt;/* 技术留在外围 */&lt;/span&gt;&lt;span class="w"&gt; &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;blockquote&gt;&#10;&lt;p&gt;判断架构是否干净的一条硬标准：删掉数据库、MQ、Web 框架，核心业务代码还能原封不动地跑并被测。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="在银行核心里怎么切"&gt;在银行核心里怎么切&#10;&lt;/h2&gt;&#10;&lt;p&gt;核心账务、计息、限额放在中心；大小额网关、CBS 接口、短信、风控回调全做成外围适配器。监管规则变了只动核心，接新清算通道只加一个适配器。&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;tr&gt;&#10; &lt;td&gt;适配层&lt;/td&gt;&#10; &lt;td&gt;REST/JDBC/MQ&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;收益是可单测、可独立部署、渠道可替换；代价是初期抽象成本与团队纪律。在银行这种长生命周期系统里，核心被技术债锁死的代价远高于前期投入。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;架构不是墙上的图，而是写在依赖箭头里的纪律。六边形的价值，是让「技术变了业务不动」成为默认结果。&lt;/p&gt;&#10;&lt;/blockquote&gt;</description></item><item><title>分库分表策略与热点治理</title><link>https://88ok.github.io/columns/data/sharding-strategy/</link><pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/data/sharding-strategy/</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;：性别这种分片键会制造大分片；自增 ID 做键会集中在最新分片，形成写入热点。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;兼顾关联查询&lt;/strong&gt;：同一客户的账户、交易最好同片，减少跨片 JOIN。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="td-code td-code--untitled" id="td-code-c3928bd6-fence-0" data-td-code data-td-code-auto-id&#10; data-td-language="sql" data-td-line-count="3"&gt;&#10; &lt;div class="td-code__viewport" id="td-code-c3928bd6-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;-- 按客户号哈希分 1024 片&#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;trans_$&lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cust_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="err"&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="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cust_id&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="o"&gt;?&lt;/span&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;trans_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="o"&gt;?&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;h2 id="扩容预分片优于临时拆"&gt;扩容：预分片优于临时拆&#10;&lt;/h2&gt;&#10;&lt;p&gt;一开始就按「未来三年规模」定足够多的逻辑分片（如 1024、4096），物理上先少后多。扩容只是把逻辑分片映射到新物理库，&lt;strong&gt;数据迁移量远小于重新分片&lt;/strong&gt;。&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;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;div class="td-code td-code--untitled" id="td-code-c3928bd6-fence-1" 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-c3928bd6-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="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;logic&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="s"&gt;&amp;#34;trans_&amp;#34;&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;custId&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;1024&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;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;phys&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;routeTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logic&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&#10;&lt;/div&gt;&#10;&lt;h2 id="热点治理"&gt;热点治理&#10;&lt;/h2&gt;&#10;&lt;p&gt;即使哈希均匀，业务上仍会有「明星账户」「大商户」成为单点热点。&lt;/p&gt;</description></item><item><title>认知匹配 / AI 认知画像</title><link>https://88ok.github.io/projects/cognitive-match/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://88ok.github.io/projects/cognitive-match/</guid><description>&lt;p&gt;项目主页见上方卡片链接。&lt;/p&gt;</description></item><item><title>中文 AI 人味化 skill</title><link>https://88ok.github.io/projects/ai-humanizer-skill/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://88ok.github.io/projects/ai-humanizer-skill/</guid><description>&lt;p&gt;项目主页见上方卡片链接。&lt;/p&gt;</description></item><item><title>银行核心系统建模随笔</title><link>https://88ok.github.io/blog/banking-core-model/</link><pubDate>Sun, 02 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/blog/redis-cache-pitfall/</link><pubDate>Wed, 08 Jul 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/projects/banking-ontology/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://88ok.github.io/projects/banking-ontology/</guid><description>&lt;p&gt;项目主页见上方卡片链接。&lt;/p&gt;</description></item><item><title>分库分表不是银弹</title><link>https://88ok.github.io/blog/mysql-sharding/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/blog/ai-workflow/</link><pubDate>Sun, 24 May 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/blog/career-architect/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/blog/reading-notes-ddd/</link><pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/blog/kafka-practice/</link><pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/blog/java-ai-framework/</link><pubDate>Wed, 11 Mar 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/blog/distributed-transaction/</link><pubDate>Mon, 23 Feb 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/blog/ai-agent-platform/</link><pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/blog/unitization-deploy/</link><pubDate>Mon, 26 Jan 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/blog/ddd-banking-core/</link><pubDate>Mon, 12 Jan 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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://88ok.github.io/blog/welcome/</link><pubDate>Sat, 15 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/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><item><title>在银行客户信息系统（ECIF）里落地 DDD 聚合</title><link>https://88ok.github.io/columns/architecture/ddd-aggregate/</link><pubDate>Wed, 12 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/architecture/ddd-aggregate/</guid><description>&lt;p&gt;ECIF 里「客户」的概念极其庞大：个人、对公、同业，各自的属性、关系、生命周期都不同。如果用一个巨大的 &lt;code&gt;Customer&lt;/code&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;客户主数据（Party）&lt;/strong&gt;：统一的自然人/机构标识与基础属性。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;客户画像（Profile）&lt;/strong&gt;：风险偏好、营销标签，读写频率高、变化快。&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;客户关系（Relationship）&lt;/strong&gt;：持股、担保、集团关系。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="聚合根怎么定"&gt;聚合根怎么定&#10;&lt;/h2&gt;&#10;&lt;p&gt;每个上下文内部再定聚合根。例如 Party 上下文里，&lt;code&gt;Party&lt;/code&gt; 是聚合根，&lt;code&gt;Address&lt;/code&gt;、&lt;code&gt;Contact&lt;/code&gt; 是其值对象，保证一致性边界内不跨聚合调用。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;经验：聚合的边界应以「事务一致性」而非「业务概念大小」来划。ECIF 里最容易犯的错，就是把所有客户信息塞进一个聚合。&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;这样设计后，主数据服务稳定，画像服务可以独立迭代，互不影响。&lt;/p&gt;</description></item><item><title>一文理清 ECIF：客户信息为什么要「集中」</title><link>https://88ok.github.io/columns/banking/ecif-overview/</link><pubDate>Mon, 10 Aug 2026 00:00:00 +0000</pubDate><guid>https://88ok.github.io/columns/banking/ecif-overview/</guid><description>&lt;p&gt;在没有 ECIF 的年代，网点、网银、信用卡中心各自维护一份客户信息。同一个客户，在不同系统里姓名、证件、联系方式都不一样，营销和风控都无从谈起。&lt;/p&gt;&#10;&lt;h2 id="ecif-解决什么"&gt;ECIF 解决什么&#10;&lt;/h2&gt;&#10;&lt;p&gt;ECIF（企业客户信息整合）把分散在各业务系统的客户主数据收敛到一处，对外提供&lt;strong&gt;唯一客户视图&lt;/strong&gt;。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;唯一标识&lt;/strong&gt;：用客户号（Party ID）统一自然人/机构，而非证件号。&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;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;/ul&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;ECIF 不是「又一个数据库」，而是银行数字化的最底层地基。&lt;/p&gt;&#10;&lt;/blockquote&gt;</description></item><item><title>关于</title><link>https://88ok.github.io/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://88ok.github.io/about/</guid><description>&lt;p&gt;##（页面为 AI 生成，仅供参考）&lt;/p&gt;&#10;&lt;h2 id="简介"&gt;简介&#10;&lt;/h2&gt;&#10;&lt;p&gt;王鹏，数学学士 + 软件工程硕士（跨专业），从事核心系统后端研发。主要关注&lt;strong&gt;分布式架构、单元化部署、银行核心业务建模&lt;/strong&gt;，以及 AI 在银行业务中的工程化落地。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;证书：PMP&lt;/li&gt;&#10;&lt;li&gt;技术主线：Java 后端、分布式系统、DDD / CQRS / Saga、企业架构与数据资产&lt;/li&gt;&#10;&lt;li&gt;当前重点：对客 AI Agent 平台建设；企业级 Java AI 框架选型（Spring AI 2.0 / LangChain4j / SAA / Embabel）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="经历"&gt;经历&#10;&lt;/h2&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;/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;/tr&gt;&#10; &lt;tr&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;/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;&lt;code&gt;Java&lt;/code&gt; · &lt;code&gt;Spring Cloud&lt;/code&gt; · 分布式事务 · 单元化 · &lt;code&gt;DDD&lt;/code&gt; · 数据库（&lt;code&gt;MySQL&lt;/code&gt; / 分库分表）· 消息与流（&lt;code&gt;Kafka&lt;/code&gt;）· 缓存（&lt;code&gt;Redis&lt;/code&gt;）· 协调服务（&lt;code&gt;ZooKeeper&lt;/code&gt;）· 微服务治理 · 企业架构&lt;/p&gt;&#10;&lt;h2 id="项目与研究"&gt;项目与研究&#10;&lt;/h2&gt;&#10;&lt;p&gt;详见 &lt;a href="https://88ok.github.io/projects/"&gt;项目&lt;/a&gt; 与 &lt;a href="https://88ok.github.io/tech/"&gt;技术笔记&lt;/a&gt;。近期方向包括：投资顾问系统（AKShare + Supabase + All-Weather）、认知匹配 / AI 认知画像、PageFly 轻量页面托管、中文 AI 人味化、银行业本体论，并计划重启「银行科技 + AI」主题公众号。&lt;/p&gt;</description></item></channel></rss>