<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>交易系统 on 追风笔记</title><link>https://88ok.github.io/tags/%E4%BA%A4%E6%98%93%E7%B3%BB%E7%BB%9F/</link><description>Recent content in 交易系统 on 追风笔记</description><generator>Hugo</generator><language>zh</language><lastBuildDate>Sat, 28 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://88ok.github.io/tags/%E4%BA%A4%E6%98%93%E7%B3%BB%E7%BB%9F/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>