<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>IM on 我的技术博客</title><link>https://buvidk1234.github.io/tags/im/</link><description>Recent content in IM on 我的技术博客</description><generator>Hugo -- 0.153.0</generator><language>zh-cn</language><lastBuildDate>Sun, 22 Mar 2026 20:02:15 +0800</lastBuildDate><atom:link href="https://buvidk1234.github.io/tags/im/index.xml" rel="self" type="application/rss+xml"/><item><title>复制滞后与多端同步</title><link>https://buvidk1234.github.io/posts/replication-lag/</link><pubDate>Sun, 22 Mar 2026 20:02:15 +0800</pubDate><guid>https://buvidk1234.github.io/posts/replication-lag/</guid><description>&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;主从复制可以显著提升系统的读能力，但复制滞后会直接破坏多端同步体验。
本文聚焦三个最常见的一致性问题：读自己写、单调读、前缀一致读，以及在即时通讯场景中的对应治理手段。&lt;/p&gt;
&lt;h2 id="读自己的写"&gt;读自己的写&lt;/h2&gt;
&lt;p&gt;读自己写失效：客户端提交了写入，但随后的读取请求被路由到了尚未完成同步的从节点，导致客户端读不到自己刚刚写入的数据。&lt;/p&gt;
&lt;p&gt;&lt;img alt="read_after_write.png" loading="lazy" src="https://buvidk1234.github.io/images/read_after_write.png"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户读自己写数据，强制走主节点。如果大部分数据都修改，会给主库造成巨大压力&lt;/li&gt;
&lt;li&gt;用户记录最近更新时间戳，可以是逻辑时间和系统时钟（时钟不可靠）。多设备不适用，并不知道记录的时间戳。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="单调读"&gt;单调读&lt;/h2&gt;
&lt;p&gt;单调读失效：用户的多次读取请求打到了同步进度不一致的多个副本上，导致用户先看到了较新的数据，随后又看到了较旧的数据，出现了“时光倒流”。&lt;/p&gt;
&lt;p&gt;&lt;img alt="monotonic_read.png" loading="lazy" src="https://buvidk1234.github.io/images/monotonic_read.png"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;始终从同一副本读取，例如基于用户id哈希。但是如果副本失效，必须重新路由到另一个副本&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="前缀一致读"&gt;前缀一致读&lt;/h2&gt;
&lt;p&gt;前缀/因果一致性失效：具有因果关系（先后顺序）的写入，由于底层分布式组件的处理速率不一致，导致在第三方的视角中，事件发生的顺序被颠倒。&lt;/p&gt;
&lt;p&gt;&lt;img alt="consistent_prefix_reads.png" loading="lazy" src="https://buvidk1234.github.io/images/consistent_prefix_reads.png"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将具有因果顺序的都交由一个分区处理。效率低&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="即时通讯场景下的主从滞后问题"&gt;即时通讯场景下的主从滞后问题&lt;/h2&gt;
&lt;h3 id="读自己写失效read-your-writes-anomaly"&gt;读自己写失效（Read-Your-Writes Anomaly）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多端同步丢失：&lt;/strong&gt; 用户在手机端发了一条消息，成功写入服务端主库。用户立刻打开电脑端（PC 版）查看，PC 端的拉取请求恰好打到了一个由于网络抖动而延迟了 500 毫秒的 MySQL 从库。结果 PC 端界面上一片空白，用户以为消息没发出去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案：纯内存 Push 模型跑赢物理复制&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;放弃让其他在线端去“查”数据库的传统做法。&lt;/li&gt;
&lt;li&gt;手机端消息先进入 Kafka，由消息处理服务按会话维度分配 &lt;code&gt;Seq&lt;/code&gt; 并完成落库。&lt;/li&gt;
&lt;li&gt;后端的 &lt;code&gt;msg_transfer&lt;/code&gt; 服务消费 Kafka，定位到 PC 端的 WebSocket 长连接，直接将消息从内存推（Push）过去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心逻辑：&lt;/strong&gt; 内存与网络的流转速度远快于磁盘 IO 和数据库 Binlog 复制。客户端直接利用 Push 过来的数据渲染上屏，从物理架构上彻底绕开了从库延迟的陷阱。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="单调读失效monotonic-reads-anomaly"&gt;单调读失效（Monotonic Reads Anomaly）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;漫游消息凭空消失：&lt;/strong&gt; 用户断网重连，触发历史消息拉取（Pull）。第一次拉取，网关路由到了延迟极低的从库 A，用户看到了最新的 10 条消息。用户立刻下拉刷新，第二次请求被路由到了卡顿的从库 B，从库 B 还没这 10 条消息。于是，用户屏幕上刚刚还在的 10 条最新聊天记录突然集体消失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案：客户端主导的严格 Seq 游标&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在 OpenIM 中，拉取漫游消息的“游标控制权”在客户端手里，而不是服务端盲查。&lt;/li&gt;
&lt;li&gt;客户端本地 SQLite 记录着自己当前看到的最后一条连续消息的 &lt;code&gt;MaxLocalSeq&lt;/code&gt;（如 Seq=100）。&lt;/li&gt;
&lt;li&gt;发起 Pull 请求时，客户端携带极其明确的条件：“只拉取 &lt;code&gt;Seq &amp;gt; 100&lt;/code&gt; 的增量消息”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心逻辑：&lt;/strong&gt; 服务端接收到游标后，会和 Redis 中维护的 &lt;code&gt;ServerMaxSeq&lt;/code&gt; 对比。如果查到的从库最新数据只有 Seq=95，服务端立刻判定该副本滞后，可以选择等待、报错或强制回源主库。这保证了客户端拉取的数据永远是向前递增的，彻底封杀时光倒流。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="前缀因果一致性失效causal-consistency-anomaly"&gt;前缀/因果一致性失效（Causal Consistency Anomaly）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;旁观者视角的逻辑错乱：&lt;/strong&gt; 在百人群聊中，用户 A 问：“去不去吃饭？”，用户 B 看到后秒回：“去”。（A 绝对发生在 B 之前）。然而，A 和 B 的消息并发打入服务端，处理 B 的线程极快，处理 A 的线程卡顿，导致 B 的消息先同步到了部分从库。此时，旁观者 C 刷新群聊，竟然先看到了 B 说“去”，过了几秒才看到 A 问“去不去吃饭”。因果逻辑彻底崩塌。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解决方案：Kafka 分区串行化 + 会话内 Seq 发号&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;步骤 1（先入队）：&lt;/strong&gt; 消息以 &lt;code&gt;ConversationID&lt;/code&gt; 为 Hash Key 投递到 Kafka，确保同一会话的消息进入同一个 Partition。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;步骤 2（再发号）：&lt;/strong&gt; 消息处理服务按 Partition 顺序消费，并为该会话分配严格递增的 &lt;code&gt;Seq&lt;/code&gt;，然后落库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;步骤 3（客户端连续性校验）：&lt;/strong&gt; 就算网络抖动导致 Seq=11 的消息先推给旁观者 C，客户端发现本地缺少 Seq=10 时，会先放入重排缓冲区，待 Pull 补齐后再按序展示。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心逻辑：&lt;/strong&gt; 先利用 Kafka 分区保证同会话处理顺序，再用会话内递增 Seq 做最终顺序锚点，最后由客户端状态机兜底连续性。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分布式架构设计：不与底层的物理不确定性（网络抖动、磁盘延迟）死磕，而是通过高层的应用逻辑（Seq、Push/Pull、分区 Hash）来建立确定的秩序&lt;/p&gt;</description></item><item><title>LSM-Tree 原理与消息存储选型</title><link>https://buvidk1234.github.io/posts/lsm-tree/</link><pubDate>Sun, 22 Mar 2026 14:14:08 +0800</pubDate><guid>https://buvidk1234.github.io/posts/lsm-tree/</guid><description>&lt;h2 id="1-概述"&gt;1. 概述&lt;/h2&gt;
&lt;p&gt;LSM-Tree（Log-Structured Merge-Tree）是一种典型的&lt;strong&gt;写优化&lt;/strong&gt;存储结构。它的核心思路是：不追求每次写入都直接落到最终有序位置，而是将写入拆成**&amp;ldquo;前台快速落地 + 后台异步整理&amp;rdquo;**两个阶段。&lt;/p&gt;
&lt;p&gt;设计目标可以归纳为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顺序化写入&lt;/strong&gt;：前台写入以追加为主，避免随机写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后台整理&lt;/strong&gt;：异步 Compaction 控制查询路径复杂度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可调节的权衡&lt;/strong&gt;：在写吞吐、读延迟和空间占用之间提供调优空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="2-整体架构与核心组件"&gt;2. 整体架构与核心组件&lt;/h2&gt;
&lt;p&gt;LSM-Tree 的运行依赖以下组件协同工作：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;位置&lt;/th&gt;
&lt;th&gt;职责&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WAL（Write-Ahead Log）&lt;/td&gt;
&lt;td&gt;磁盘&lt;/td&gt;
&lt;td&gt;写前日志，保证崩溃恢复&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MemTable&lt;/td&gt;
&lt;td&gt;内存&lt;/td&gt;
&lt;td&gt;有序表，接收实时写入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immutable MemTable&lt;/td&gt;
&lt;td&gt;内存&lt;/td&gt;
&lt;td&gt;写满后冻结，等待刷盘&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSTable&lt;/td&gt;
&lt;td&gt;磁盘&lt;/td&gt;
&lt;td&gt;有序、不可变的持久化文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction&lt;/td&gt;
&lt;td&gt;后台任务&lt;/td&gt;
&lt;td&gt;合并 SSTable，版本收敛与空间回收&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;数据在组件间的流转路径如下：&lt;/p&gt;
&lt;div class="mermaid"&gt;flowchart LR
W[Write Request] --&gt; L[Append WAL]
L --&gt; M[Insert MemTable]
M --&gt;|Threshold reached| IM[Immutable MemTable]
IM --&gt; F[Flush to SSTable]
F --&gt; C[Compaction]
C --&gt; S[(Merged SSTables)]&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="3-写路径"&gt;3. 写路径&lt;/h2&gt;
&lt;p&gt;写入的关键原则是 &lt;strong&gt;&amp;ldquo;先确认可恢复，再确认可查询&amp;rdquo;&lt;/strong&gt;。具体步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;追加 WAL&lt;/strong&gt;——保证持久化，崩溃后可重放。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新 MemTable&lt;/strong&gt;——对外可查。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冻结 MemTable&lt;/strong&gt;——达到阈值后转为 Immutable。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flush 为 SSTable&lt;/strong&gt;——后台将 Immutable MemTable 写入磁盘。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这条路径带来的关键特性：&lt;/p&gt;</description></item><item><title>分布式 IM 网关路由架构：从集中式 Redis 到连接即路由</title><link>https://buvidk1234.github.io/posts/distributed-im-gateway-routing-architecture/</link><pubDate>Fri, 13 Mar 2026 16:05:30 +0800</pubDate><guid>https://buvidk1234.github.io/posts/distributed-im-gateway-routing-architecture/</guid><description>&lt;h1 id="分布式-im-网关路由架构分析与设计"&gt;分布式 IM 网关路由架构分析与设计&lt;/h1&gt;
&lt;p&gt;单机 IM 网关阶段，用户与 WebSocket 连接关系维护在本地内存即可，例如使用 &lt;code&gt;sync.Map&lt;/code&gt; 或 &lt;code&gt;map[int64]*websocket.Conn + RWMutex&lt;/code&gt;。该模式实现简单、链路短、时延低。&lt;/p&gt;
&lt;p&gt;当在线规模持续增长后，单机的 TCP 连接数、CPU 核数与网络带宽会触达上限，系统必须演进为多实例网关集群。此时核心问题变为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用户分散在不同网关实例后，A 给 B 发消息时，如何在毫秒级准确定位 B 所在节点？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本文围绕该问题展开，重点覆盖接入层方案、路由演进路径、集中式 Redis 架构在高规模下的瓶颈，以及“连接即路由”的去中心化方向。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一接入层客户端先连到谁"&gt;一、接入层：客户端先连到谁&lt;/h2&gt;
&lt;p&gt;在内部路由之前，需要先确定客户端第一跳接入方式。主流做法通常分为两类。&lt;/p&gt;
&lt;h3 id="1-前置负载均衡接入nginxlvsslb"&gt;1. 前置负载均衡接入（Nginx/LVS/SLB）&lt;/h3&gt;
&lt;p&gt;在网关集群前加反向代理，客户端连接统一入口，再由代理层分发到后端网关。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;特点：接入结构直观，客户端侧配置简单。&lt;/li&gt;
&lt;li&gt;问题：代理层同样承担长连接状态，在高并发七层代理场景下会带来明显内存与成本压力。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-dispatch-服务引流服务发现--直连"&gt;2. Dispatch 服务引流（服务发现 + 直连）&lt;/h3&gt;
&lt;p&gt;客户端先请求无状态的 HTTP Dispatch 接口。Dispatch 根据网关健康度、负载指标或一致性 Hash 返回目标地址，例如 &lt;code&gt;192.168.1.100:8080&lt;/code&gt;，然后客户端直接连目标网关。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优势：去掉代理层代持长连接成本，网络拓扑更扁平。&lt;/li&gt;
&lt;li&gt;代价：对调度策略、健康探测和故障转移能力要求更高。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="二路由策略a-如何找到-b"&gt;二、路由策略：A 如何找到 B&lt;/h2&gt;
&lt;p&gt;接入路径确定后，核心问题回到路由：A 发给 B，A 所在网关如何将消息准确投递到 B 所在网关。&lt;/p&gt;
&lt;h3 id="演进一全局广播broadcast"&gt;演进一：全局广播（Broadcast）&lt;/h3&gt;
&lt;p&gt;A 所在网关把消息广播给所有网关实例。每个实例检查本地连接表，命中就投递，未命中就丢弃。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：实现成本低，网关可保持近似无状态。&lt;/li&gt;
&lt;li&gt;缺点：单聊被放大为 $N$ 份网络与计算开销（$N$ 为网关节点数），规模增大后资源浪费显著。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="演进二redis-集中式路由centralized-routing"&gt;演进二：Redis 集中式路由（Centralized Routing）&lt;/h3&gt;
&lt;p&gt;为避免广播风暴，可将 Redis 作为全局路由目录：用户登录时写入“用户-网关”绑定，发消息前查询 Redis 后再定向投递。&lt;/p&gt;</description></item><item><title>Id Seq Generation</title><link>https://buvidk1234.github.io/posts/id-seq-generation/</link><pubDate>Thu, 05 Mar 2026 15:34:07 +0800</pubDate><guid>https://buvidk1234.github.io/posts/id-seq-generation/</guid><description>&lt;h1 id="分布式-id--序列号生成方案技术选型"&gt;分布式 ID / 序列号生成方案技术选型&lt;/h1&gt;
&lt;h2 id="1-方案概述"&gt;1. 方案概述&lt;/h2&gt;
&lt;h3 id="11-uuid"&gt;1.1 UUID&lt;/h3&gt;
&lt;p&gt;128 位随机数（V4）或基于时间+MAC（V1）。无需中心节点，生成即唯一。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;完全无序、不连续，无法用于排序或范围查询&lt;/li&gt;
&lt;li&gt;36 字节字符串存储开销大，随机值导致 B+Tree 页分裂，写入性能差&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="12-数据库自增-id"&gt;1.2 数据库自增 ID&lt;/h3&gt;
&lt;p&gt;MySQL &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; 主键，单表内严格递增且连续。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每次发号伴随磁盘写（INSERT/UPDATE），单机上限约 1000-3000 TPS&lt;/li&gt;
&lt;li&gt;强依赖单点数据库，宕机即停；分库后各库独立递增，全局不连续&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="13-snowflake雪花算法"&gt;1.3 Snowflake（雪花算法）&lt;/h3&gt;
&lt;p&gt;64 位 = 1 bit 符号 + 41 bit 时间戳 + 10 bit 机器 ID + 12 bit 序列号。纯内存计算，单机 400 万+/秒。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;趋势递增但不连续，两个 ID 的差值无业务含义&lt;/li&gt;
&lt;li&gt;依赖机器时钟，时钟回拨可能导致 ID 重复&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="14-leaf-segment美团-leaf-号段模式"&gt;1.4 Leaf-Segment（美团 Leaf 号段模式）&lt;/h3&gt;
&lt;p&gt;从数据库批量预取号段（如 1000 个），应用内存中顺序分发。当前号段消耗到阈值时异步预加载下一号段，避免切换时阻塞。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;号段内连续，但进程重启时未消费的号段被浪费，产生空洞&lt;/li&gt;
&lt;li&gt;多实例部署时各进程持有不同号段，同一业务维度内 ID 交叉，无法保证连续&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="15-leaf-snowflake"&gt;1.5 Leaf-Snowflake&lt;/h3&gt;
&lt;p&gt;Snowflake 变体，用 ZooKeeper 管理 workerID 并解决时钟回拨。本质仍是 Snowflake，不连续的缺陷不变。&lt;/p&gt;</description></item><item><title>Mysql Slow Query Filesort Optimization</title><link>https://buvidk1234.github.io/posts/mysql-slow-query-filesort-optimization/</link><pubDate>Sun, 28 Dec 2025 22:27:08 +0800</pubDate><guid>https://buvidk1234.github.io/posts/mysql-slow-query-filesort-optimization/</guid><description>&lt;h1 id="im-消息拉取接口的-sql-优化记录"&gt;IM 消息拉取接口的 SQL 优化记录&lt;/h1&gt;
&lt;h3 id="1-背景与问题"&gt;1. 背景与问题&lt;/h3&gt;
&lt;p&gt;在开发 IM 系统的“拉取历史消息”功能时，我发现当单张消息表（&lt;code&gt;messages&lt;/code&gt;）的数据量达到百万级，且某个热点会话（&lt;code&gt;conversation_id&lt;/code&gt;）拥有超过 10 万条消息时，接口响应出现明显卡顿。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;环境信息：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据库：MySQL 8.0+ (InnoDB)&lt;/li&gt;
&lt;li&gt;数据量：单表 100 万行，大会话 10 万条信息&lt;/li&gt;
&lt;li&gt;场景：用户查看最新的 20 条消息（Top-N 查询）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;慢查询 SQL：&lt;/strong&gt;&lt;/p&gt;
&lt;details class="code-fold"&gt;
&lt;summary&gt;SQL&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;SELECT&lt;/span&gt;&lt;span style="color:#fff"&gt; &lt;/span&gt;&lt;span style="color:#0550ae"&gt;*&lt;/span&gt;&lt;span style="color:#fff"&gt; &lt;/span&gt;&lt;span style="color:#cf222e"&gt;FROM&lt;/span&gt;&lt;span style="color:#fff"&gt; &lt;/span&gt;messages&lt;span style="color:#fff"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;WHERE&lt;/span&gt;&lt;span style="color:#fff"&gt; &lt;/span&gt;conversation_id&lt;span style="color:#fff"&gt; &lt;/span&gt;&lt;span style="color:#0550ae"&gt;=&lt;/span&gt;&lt;span style="color:#fff"&gt; &lt;/span&gt;&lt;span style="color:#0a3069"&gt;&amp;#39;chat_hot&amp;#39;&lt;/span&gt;&lt;span style="color:#fff"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;ORDER&lt;/span&gt;&lt;span style="color:#fff"&gt; &lt;/span&gt;&lt;span style="color:#cf222e"&gt;BY&lt;/span&gt;&lt;span style="color:#fff"&gt; &lt;/span&gt;seq&lt;span style="color:#fff"&gt; &lt;/span&gt;&lt;span style="color:#cf222e"&gt;DESC&lt;/span&gt;&lt;span style="color:#fff"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#cf222e"&gt;LIMIT&lt;/span&gt;&lt;span style="color:#fff"&gt; &lt;/span&gt;&lt;span style="color:#0550ae"&gt;20&lt;/span&gt;&lt;span style="color:#1f2328"&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/details&gt;
&lt;h3 id="2-排查过程"&gt;2. 排查过程&lt;/h3&gt;
&lt;h4 id="21-慢日志抓取"&gt;2.1 慢日志抓取&lt;/h4&gt;
&lt;p&gt;开启 MySQL 慢查询日志（&lt;code&gt;log_output=FILE&lt;/code&gt;, &lt;code&gt;long_query_time=0.1&lt;/code&gt;），捕获到该语句的执行情况：&lt;/p&gt;
&lt;details class="code-fold"&gt;
&lt;summary&gt;TEXT&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Query_time: 0.148528 Lock_time: 0.000003 Rows_sent: 20 Rows_examined: 151676&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/details&gt;
&lt;p&gt;&lt;strong&gt;异常点&lt;/strong&gt;：为了返回 20 条数据，MySQL 实际扫描了 15 万行数据。扫描/返回比极低，说明索引效率存在严重问题。&lt;/p&gt;
&lt;h4 id="22-执行计划分析-explain"&gt;2.2 执行计划分析 (EXPLAIN)&lt;/h4&gt;
&lt;p&gt;执行 &lt;code&gt;EXPLAIN&lt;/code&gt; 查看当前索引使用情况：
&lt;img alt="sql_optimization_before_explain" loading="lazy" src="https://buvidk1234.github.io/images/sql_optimization_before_explain.png"&gt;&lt;/p&gt;</description></item><item><title>Rockscache Consistency</title><link>https://buvidk1234.github.io/posts/rockscache-consistency/</link><pubDate>Tue, 23 Dec 2025 21:47:09 +0800</pubDate><guid>https://buvidk1234.github.io/posts/rockscache-consistency/</guid><description>&lt;h1 id="深入解析-rockscache如何优雅地解决缓存与数据库一致性问题"&gt;深入解析 RocksCache：如何优雅地解决缓存与数据库一致性问题&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本文深入剖析 RocksCache 的设计思想与核心实现，带你理解这个首创的缓存一致性解决方案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;在分布式系统中，缓存是提升性能的利器，但也是一致性问题的重灾区。你是否曾经遇到过这样的困扰：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;明明更新了数据库，为什么缓存里还是旧数据？&lt;/li&gt;
&lt;li&gt;用了「先更新DB，再删缓存」策略，为什么还是会出现不一致？&lt;/li&gt;
&lt;li&gt;如何在保证一致性的同时，还能保持高性能？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;今天介绍的 &lt;strong&gt;RocksCache&lt;/strong&gt;，是一个来自 DTM Labs 的开源项目，它通过一套精巧的设计，&lt;strong&gt;在不引入版本号的前提下&lt;/strong&gt;，优雅地解决了缓存与数据库的一致性难题。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一经典的缓存一致性问题"&gt;一、经典的缓存一致性问题&lt;/h2&gt;
&lt;h3 id="11-常见的缓存策略"&gt;1.1 常见的缓存策略&lt;/h3&gt;
&lt;p&gt;最常用的缓存管理策略是 &lt;strong&gt;Cache-Aside&lt;/strong&gt;（旁路缓存）：&lt;/p&gt;
&lt;details class="code-fold"&gt;
&lt;summary&gt;TEXT&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;读取：先查缓存 → 缓存命中则返回 → 未命中则查DB → 写入缓存 → 返回
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;更新：更新DB → 删除缓存&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/details&gt;
&lt;p&gt;这个策略看似简单，却隐藏着一个致命的并发问题。&lt;/p&gt;
&lt;h3 id="12-并发场景下的数据不一致"&gt;1.2 并发场景下的数据不一致&lt;/h3&gt;
&lt;p&gt;考虑以下时序：&lt;/p&gt;
&lt;details class="code-fold"&gt;
&lt;summary&gt;TEXT&lt;/summary&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;时间 →→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;线程A（读请求）:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 查DB(v1) ─────────────────────────────→ 写缓存(v1)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (网络延迟)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;线程B（写请求）:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 更新DB(v2) → 删除缓存&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/details&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：线程A 查询到 v1 后，发生了网络延迟。此时线程B 完成了更新并删除缓存。但线程A 的写缓存操作在删除之后执行，导致缓存中存储了旧值 v1。&lt;/p&gt;
&lt;p&gt;这就是著名的 &lt;strong&gt;&amp;ldquo;删除后写入&amp;rdquo;&lt;/strong&gt; 问题，常规的「先更新DB再删缓存」策略无法解决。&lt;/p&gt;
&lt;h3 id="13-传统解决方案的局限"&gt;1.3 传统解决方案的局限&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;延迟双删&lt;/td&gt;
&lt;td&gt;删除缓存后，延迟一段时间再删一次&lt;/td&gt;
&lt;td&gt;延迟时间难以确定，仍有不一致窗口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;版本号&lt;/td&gt;
&lt;td&gt;每条数据带版本号，写入时比较版本&lt;/td&gt;
&lt;td&gt;侵入业务，改造成本高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;分布式锁&lt;/td&gt;
&lt;td&gt;读写都加锁&lt;/td&gt;
&lt;td&gt;性能差，热点数据成为瓶颈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;订阅 binlog&lt;/td&gt;
&lt;td&gt;通过 Canal 等订阅 DB 变更&lt;/td&gt;
&lt;td&gt;架构复杂，延迟较高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;有没有一种方案，既能保证一致性，又不侵入业务，还能保证高性能？&lt;/p&gt;</description></item></channel></rss>