Redis Event Loop

Redis 事件循环 这篇不按 API 一个个介绍,而是跟着一个具体请求读源码。先固定场景: TEXT系统:Linux,使用 epoll 连接:普通 TCP,不考虑 TLS 线程:io-threads = 1 监听 socket:fd = 6 客户端 socket:fd = 8 客户端发送:SET name alice 要追的不是一堆互相独立的函数,而是下面两次唤醒: TEXT第一次 epoll_wait 返回 fd=6 可读 -> accept 新连接 -> 得到 fd=8 -> 给 fd=8 注册读回调 第二次 epoll_wait 返回 fd=8 可读 -> 读请求 -> 执行 SET -> 生成 +OK -> 下一轮 beforeSleep 写回客户端 主要源码按这个顺序看: ...

July 11, 2026

Redis Replication

Redis 复制 用这条命令看 Redis 复制的数据流: BASHSET token abc EX 60 它在 master 上执行一次,随后进入复制流。replica 重放的命令会被改写为: TEXTSET token abc PXAT <absolute-millisecond-timestamp> 主线: TEXTREPLICAOF -> connectWithMaster -> syncWithMaster -> PSYNC -> CONTINUE:补 backlog -> FULLRESYNC:加载 RDB 基线 SET token abc EX 60 -> 改写 PXAT -> 写入复制流 -> backlog + replica 输出缓冲 -> replica 以 CLIENT_MASTER 身份重放 以下代码均为源码截取,只保留影响这条路径的判断和赋值。 复制历史 部分重同步依赖 replid + offset + backlog。 ...

July 11, 2026

Redis Command Execution

Redis 命令执行 这篇看一条命令从客户端发到 Redis 后,源码里大概怎么走。例子还是: BASHSET name alice 客户端一般发的是 RESP: TEXT*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$5\r\nalice\r\n 主线可以压成这一条: TEXTreadQueryFromClient -> processInputBuffer -> processMultibulkBuffer / processInlineBuffer -> preprocessCommand -> processCommandAndResetClient -> processCommand -> call -> setCommand -> addReply -> handleClientsWithPendingWrites / writeToClient 主要源码: 环节 源码 读请求、协议解析、回写 src/networking.c 查命令、校验、调度 src/server.c / src/server.h 命令声明 src/commands/*.json / src/commands.def SET 实现 src/t_string.c 写入数据库 src/db.c client:一条连接的执行现场 先看 server.h 里的 client,删掉大量无关字段后是这样: ...

July 11, 2026

编译原理

编译原理:从源代码到可执行程序 高级语言中的变量、函数、结构体、接口、模块等概念更接近抽象语义;机器实际执行的是指令、寄存器操作、内存访问和跳转。编译器的核心任务是将高级语言程序转换为机器可执行的目标形式,并在这个过程中完成语法约束、类型约束、控制流约束、内存布局和目标平台约束的处理。 典型编译流程可以抽象为: TEXT源代码 -> 词法分析:字符流 -> Token 流 -> 语法分析:Token 流 -> AST -> 语义分析:AST -> 带类型、作用域、绑定关系的 AST -> 中间代码生成:AST -> IR -> 代码优化:IR -> 优化后的 IR -> 目标代码生成:IR -> 汇编 / 机器指令 -> 汇编与链接:目标文件 -> 可执行文件 这条流水线的关键不是“把文本翻译成另一段文本”,而是不断把程序转换为更适合下一阶段处理的结构化表示。Token 适合语法分析,AST 适合表达源代码结构,IR 适合数据流和控制流优化,机器指令适合在具体硬件上执行。 编译过程概览 以一段简单的 Go 代码为例: GOpackage main func add(a, b int) int { return a + b } 在编译器内部,它会经历类似下面的结构转换: ...

June 20, 2026

Serialization and Deserialization

Java 序列化与反序列化:原理、Jackson 实战与避坑指南 一、序列化与反序列化基础 1.1 定义 序列化(Serialization):将内存中的对象转换为可存储或可传输的数据格式(字节流、JSON、XML 等)的过程。 反序列化(Deserialization):将存储或传输的数据格式还原为内存对象的逆过程。 TEXT 序列化 Object ──────► byte[] / JSON / XML / Protobuf ... 反序列化 Object ◄────── byte[] / JSON / XML / Protobuf ... 1.2 核心目的 场景 说明 持久化 将对象状态保存到磁盘、数据库 网络传输 RPC、HTTP API、消息队列中传递对象 进程间通信 跨 JVM 数据交换 深拷贝 通过序列化/反序列化实现对象深复制 缓存 Redis、Memcached 等存储 Java 对象 1.3 常见序列化方案对比 方案 格式 可读性 性能 跨语言 典型场景 JDK Serializable 二进制 ✗ 低 ✗ 遗留系统 JSON (Jackson/Gson) 文本 ✓ 中 ✓ REST API、配置 Protobuf 二进制 ✗ 高 ✓ gRPC、高性能 RPC Kryo 二进制 ✗ 高 ✗ Spark、Flink 内部 Hessian 二进制 ✗ 中 ✓ Dubbo Avro 二进制 ✗ 高 ✓ Kafka、大数据 1.4 序列化的本质问题 序列化不仅仅是"转格式",需要解决以下核心问题: ...

April 12, 2026

Regex Guide

Regex 1. 字符类 (Character Classes) 语法 说明 等价于 . 匹配换行符外的任意单字符 [^\n\r] \d 匹配数字 [0-9] \D 匹配非数字 [^0-9] \w 匹配字母、数字或下划线 [A-Za-z0-9_] \W 匹配非单词字符 [^A-Za-z0-9_] \s 匹配空白符 [ \t\n\r\f\v] \S 匹配非空白符 [^ \t\n\r\f\v] [ABC] 匹配括号内任意单字符 - [^ABC] 匹配括号内字符外的任意单字符 - 2. 量词 (Quantifiers) 语法 说明 * 匹配 0 次或多次 + 匹配 1 次或多次 ? 匹配 0 次或 1 次 {n} 匹配确切 n 次 {n,} 匹配至少 n 次 {n,m} 匹配 n 到 m 次 注:默认贪婪匹配(匹配最大长度)。在量词后追加 ?(如 *?、+?)切换为懒惰匹配(匹配最小长度)。 ...

April 6, 2026

微服务稳定性兜底:深入解析 go-kratos/aegis 的核心设计

微服务稳定性兜底:深入解析 go-kratos/aegis 的核心设计 在高并发微服务体系中,单点故障、流量洪峰、缓存热点都可能引发雪崩。aegis 是 Kratos 框架中的稳定性组件库,集成了多种常见的保护机制,以较低开销提供多层防护。本文围绕其核心模块展开,分析背后的算法原理与工程权衡。 一、项目全景 TEXTgo-kratos/aegis ├── ratelimit/bbr # 自适应限流(BBR 算法) ├── circuitbreaker/sre # 熔断器(Google SRE 自适应节流) ├── topk/heavykeeper # Top-K 热点 key 检测(HeavyKeeper 算法) ├── hotkey # 热点 key 自动本地缓存 ├── subset # 一致性哈希子集路由 └── internal ├── window # 滑动窗口(环形数组) ├── consistent # 一致性哈希 ├── minheap # 最小堆 └── cpu # CPU 使用率采样 核心哲学:用概率和统计代替精确计数,以极低的内存和计算开销换取足够准确的系统保护。 二、BBR 自适应限流 2.1 问题背景 传统限流通常是硬编码一个 QPS 上限(如"最多 1000 RPS")。问题在于: ...

April 2, 2026

复制滞后与多端同步

前言 主从复制可以显著提升系统的读能力,但复制滞后会直接破坏多端同步体验。 本文聚焦三个最常见的一致性问题:读自己写、单调读、前缀一致读,以及在即时通讯场景中的对应治理手段。 读自己的写 读自己写失效:客户端提交了写入,但随后的读取请求被路由到了尚未完成同步的从节点,导致客户端读不到自己刚刚写入的数据。 用户读自己写数据,强制走主节点。如果大部分数据都修改,会给主库造成巨大压力 用户记录最近更新时间戳,可以是逻辑时间和系统时钟(时钟不可靠)。多设备不适用,并不知道记录的时间戳。 单调读 单调读失效:用户的多次读取请求打到了同步进度不一致的多个副本上,导致用户先看到了较新的数据,随后又看到了较旧的数据,出现了“时光倒流”。 始终从同一副本读取,例如基于用户id哈希。但是如果副本失效,必须重新路由到另一个副本 前缀一致读 前缀/因果一致性失效:具有因果关系(先后顺序)的写入,由于底层分布式组件的处理速率不一致,导致在第三方的视角中,事件发生的顺序被颠倒。 将具有因果顺序的都交由一个分区处理。效率低 即时通讯场景下的主从滞后问题 读自己写失效(Read-Your-Writes Anomaly) 多端同步丢失: 用户在手机端发了一条消息,成功写入服务端主库。用户立刻打开电脑端(PC 版)查看,PC 端的拉取请求恰好打到了一个由于网络抖动而延迟了 500 毫秒的 MySQL 从库。结果 PC 端界面上一片空白,用户以为消息没发出去。 解决方案:纯内存 Push 模型跑赢物理复制 放弃让其他在线端去“查”数据库的传统做法。 手机端消息先进入 Kafka,由消息处理服务按会话维度分配 Seq 并完成落库。 后端的 msg_transfer 服务消费 Kafka,定位到 PC 端的 WebSocket 长连接,直接将消息从内存推(Push)过去。 核心逻辑: 内存与网络的流转速度远快于磁盘 IO 和数据库 Binlog 复制。客户端直接利用 Push 过来的数据渲染上屏,从物理架构上彻底绕开了从库延迟的陷阱。 单调读失效(Monotonic Reads Anomaly) 漫游消息凭空消失: 用户断网重连,触发历史消息拉取(Pull)。第一次拉取,网关路由到了延迟极低的从库 A,用户看到了最新的 10 条消息。用户立刻下拉刷新,第二次请求被路由到了卡顿的从库 B,从库 B 还没这 10 条消息。于是,用户屏幕上刚刚还在的 10 条最新聊天记录突然集体消失。 解决方案:客户端主导的严格 Seq 游标 在 OpenIM 中,拉取漫游消息的“游标控制权”在客户端手里,而不是服务端盲查。 客户端本地 SQLite 记录着自己当前看到的最后一条连续消息的 MaxLocalSeq(如 Seq=100)。 发起 Pull 请求时,客户端携带极其明确的条件:“只拉取 Seq > 100 的增量消息”。 核心逻辑: 服务端接收到游标后,会和 Redis 中维护的 ServerMaxSeq 对比。如果查到的从库最新数据只有 Seq=95,服务端立刻判定该副本滞后,可以选择等待、报错或强制回源主库。这保证了客户端拉取的数据永远是向前递增的,彻底封杀时光倒流。 前缀/因果一致性失效(Causal Consistency Anomaly) 旁观者视角的逻辑错乱: 在百人群聊中,用户 A 问:“去不去吃饭?”,用户 B 看到后秒回:“去”。(A 绝对发生在 B 之前)。然而,A 和 B 的消息并发打入服务端,处理 B 的线程极快,处理 A 的线程卡顿,导致 B 的消息先同步到了部分从库。此时,旁观者 C 刷新群聊,竟然先看到了 B 说“去”,过了几秒才看到 A 问“去不去吃饭”。因果逻辑彻底崩塌。 解决方案:Kafka 分区串行化 + 会话内 Seq 发号 步骤 1(先入队): 消息以 ConversationID 为 Hash Key 投递到 Kafka,确保同一会话的消息进入同一个 Partition。 步骤 2(再发号): 消息处理服务按 Partition 顺序消费,并为该会话分配严格递增的 Seq,然后落库。 步骤 3(客户端连续性校验): 就算网络抖动导致 Seq=11 的消息先推给旁观者 C,客户端发现本地缺少 Seq=10 时,会先放入重排缓冲区,待 Pull 补齐后再按序展示。 核心逻辑: 先利用 Kafka 分区保证同会话处理顺序,再用会话内递增 Seq 做最终顺序锚点,最后由客户端状态机兜底连续性。 分布式架构设计:不与底层的物理不确定性(网络抖动、磁盘延迟)死磕,而是通过高层的应用逻辑(Seq、Push/Pull、分区 Hash)来建立确定的秩序 ...

March 22, 2026

LSM-Tree 原理与消息存储选型

1. 概述 LSM-Tree(Log-Structured Merge-Tree)是一种典型的写优化存储结构。它的核心思路是:不追求每次写入都直接落到最终有序位置,而是将写入拆成**“前台快速落地 + 后台异步整理”**两个阶段。 设计目标可以归纳为: 顺序化写入:前台写入以追加为主,避免随机写。 后台整理:异步 Compaction 控制查询路径复杂度。 可调节的权衡:在写吞吐、读延迟和空间占用之间提供调优空间。 2. 整体架构与核心组件 LSM-Tree 的运行依赖以下组件协同工作: 组件 位置 职责 WAL(Write-Ahead Log) 磁盘 写前日志,保证崩溃恢复 MemTable 内存 有序表,接收实时写入 Immutable MemTable 内存 写满后冻结,等待刷盘 SSTable 磁盘 有序、不可变的持久化文件 Compaction 后台任务 合并 SSTable,版本收敛与空间回收 数据在组件间的流转路径如下: flowchart LR W[Write Request] --> L[Append WAL] L --> M[Insert MemTable] M -->|Threshold reached| IM[Immutable MemTable] IM --> F[Flush to SSTable] F --> C[Compaction] C --> S[(Merged SSTables)] 3. 写路径 写入的关键原则是 “先确认可恢复,再确认可查询”。具体步骤: 追加 WAL——保证持久化,崩溃后可重放。 更新 MemTable——对外可查。 冻结 MemTable——达到阈值后转为 Immutable。 Flush 为 SSTable——后台将 Immutable MemTable 写入磁盘。 这条路径带来的关键特性: ...

March 22, 2026

分布式 IM 网关路由架构:从集中式 Redis 到连接即路由

分布式 IM 网关路由架构分析与设计 单机 IM 网关阶段,用户与 WebSocket 连接关系维护在本地内存即可,例如使用 sync.Map 或 map[int64]*websocket.Conn + RWMutex。该模式实现简单、链路短、时延低。 当在线规模持续增长后,单机的 TCP 连接数、CPU 核数与网络带宽会触达上限,系统必须演进为多实例网关集群。此时核心问题变为: 用户分散在不同网关实例后,A 给 B 发消息时,如何在毫秒级准确定位 B 所在节点? 本文围绕该问题展开,重点覆盖接入层方案、路由演进路径、集中式 Redis 架构在高规模下的瓶颈,以及“连接即路由”的去中心化方向。 一、接入层:客户端先连到谁 在内部路由之前,需要先确定客户端第一跳接入方式。主流做法通常分为两类。 1. 前置负载均衡接入(Nginx/LVS/SLB) 在网关集群前加反向代理,客户端连接统一入口,再由代理层分发到后端网关。 特点:接入结构直观,客户端侧配置简单。 问题:代理层同样承担长连接状态,在高并发七层代理场景下会带来明显内存与成本压力。 2. Dispatch 服务引流(服务发现 + 直连) 客户端先请求无状态的 HTTP Dispatch 接口。Dispatch 根据网关健康度、负载指标或一致性 Hash 返回目标地址,例如 192.168.1.100:8080,然后客户端直接连目标网关。 优势:去掉代理层代持长连接成本,网络拓扑更扁平。 代价:对调度策略、健康探测和故障转移能力要求更高。 二、路由策略:A 如何找到 B 接入路径确定后,核心问题回到路由:A 发给 B,A 所在网关如何将消息准确投递到 B 所在网关。 演进一:全局广播(Broadcast) A 所在网关把消息广播给所有网关实例。每个实例检查本地连接表,命中就投递,未命中就丢弃。 优点:实现成本低,网关可保持近似无状态。 缺点:单聊被放大为 $N$ 份网络与计算开销($N$ 为网关节点数),规模增大后资源浪费显著。 演进二:Redis 集中式路由(Centralized Routing) 为避免广播风暴,可将 Redis 作为全局路由目录:用户登录时写入“用户-网关”绑定,发消息前查询 Redis 后再定向投递。 ...

March 13, 2026