我基于 TCP 写了个通信协议,客户端连发两条消息,服务端一次读出来却粘成了一坨,有时一条消息又被拆成两次才收全,我对着 TCP 是字节流没有消息边界这个粘包拆包的坑排查大半天的复盘

一个让我对 TCP 到底传的是什么彻底搞明白的网络坑,它让我意识到我一直以为的我 send 一条消息对方就 recv 到一条消息这个朴素直觉在 TCP 根本不成立,发和收之间根本没有消息这个概念。自己基于 TCP socket 写通信协议,客户端 conn.Write(hello) 再 Write(world),服务端一次 Read 却读到了 helloworld 两条粘在一起(粘包),有时一条消息又被拆成 hel 和 loworld 两次才读全(拆包)。深究 TCP 本质才明白:TCP 是面向字节流的协议,把数据看成一串连续没有边界的字节(像水管里的水),保证字节按顺序不重不丢到达,但完全不保证一次 send 的数据=一次 recv 读到的数据,在 TCP 看来根本没有消息/包的概念只有字节流;你 Write 几次每次多少字节和对方 Read 几次每次读多少毫无对应关系;消息边界是应用层的概念 TCP 不关心也不维护,TCP 只负责把字节可靠有序搬过去,哪到哪是一条消息全要应用层自己解决。我把应用层的消息和 TCP 的字节流混为一谈了。这篇从故障现场、TCP 字节流粘包拆包真相、正解(长度前缀法先发长度再发内容 io.ReadFull 读满最常用可靠、分隔符法、定长法、优先用 HTTP/gRPC/WebSocket 成熟协议别造轮子)、TCP 其他认知坑(Read 返回 n 不一定够要 ReadFull、Write 也可能没写完、UDP 有边界但不可靠)、分帧方法对照表、TCP 为何设计成字节流的分层与单一职责智慧、决策图与铁律,到附上一个完整的长度前缀分帧编解码实现(含对不可信 length 字段做最大值校验防超大内存)。核心领悟:使用任何组件最关键是搞清它到底保证了什么又没保证什么(TCP 保证可靠有序字节不保证消息边界),别在它未承诺的能力上建立依赖;给数据流断句识别有结构单元是跨领域同构问题(分帧/词法分析/解析)解法共通;分层架构威力来自每层职责清晰克制要在正确的层解决正确的问题;处理网络数据时连描述数据的元信息(长度)也不可信要校验防资源耗尽。

我基于 TCP 写了个通信协议,客户端连发两条消息,服务端一次读出来却粘成了一坨,有时一条消息又被拆成两次才收全,我对着 TCP 是字节流没有消息边界这个粘包拆包的坑排查了大半天的复盘

这是一个让我对 TCP "到底传的是什么"彻底搞明白的网络坑。它让我意识到:我一直以为的"我 send 一条消息、对方就 recv 到一条消息"这个朴素直觉,在 TCP 这里根本不成立——发和收之间,根本没有"消息"这个概念。

需求是自己基于 TCP socket 写一个简单的通信协议:客户端发消息,服务端收消息处理。我天真地以为"一次 send 对应一次 recv",代码写得理所当然:

// 客户端: 连续发两条消息
conn.Write([]byte("hello"))     // 发第一条
conn.Write([]byte("world"))     // 发第二条

// 服务端: 读消息(有问题的版本)
buf := make([]byte, 1024)
n, _ := conn.Read(buf)          // 我以为: 一次Read读到一条消息
msg := string(buf[:n])
fmt.Println("收到:", msg)
// 我期望: 先收到 "hello", 再收到 "world"(两次Read各一条)
// 实际可能:
//   一次Read读到了 "helloworld"   ← 💥 两条消息"粘"在一起了!(粘包)
//   或者: 第一次Read读到 "hel", 第二次读到 "loworld"  ← 一条被拆开了(拆包)
//   或者: 各种乱七八糟的切分...

我盯着服务端打印出的 helloworld(两条消息粘成一坨),还有有时一条消息被切成几段才读全的诡异现象,彻底懵了。我明明在客户端分两次、清清楚楚地 send 了 "hello" 和 "world" 两条消息,为什么服务端一次 Read 就把它们粘在了一起?有时又把一条消息拆成了好几次才读完?发送和接收的次数、边界,怎么完全对不上?这种"消息边界丢失"的现象,让我精心设计的协议根本没法正确解析消息

第一件事:看清真相——TCP 是面向字节流的,根本没有"消息"的概念

我去深入理解了 TCP 的本质,才彻底明白这个"粘包拆包"之谜——TCP 是一个面向"字节流(byte stream)"的协议:它保证的是"你发出去的字节,会按顺序、不丢失地到达对方",但它完全不保证"消息的边界"——在 TCP 眼里,根本没有"消息"这个东西,只有一串连续的字节;你 Write 几次、每次多少字节,和对方 Read 几次、每次读到多少字节,毫无对应关系

TCP 粘包/拆包的真相

# 1. TCP 是【面向字节流(byte stream)】的协议, 不是【面向消息】的:
#    - 它把数据看成一串连续的、没有边界的【字节流】(像水管里的水);
#    - 它保证: 你发的字节, 【按顺序、不重不丢】地到达对方;
#    - 但它【不保证】: "一次send的数据 = 一次recv读到的数据"!
#    - 在TCP看来, 根本【没有"消息/包"的概念】, 只有字节流。

# 2. 所以会发生:
#    a) 【粘包】: 你两次 Write("hello")、Write("world"),
#       TCP可能把它们合在一起发/对方一次Read读到 "helloworld"。
#       (TCP/Nagle算法为效率, 可能合并小包一起发)
#    b) 【拆包】: 你一次 Write 一条大消息,
#       对方可能要分多次Read才读全(受MTU、缓冲区、网络分段影响)。
#    c) 甚至半条粘半条: Read到 "hellowor", 下次 "ld"...

# 3. 根源: "消息的边界" 是【应用层的概念】, TCP【不关心、也不维护】它;
#    - TCP只负责"把字节可靠有序地搬过去", 至于"这串字节里, 哪到哪是一条消息",
#      TCP一无所知, 也不负责——这是【应用层自己要解决的事】!

# 4. 为什么我会错: 我把"应用层的消息"和"TCP的字节流"混为一谈了;
#    我以为send一条消息是一个"原子的、有边界的"动作, 但对TCP它只是"往字节流里塞了几个字节"。
#    (注: UDP是面向消息的, 一个数据报对应一次收发, 有边界; 但UDP不可靠。TCP可靠但无边界。)

# 5. 既然TCP不管边界, 应用层就必须【自己定义和解析消息边界】。

# 核心: TCP是面向字节流的, 保证字节有序不丢但不保证消息边界, send和recv不一一对应,
#   会粘包/拆包; "消息边界"是应用层的事, 必须由应用层协议自己定义和解析。

真相大白,我恍然大悟。原来 TCP 是一个面向"字节流"的协议:它把数据看成一串连续的、没有边界的字节(像水管里的水);它保证"你发的字节按顺序、不重不丢地到达",但完全不保证"一次 send 的数据 = 一次 recv 读到的数据"——在 TCP 看来,根本没有"消息/包"的概念,只有字节流。所以会发生:粘包(两次 Write 的 hello、world 被合在一起,一次 Read 读到 helloworld——TCP/Nagle 算法为效率可能合并小包)、拆包(一条大消息要分多次 Read 才读全——受 MTU、缓冲区影响)、甚至半条粘半条。根源是:"消息的边界"是应用层的概念,TCP 不关心、也不维护它——TCP 只负责"把字节可靠有序地搬过去",至于"这串字节里哪到哪是一条消息",TCP 一无所知,这是应用层自己要解决的事我犯错,是因为把"应用层的消息"和"TCP 的字节流"混为一谈了——我以为 send 一条消息是个"原子的、有边界的"动作,但对 TCP 它只是"往字节流里塞了几个字节"。(注:UDP 是面向消息的、有边界但不可靠;TCP 可靠但无边界。)既然 TCP 不管边界,应用层就必须自己定义和解析消息边界

第二件事:正解——应用层定义消息边界(长度前缀最常用)

搞懂了原理,正解就清晰了:在应用层协议里自己定义"消息边界"——最常用、最可靠的是"长度前缀法"(先发消息长度,再发消息内容;接收方先读长度,再按长度读完整一条)

// ====== 正解一(最常用、最可靠): 长度前缀法(length-prefix) ======
// 协议: 每条消息 = [4字节长度][消息内容]
// 发送:
func sendMsg(conn net.Conn, data []byte) error {
    length := uint32(len(data))
    binary.Write(conn, binary.BigEndian, length)   // 先发4字节长度
    _, err := conn.Write(data)                     // 再发消息内容
    return err
}
// 接收: 先读4字节长度, 再【按长度读满】完整一条
func recvMsg(conn net.Conn) ([]byte, error) {
    var length uint32
    if err := binary.Read(conn, binary.BigEndian, &length); err != nil {  // 读长度
        return nil, err
    }
    data := make([]byte, length)
    _, err := io.ReadFull(conn, data)   // ★ io.ReadFull: 一定读满length个字节(自动处理拆包)
    return data, err
}
// → 接收方"先知道这条消息有多长, 再精确地读这么多字节", 边界清清楚楚, 不粘不拆。

// ====== 正解二: 分隔符法(适合文本协议) ======
// 用特殊字符(如 \n)分隔消息: "hello\nworld\n"
// 接收方用 bufio.Scanner / ReadString('\n') 按分隔符切分
reader := bufio.NewReader(conn)
line, _ := reader.ReadString('\n')   // 读到一个\n为止, 就是一条消息
// → 简单, 但消息内容里不能含分隔符(或要转义); 适合行文本协议。

// ====== 正解三: 定长消息法 ======
// 每条消息固定长度(如固定128字节), 接收方每次读固定字节数。
// → 简单, 但浪费(短消息也占满)、不灵活; 适合消息长度固定的场景。

// ====== 正解四(推荐): 直接用成熟的协议/框架 ======
// 别自己造轮子! 用 HTTP、gRPC、WebSocket、Protobuf+成熟RPC框架,
// 它们已经帮你处理好了消息边界(分帧/framing)、序列化等一切。
// → 除非有特殊需求, 一般不该自己裸写TCP协议。

// 核心: 应用层必须自己定义消息边界——长度前缀法(先发长度再发内容, io.ReadFull读满)最常用可靠,
//   或分隔符法(文本)、定长法; 但更推荐直接用HTTP/gRPC/WebSocket等成熟协议, 别自己造轮子。

修复的核心,是"应用层自己定义消息边界,长度前缀法最常用"正解一(最常用、最可靠):长度前缀法——协议定为 [4字节长度][消息内容]:发送时先发 4 字节长度、再发内容;接收时先读 4 字节长度、再用 io.ReadFull 按长度读满完整一条(ReadFull 会自动处理拆包、一定读够指定字节);接收方"先知道这条多长、再精确读这么多",边界清清楚楚,不粘不拆正解二:分隔符法——用特殊字符(如 \n)分隔,接收方按分隔符切分;简单但内容不能含分隔符(或要转义),适合行文本协议正解三:定长消息法(每条固定长度,简单但浪费、不灵活)。正解四(推荐):直接用成熟协议/框架——别自己造轮子,用 HTTP、gRPC、WebSocket、Protobuf+RPC 框架,它们已处理好消息边界(分帧)、序列化等一切;除非有特殊需求,一般不该自己裸写 TCP 协议归根结底:应用层必须自己定义消息边界——长度前缀法(先发长度再发内容、io.ReadFull 读满)最常用可靠,或分隔符/定长;但更推荐直接用 HTTP/gRPC/WebSocket 等成熟协议。

第三件事:TCP / 网络编程的其他常见认知坑

排查后我把 TCP 和网络编程相关的其他常见认知坑也系统梳理了一遍。

TCP / 网络编程的其他认知坑

# 1. 以为send/recv一一对应(本文): TCP是字节流无边界。→ 应用层定义边界。

# 2. Read返回的n不一定等于你要的: Read可能只读到一部分。→ 用ReadFull读满, 或循环读。

# 3. Write也可能没写完: 一次Write不保证全写出去(尤其大数据)。→ 检查返回值或用完整写。

# 4. 以为TCP有"包": 没有应用层的"包/消息", 只有字节流。

# 5. 忽略半包/粘包处理: 不做分帧, 高负载/大消息时必现解析错乱。

# 6. UDP当TCP用(或反之): UDP有边界但不可靠(会丢/乱序/重复), TCP可靠但无边界, 别搞混。

# 7. 没处理连接半关闭/对端关闭: 对方关了还在写/读。→ 正确处理EOF和错误。

# 8. 自己造轮子裸写协议: 容易漏掉边界、序列化、错误处理等。→ 优先用成熟协议。

# 共同根源: 把"应用层的、有结构的消息"和"传输层TCP提供的、无结构的可靠字节流"混淆了;
#   TCP给的是"可靠有序的字节搬运", 而"消息的结构和边界"全要应用层自己负责。

# 核心: 理解TCP是可靠有序但无边界的字节流(非消息); send/recv不对应、Read/Write可能不完整;
#   应用层负责分帧(消息边界)和完整读写; 优先用HTTP/gRPC等成熟协议而非裸写TCP。

排查让我把 TCP 的其他认知坑也梳理清了。一、以为 send/recv 一一对应(本文)。二、Read 返回的 n 不一定等于你要的(可能只读到一部分,用 ReadFull 或循环读)。三、Write 也可能没写完(检查返回值)。四、以为 TCP 有"包"(只有字节流)。五、忽略半包/粘包处理(不分帧高负载必现错乱)。六、UDP 当 TCP 用(UDP 有边界但不可靠,TCP 可靠但无边界)。七、没处理连接半关闭/对端关闭八、自己造轮子裸写协议它们的共同根源是:把"应用层的、有结构的消息"和"传输层 TCP 提供的、无结构的可靠字节流"混淆了;TCP 给的是"可靠有序的字节搬运",而"消息的结构和边界"全要应用层自己负责核心是:理解 TCP 是可靠有序但无边界的字节流;send/recv 不对应、Read/Write 可能不完整;应用层负责分帧和完整读写;优先用 HTTP/gRPC 等成熟协议下面这张图,是这次粘包拆包的成因与解法:

第四件事:消息分帧的几种方法对照表

这次踩坑后,我把应用层"分帧(定义消息边界)"的几种方法整理成一张表。

方法 原理 优点 / 缺点
长度前缀 先发长度, 再发内容 ✓最通用可靠/内容任意; 要约定长度字段格式
分隔符 用特殊字符分隔(如\n) ✓简单直观/内容不能含分隔符或要转义
定长消息 每条固定长度 ✓最简单/浪费空间、不灵活
长度+类型+内容(TLV) 长度前缀基础上加类型字段 ✓表达力强/略复杂
用成熟协议 HTTP/gRPC/WS已处理分帧 ✓省心健壮/有协议开销, 但通常最优选

这张表把分帧方法钉清了。核心是:应用层有多种方式给字节流"断句"(定义消息边界)——长度前缀(最通用)、分隔符(文本)、定长(简单)、TLV(表达力强);而最省心的是直接用已经帮你处理好分帧的成熟协议(HTTP/gRPC/WebSocket)它给我的最大启发是:"给一段连续的数据流断句、识别出一个个有意义的单元",是一个反复出现的、本质相同的问题——它不只出现在 TCP 分帧,也出现在解析文件格式、词法分析(把字符流切成 token)、解析协议、流式数据处理等无数地方;而解决它的思路也高度一致:长度前缀、分隔符、定长、或更复杂的语法规则这让我意识到一个有价值的视角:很多看似不同领域的问题,在抽象层面其实是同一类问题("从无结构的流中,识别出有结构的单元");当你把它们抽象到一起看,就会发现它们共享着同一套解法和思想;这种"跨领域识别同构问题"的能力,能让你把在一个领域学到的解法(如 TCP 分帧),迁移到另一个领域(如解析自定义文件格式)识别"给数据流断句"这类同构问题、迁移共通的分帧思想——是这个粘包坑带给我的、超越网络本身的思维收获。

第五件事:为什么 TCP 要设计成字节流而非消息

理解了 TCP 为何这样设计,我对这个""也多了一分理解和释然。

设计 带来的好处 代价(坑)
面向字节流(无边界) 简单通用, 不限制应用层用什么消息格式 应用层要自己分帧(粘包)
可合并小包(Nagle) 减少小包数量, 提高网络效率 加剧粘包
按需分段(MTU) 适配不同网络的传输单元 导致拆包
只保证可靠有序 职责单一清晰, 把"含义"留给上层 上层要负责消息语义

这张表道出了 TCP 设计的"取舍"。核心是:TCP 选择"面向字节流、只保证可靠有序、不管消息边界",是一个深思熟虑的、保持职责单一和通用性的设计——它只做好"可靠有序地搬运字节"这一件事,而把"这些字节代表什么消息、边界在哪、什么格式"完全留给应用层,从而能通用地支撑各种各样的应用层协议(HTTP、SMTP、自定义协议……都跑在 TCP 上);"粘包"这个坑,正是这种"通用性"的代价。它给我的启发是:这是分层设计单一职责的一个经典体现——TCP 这一层只专注做好"可靠字节传输",不越界去管上层的"消息语义";这种"每层只做好自己的事、把上层的事留给上层"的克制,正是分层网络协议栈(及一切分层架构)能够灵活、通用、各层独立演进的根本原因这让我对"分层"有了更深的体会:分层架构的威力,来自每层职责的清晰和克制——下层为上层提供一个定义良好但有限的能力(TCP:可靠字节流),上层在此之上构建自己的抽象(应用层:消息);理解"每层提供什么、不提供什么",并在正确的层去解决正确的问题(在应用层解决消息边界,而非指望 TCP),是用好分层系统的关键从 TCP 的"字节流"设计读懂分层与单一职责的智慧、在正确的层解决正确的问题——是这个粘包坑,在技术之上,带给我的更深思考。

第六件事:基于 TCP 做通信时,我现在的判断习惯

现在每当我要基于 TCP 做网络通信,我都会按这张图先想清楚:

这张图的精髓,是"优先用成熟协议;必须裸写 TCP 就牢记它是字节流、自己设计分帧"能用 HTTP/gRPC/WebSocket 就直接用(已处理好分帧);必须裸写 TCP 就牢记它是字节流没有消息边界、必须自己设计分帧(长度前缀最通用),读用 ReadFull 读满整条、写也确保完整写出这套习惯,让我做 TCP 通信时,从"以为 send 一条 recv 一条"变成了"先想分帧、优先用成熟协议"——核心始终是:TCP 是无边界的字节流,消息边界要应用层自己分帧;优先用成熟协议别造轮子。

我立下的几条规矩

这场"TCP 粘包拆包"的事故,换来了我做网络编程时,刻进骨子里的几条铁律:

  1. TCP 是面向字节流的,没有消息边界。这是理解一切粘包拆包的根基。
  2. send 和 recv 不一一对应。多条可能粘、一条可能拆。
  3. 消息边界是应用层的事,要自己分帧。TCP 不管。
  4. 长度前缀法最通用可靠。先发长度,接收用 ReadFull 读满整条。
  5. Read/Write 都可能不完整。读用 ReadFull/循环,写要确保全部写出。
  6. UDP 有边界但不可靠,别和 TCP 搞混。选对传输层协议。
  7. 优先用 HTTP/gRPC 等成熟协议。别自己裸写 TCP 协议造轮子。

附:一个完整的长度前缀分帧编解码实现

这次踩坑后,我把"长度前缀分帧"的编解码封装成了一个完整、可复用的实现(Go 示意),裸写 TCP 协议时直接用它收发消息:

package framing

import (
    "encoding/binary"
    "errors"
    "io"
)

const maxMsgSize = 10 * 1024 * 1024   // ★ 限制最大消息大小, 防止恶意/错误的超大length撑爆内存

// WriteMsg: 把一条消息按 [4字节长度][内容] 格式写出
func WriteMsg(w io.Writer, data []byte) error {
    if len(data) > maxMsgSize {
        return errors.New("消息过大")
    }
    // 1. 写4字节长度(大端)
    var lenBuf [4]byte
    binary.BigEndian.PutUint32(lenBuf[:], uint32(len(data)))
    if _, err := w.Write(lenBuf[:]); err != nil {
        return err
    }
    // 2. 写消息内容
    _, err := w.Write(data)
    return err
}

// ReadMsg: 读出完整的一条消息(自动处理粘包/拆包)
func ReadMsg(r io.Reader) ([]byte, error) {
    // 1. 先读4字节长度
    var lenBuf [4]byte
    if _, err := io.ReadFull(r, lenBuf[:]); err != nil {   // ★ ReadFull确保读满4字节
        return nil, err
    }
    length := binary.BigEndian.Uint32(lenBuf[:])
    // 2. 校验长度, 防止超大length导致make巨大切片
    if length > maxMsgSize {
        return nil, errors.New("消息长度超限, 可能是非法数据")
    }
    // 3. 按length精确读满整条消息
    data := make([]byte, length)
    if _, err := io.ReadFull(r, data); err != nil {        // ★ ReadFull自动处理拆包
        return nil, err
    }
    return data, nil
}

// 用起来极简, 收发都是"一条完整消息", 不用操心粘包拆包:
//   framing.WriteMsg(conn, []byte("hello"))
//   msg, err := framing.ReadMsg(conn)   // 一定拿到完整的"hello"

// 核心: 长度前缀分帧 = 写时[4字节长度][内容]、读时先ReadFull读长度再ReadFull读满内容;
//   配合最大长度校验防超大length攻击; 封装好后收发即"完整消息", 彻底屏蔽粘包拆包。

这个分帧编解码实现,是我这次踩坑后最有价值的工程沉淀。它把"长度前缀分帧"这个原理,落地成了两个干净的函数 WriteMsg/ReadMsg:发送方调 WriteMsg 写一条消息、接收方调 ReadMsg 拿到完整的一条消息,粘包拆包的所有复杂性,都被彻底屏蔽在这两个函数内部——上层业务代码,终于可以回到那个朴素直觉:"我发一条消息、你收一条消息"。它还藏着一个我特意加的安全细节:对读到的 length 做最大值校验——因为 length 是从网络读来的、不可信的数据,如果一个恶意或错误的报文声称自己有 4GB 长,而我直接 make([]byte, length),就会瞬间撑爆内存;所以必须限制最大消息大小,拒绝异常的 length这正是我想强调的核心思想:处理来自网络的数据时,要对其中的每一个"控制字段"(尤其是长度、数量、偏移这类会被用来分配资源或控制循环的字段)保持警惕——把它们当成不可信输入来校验;因为攻击者(或一个出错的对端)可以伪造这些字段,用一个"声称的超大长度"诱导你分配巨量内存、或用一个"畸形的偏移"让你越界这其实呼应了"信任边界"的原则:网络的另一端是你不可信的外部世界,从那里读来的不只是"数据内容"不可信,连"描述数据的元信息(如长度)"也同样不可信;在用这些元信息做任何"分配资源/控制流程"的决策前,必须先校验它们的合理性把长度前缀分帧封装好彻底屏蔽粘包、并对不可信的长度字段做校验防资源耗尽——这,是我用一次粘包的事故,换来的、关于"如何健壮且安全地处理网络数据"的实用工程智慧。

写在最后

回头看,这场由"TCP 粘包拆包"引发的、消息解析错乱的事故,真正教给我的,远不止"用长度前缀分帧"这一个技巧。它让我对"不同抽象层提供的'保证'是不同的",有了一次深刻的认识。我栽跟头,根源是我错误地假设了 TCP 这一层提供的"保证"。我潜意识里以为 TCP 提供的是"消息级"的保证(我发一条消息、你收一条消息),可它实际提供的,只是"字节级"的保证(我发的字节、你能可靠有序地收到)。我把"我希望它提供的保证(消息边界)"当成了"它实际提供的保证(可靠字节流)",在一个它从未承诺过的能力上建立了我的代码,自然就崩了。这让我领悟到一个深刻的认知:使用任何一个组件/层/服务时,最关键的,是准确地搞清楚它到底向你"保证(guarantee)"了什么、又没有保证什么;不能想当然地假设它提供了某种"你希望它有"的保证,而要去确认它契约里白纸黑字承诺的边界;很多系统级的 bug,都源于"在一个组件没有提供的保证之上,建立了依赖"(以为 TCP 保证消息边界、以为某存储保证强一致、以为某队列保证不重复)这其实是分布式/系统设计的一条核心准则:清晰地理解你所依赖的每一层/每个组件的"保证语义"——它保证什么(TCP:可靠有序的字节)、不保证什么(消息边界、实时性);然后在它"保证的能力"之上,由你自己补足那些"它不保证、但你需要"的能力(自己分帧);把"它有什么保证"搞清楚,是正确地依赖它、并在它之上构建可靠系统的前提准确理解每一层的"保证语义"、不在它未承诺的能力上建立依赖——这,是我用一次 TCP 粘包的事故,换来的、关于网络、关于分层系统、也关于如何正确依赖任何组件的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次基于 TCP(或任何底层组件)做事前,先问一句"它到底保证了什么、没保证什么?",那我对着那坨粘在一起的消息排查的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的订单列表页慢得离谱,抓 SQL 一看一个请求里竟然发了一百多条几乎一样的查询,我对着 ORM 懒加载在循环里逐个触发的 N+1 查询这个坑排查了大半天的复盘

2026-6-2 12:26:45

技术教程

我给容器设了 2G 内存上限,Java 服务却反复被 OOMKilled 重启,JVM 日志里还说自己堆远没满,我对着容器里的 JVM 不感知 cgroup 内存限制按宿主机内存设堆这个坑排查了大半天的复盘

2026-6-2 12:39:53

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索