我在 C# 里打开了一堆文件流和数据库连接用完就不管了,以为有垃圾回收会自动清理,结果跑久了报 too many open files、连接池也满了,因为 GC 根本不负责释放这些非托管资源的深度复盘

我有段代码要读很多文件、查很多次数据库,写得很自然:new FileStream、new SqlConnection 用完就不管,心想 C# 有垃圾回收、对象没人引用了 GC 会自动清理。功能测试都过了,可一上压力、跑久了服务就崩:报 too many open files(文件句柄太多)、连接池满了、socket 也耗尽。复盘才搞懂:.NET 的 GC 只负责回收托管堆上的内存,但文件句柄、数据库连接、网络 socket、OS 锁这些是非托管资源,是操作系统/外部世界的资源,不在 GC 管辖内;它们必须通过对象的 Dispose() 显式释放(实现 IDisposable 的类型就是在告诉你它持有非托管资源、用完请 Dispose)。而我既没 Dispose 也没用 using,那些句柄和连接就一直被占着不释放,越积越多直到耗尽。这篇复盘从故障现场讲到 GC 的边界(管托管内存不管非托管资源)、IDisposable 与 using,再到用 using 确定性释放、类持有 IDisposable 字段也实现 IDisposable、HttpClient 要复用的完整正解,以及其他以为自动其实要自己管的坑,和自动机制只覆盖有限边界、边界外的责任仍在自己、别被管了一部分的安全感骗成管了全部的认知。

我在 C# 里打开了一堆文件流和数据库连接用完就不管了,以为有垃圾回收会自动清理,结果跑久了报 too many open files、连接池也满了,因为 GC 根本不负责释放这些非托管资源:一次没用 using 释放 IDisposable、误以为垃圾回收什么都管的深度复盘

那次"服务跑几个小时就崩、报句柄耗尽和连接池满"的故障,源于我对 C# 垃圾回收的一个根本性误解。我有段代码,要读很多文件、查很多次数据库,我写得很"自然":var stream = new FileStream(path, ...); // 读它; var conn = new SqlConnection(...); conn.Open(); // 用它——用完就不管了,心想"C# 有垃圾回收(GC),这些对象没人引用了,GC 会自动帮我清理掉"。功能测试都过了。可一上压力、一跑久,服务就崩:IOException: too many open files(打开的文件句柄太多)、The connection pool ... reached its maximum size(数据库连接池满了)、socket 也耗尽。复盘这件事,我才彻底搞懂,后背发凉:问题出在我以为"GC 会自动清理一切",却不知道 GC 只管"托管内存",根本不管"非托管资源".NET 的 GC,负责的是回收"托管堆上的内存"(你 new 出来的对象占的内存)——这部分它确实自动管;但像文件句柄、数据库连接、网络 socket、操作系统锁这些,是"非托管资源"——它们是操作系统/外部世界的资源,不在 GC 的管辖范围内;这些非托管资源,必须通过对象的 Dispose() 方法显式释放(实现了 IDisposable 接口的类型,就是在告诉你"我持有非托管资源,用完请 Dispose 我");而我既没调 Dispose()、也没用 using 语句,只是"不引用它了"——那些文件句柄、数据库连接,就一直被占着不释放,越积越多,直到把操作系统的句柄数、连接池的连接数耗尽。根本原因是:GC 只回收托管内存、不释放非托管资源(文件句柄/连接/socket);这些资源要靠实现 IDisposable 的对象的 Dispose() 显式释放(用 using);我误以为 GC 什么都管,用完不 Dispose,导致非托管资源泄漏耗尽。问题的根,是没用 using/Dispose 释放 IDisposable 对象——误以为 GC 会回收一切,而 GC 不管非托管资源,导致文件句柄/连接泄漏直至耗尽。这篇就把这次"IDisposable 没释放"的坑,从头到尾复盘一遍。

故障现场:用完不释放,句柄和连接耗尽

问题在于持有非托管资源的 IDisposable 对象没被 Dispose:

// 我的错误写法: 用完就不管, 以为GC会清理
public string ReadFile(string path)
{
    var stream = new FileStream(path, FileMode.Open);   // 持有一个文件句柄(非托管资源)
    var reader = new StreamReader(stream);
    return reader.ReadToEnd();
    // ✗ 没有 Dispose / using! 方法返回后, stream/reader 不再被引用,
    //   但它们持有的【文件句柄】不会被立即释放——GC不管这个!
}

public int QueryCount()
{
    var conn = new SqlConnection(connStr);
    conn.Open();                                        // 从连接池借了一个连接(非托管资源)
    var cmd = new SqlCommand("SELECT COUNT(*) FROM t", conn);
    return (int)cmd.ExecuteScalar();
    // ✗ 没有 Dispose / using! 连接没归还连接池, 一直被占着。
}

/*
跑久了为什么崩:
  - 每次调用都打开一个文件句柄/借一个连接, 用完【没释放】;
  - 我以为"对象没人引用了, GC会回收"——但:
    * GC 回收的是【托管内存】(对象本身占的那点内存), 它确实会回收;
    * 但对象持有的【非托管资源】(文件句柄、数据库连接、socket、OS锁)【不在GC管辖内】!
  - 所以: 对象的内存可能被GC回收了, 但它持有的文件句柄/连接, GC不会去释放;
    (就算有终结器finalizer兜底, 也是不确定何时执行、且很慢, 不能依赖);
  - 结果: 文件句柄越占越多 → too many open files;
          连接借了不还 → 连接池很快满 → 拿不到连接 → 超时/报错。

为什么有 IDisposable / Dispose:
  - 实现了 IDisposable 接口(有 Dispose() 方法)的类型, 就是在声明:
    "我持有一些GC管不到的非托管资源, 你用完我必须调 Dispose() 来释放它们";
  - FileStream、SqlConnection、HttpClient(注意复用)、StreamReader、各种Stream/锁... 都是;
  - using 语句 = 自动在作用域结束时(包括异常)调用 Dispose(), 是释放的标准姿势。

★ 核心: GC只自动管"托管内存", 不管"非托管资源(文件句柄/连接/socket/OS锁)";
  实现 IDisposable 的对象, 持有非托管资源, 必须用 using 或显式 Dispose() 释放;
  我的错: 以为"GC什么都管", 用完不Dispose → 非托管资源泄漏 → 句柄/连接耗尽。

盯着 too many open files 和连接池满的报错,我又懊恼又恍然:"我一直以为 C# 有 GC,内存、资源什么的它都会自动收拾,我只管 new、不用管释放……谁知道 GC 只管内存这一块,文件句柄、数据库连接这些'外部资源'它压根不碰,得我自己用 using 关掉。"这个坑最隐蔽的地方在于:功能测试完全正常(测试时调用次数少,句柄/连接没耗尽);它的危害是"泄漏"——缓慢累积、跑久了才爆(同 596 日志写满,都是"只占不放"型);而且 GC 的存在恰恰麻痹了人:正因为"内存有 GC 管",才让人误以为"所有资源都有 GC 管"下面就来拆解,IDisposable 资源到底该怎么释放。

第一件事:搞懂 GC 的边界与 IDisposable

我顺着这次事故,把 GC 管什么、不管什么彻底理清了。

GC 管什么、不管什么? IDisposable 怎么用?

【核心: GC只自动回收"托管内存", 不释放"非托管资源"(文件句柄/连接/socket/OS锁); 实现IDisposable的
   对象持有非托管资源, 必须用using或Dispose()显式释放; 别以为GC什么都管, 非托管资源要确定性释放】

1. GC 管的: 托管内存
   - 你 new 出来的对象, 占用托管堆的内存; 没人引用后, GC会(在某个时机)自动回收这块内存;
   - 这是C#"自动内存管理"的部分, 确实不用你操心。

2. GC 不管的: 非托管资源
   - 文件句柄、数据库连接、网络socket、操作系统锁/信号量、非托管内存(Marshal分配的)...
   - 这些是"操作系统/外部世界"的资源, 数量有限(句柄数、连接数), 不在GC管辖内;
   - GC回收对象内存时, 不会去释放对象持有的这些外部资源(除非有终结器, 但不可靠不及时)。

3. IDisposable: "我持有非托管资源, 用完请释放我"
   - 一个类型实现 IDisposable(有 Dispose() 方法), 就是在声明它持有需要显式释放的资源;
   - 用完它, 必须调 Dispose() 来释放——这是契约;
   - FileStream/SqlConnection/StreamReader/HttpResponseMessage/各种Stream、锁 都是。

4. using: 释放的标准姿势(自动、异常安全)
   - using 语句确保作用域结束时(正常或异常)自动调用 Dispose();
   - using var x = new FileStream(...);   // C#8+ 简化写法, 作用域结束自动Dispose
   - using (var x = ...) { ... }           // 传统写法
   - 等价于 try { ... } finally { x.Dispose(); }——保证一定释放(同591: 资源释放要有归宿)。

5. 几个要点:
   - 持有IDisposable字段的类, 自己也应实现IDisposable, 在Dispose里释放它们(链式);
   - 终结器(~Finalizer)是兜底, 不确定何时跑、还拖慢GC, 别依赖它释放, 优先Dispose;
   - HttpClient特殊: 它是IDisposable但应"长生命周期复用"(别每次new再Dispose, 会耗尽socket),
     用IHttpClientFactory——"该释放的释放, 该复用的复用", 看具体资源的最佳实践。

6. 本质: 自动机制(GC)有它的覆盖边界, 边界外的资源要你自己确定性管理
   - GC让"内存"自动化了, 但"外部资源"没有; 别把"内存被自动管"推广成"一切被自动管";
   - 外部资源(句柄/连接)要"用完即还"——确定性释放(Dispose), 而非交给不确定的GC。

一句话: GC只回收托管内存、不释放非托管资源(句柄/连接/socket); 实现IDisposable的对象持有非托管资源,
   必须用using或Dispose()确定性释放; 别以为GC什么都管, 边界外的外部资源要你自己用完即还。

这套认知,是整个坑的根。GC 管的:托管内存——你 new 的对象占的内存,没人引用后 GC 自动回收GC 不管的:非托管资源(文件句柄、数据库连接、socket、OS 锁)——操作系统的资源、数量有限、不在 GC 管辖内IDisposable:实现它=声明"我持有需显式释放的资源,用完请 Dispose 我"using:确保作用域结束(正常或异常)自动 Dispose,是标准姿势要点:持有 IDisposable 字段的类自己也要实现 IDisposable、终结器不可靠别依赖、HttpClient 要复用别频繁 new本质:自动机制(GC)有覆盖边界,边界外的外部资源要你自己确定性管理(用完即还)一句话:GC 只回收托管内存、不释放非托管资源(句柄/连接/socket);实现 IDisposable 的对象持有非托管资源,必须用 using 或 Dispose() 确定性释放;别以为 GC 什么都管,边界外的外部资源要你自己用完即还。

第二件事:正解——用 using 确定性释放

知道了 GC 不管非托管资源,正解就清楚了:对 IDisposable,用 using 确保用完即释放。

// 正解1: 用 using 释放(C#8+ 简化写法, 作用域结束自动Dispose)——本次该做的
public string ReadFile(string path)
{
    using var stream = new FileStream(path, FileMode.Open);   // 作用域结束自动Dispose, 释放句柄
    using var reader = new StreamReader(stream);
    return reader.ReadToEnd();
}   // ← 方法结束时, reader、stream 按相反顺序自动Dispose, 文件句柄确定性释放

// 正解2: 数据库连接也用 using(用完归还连接池)
public int QueryCount()
{
    using var conn = new SqlConnection(connStr);
    conn.Open();
    using var cmd = new SqlCommand("SELECT COUNT(*) FROM t", conn);
    return (int)cmd.ExecuteScalar();
}   // ← conn 自动Dispose, 连接归还连接池(不是真关, 是还池, 这正是连接池要的)

// 正解3: 传统 using 块写法(需要明确控制作用域时)
using (var stream = new FileStream(path, FileMode.Open))
{
    // 用 stream
}   // 这里Dispose, 等价于 try{...}finally{ stream.Dispose(); }

// 正解4: 自己的类持有 IDisposable 字段, 自己也实现 IDisposable(链式释放)
public class MyService : IDisposable
{
    private readonly FileStream _fs = new("data", FileMode.Open);
    public void Dispose() => _fs.Dispose();    // 释放持有的非托管资源
}
// 调用方: using var svc = new MyService();  // 它也能被using管理

// 正解5: 特殊资源看最佳实践——别一刀切
//   - HttpClient: 是IDisposable, 但应【长生命周期复用】, 别每次new+Dispose(频繁new会耗尽socket/TIME_WAIT);
//     用 IHttpClientFactory 管理; "该复用的复用"也是正确释放的一部分;
//   - 静态/单例持有的资源: 随应用生命周期, 不必每次释放, 但要在应用关闭时释放。

// 反例(别这样):
// var s = new FileStream(...);  // 用完不管, 等GC —— 非托管资源泄漏!
// 依赖 ~Finalizer 释放 —— 不确定何时跑、拖慢GC, 不可靠。

// 核心: 实现IDisposable的对象, 用 using(或显式Dispose)确定性释放, 用完即还;
//   持有IDisposable的类自己也实现IDisposable; 特殊资源(HttpClient)看最佳实践复用; 别等GC。

这套正解的关键,是对持有非托管资源的 IDisposable 对象,用 using 做"用完即还"的确定性释放,而非交给不确定的 GC用 using 释放:文件流、连接都 using,作用域结束(含异常)自动 Dispose——这正是本次缺的。连接归还连接池:Dispose 连接不是真关,是还池,正是连接池要的。自己的类持有 IDisposable 字段:自己也实现 IDisposable,链式释放。特殊资源看最佳实践:HttpClient 要长生命周期复用(频繁 new+Dispose 会耗尽 socket),"该复用的复用"也是正确管理的一部分。

第三件事:其他几个"以为自动其实要自己管"的坑

顺着这次 IDisposable,我把"误以为某机制全自动、忽略其边界"的几类坑也一并理了:

几类"以为自动、其实边界外要自己管"的坑:

坑1: 以为GC防住一切内存问题——其实"内存泄漏"在GC语言里照样有:
   静态集合只加不删、事件没退订(同563)、闭包/缓存长期持有引用 → 对象一直被引用, GC回收不了;
   正解: 注意"还被谁引用着", GC只回收"没人引用的", 长期持有就是泄漏。

坑2: 以为try-finally/using之外异常会自动清理——不会, 资源要自己在finally/using里释放(同591)。

坑3: 以为连接池会自动处理一切——它管复用, 但你借了不还(不Dispose), 池照样会满。

坑4: 以为框架/容器会自动释放我手动new的对象——只有它管理生命周期的(DI注入的)才释放,
   你自己 new 的要自己管。

坑5: 以为线程/任务结束就自动清理其占用——线程局部变量、未取消的定时器、后台任务可能还在;
   正解: 显式取消(CancellationToken)、释放。

坑6: 以为"语言有自动内存管理"就不用懂资源管理——内存只是资源的一种;
   文件、连接、锁、句柄都要管; 自动化覆盖一部分, 不覆盖全部。

共同的根: 一个"自动化/托管机制"(GC、连接池、框架、容器)总有它明确的【覆盖边界】;
   它自动处理边界【之内】的事(如GC管托管内存), 但边界【之外】的事(非托管资源、被持有的引用)
   仍需你自己负责; 误把"它管一部分"当成"它管全部", 就会在它管不到的地方栽跟头。

这些坑看似不同,根却是同一个:一个"自动化/托管机制"(GC、连接池、框架)总有明确的覆盖边界;它自动处理边界之内的事,但边界之外的事仍需你自己负责;误把"它管一部分"当成"它管全部",就会在它管不到的地方栽跟头认清这个根("自动机制有边界,边界外的责任仍在你"),就不会被"有 GC 了还要我管什么"的错觉坑到。

第四件事:GC 管 vs 要自己管 / 资源处理——两张对照表

我把"GC 管什么、要自己管什么"、以及各类资源的处理方式,整理成对照表,贴在了团队的 C# 规范里:

资源 谁负责回收/释放 怎么处理
托管内存(对象本身) GC 自动 不用管(没人引用即回收)
文件句柄(FileStream) 你(Dispose) using
数据库连接 你(Dispose) using,归还连接池
网络 socket / Stream 你(Dispose) using
OS 锁/信号量 你(Dispose/释放) using/try-finally
HttpClient 你,但复用 长生命周期,别频繁 new
被静态集合/事件持有的对象 你(解除引用) 移除引用/退订(同563)
场景 正确做法
局部用一个 IDisposable using var x = ...
需控制释放时机 using (...) { }
类持有 IDisposable 字段 类实现 IDisposable,Dispose 里释放
异步释放 await using / IAsyncDisposable
HttpClient IHttpClientFactory 复用
纯内存对象 不用管,交给 GC

这两张表的核心,第一张是只有"托管内存"是 GC 自动管的,文件句柄/连接/socket/锁这些非托管资源,全都要你自己用 using 释放;第二张是按场景选对释放方式,核心都是"用 using / 实现 IDisposable 做确定性释放"。记住一条:看到一个实现了 IDisposable 的类型,就要条件反射地想"它要 using"——这是它在提醒你"我持有 GC 管不到的资源"。

第五件事:关于 GC 与资源释放的几组容易想当然的认知

这次事故也让我厘清了几组关于 GC 的、容易想当然的概念:

直觉以为 实际上
有 GC,所有资源都自动回收 GC 只管托管内存,非托管资源要自己释放
对象没人引用了资源就释放了 内存会回收,但非托管资源要 Dispose
终结器(Finalizer)会兜底释放 不确定何时跑、拖慢 GC,不可靠
GC 语言不会内存泄漏 长期持有引用(静态集合/事件)照样泄漏
using 是可有可无的语法糖 是确定性释放非托管资源的关键
Dispose 连接是真把它关了 是归还连接池(池化连接),正是要的
HttpClient 也该每次 using 它要复用,频繁 new+Dispose 会耗尽 socket

这张表里,我栽的是第一行和第二行:把"有 GC"理解成了"所有资源都自动回收",以为"对象不引用了资源就没了",完全不知道文件句柄、连接这些非托管资源要我自己 Dispose厘清这些,核心是一个意识:C# 的"自动内存管理(GC)"只解决了"内存"这一类资源;"外部世界的资源"(文件、连接、socket、锁)不在它管辖内,必须由你用 using/Dispose 确定性地"用完即还"——别让 GC 的存在麻痹了你对这些资源的责任。

第六件事:用一个对象 / 资源时,我现在的自检习惯

现在每当我 new 一个对象、或用一个资源,我都会先按这张图问自己:

这张图的精髓,是"看到 IDisposable 就想到 using、用完即还、该复用的复用"先问实现 IDisposable 吗(是就要释放)、用完即还还是复用我的类持有它就也实现 IDisposable这套习惯,让我从"有 GC 我啥都不用管"变成了"非托管资源我用完即还"——核心始终是:GC 只回收托管内存、不释放非托管资源;实现 IDisposable 的对象持有非托管资源,必须用 using 或 Dispose 确定性释放;别以为 GC 什么都管,边界外的外部资源要你自己用完即还。

我立下的几条规矩

这场"句柄和连接耗尽"的事故,换来了我写 C# 时,刻进骨子里的几条铁律:

  1. GC 只自动回收托管内存,不释放非托管资源(文件句柄/数据库连接/socket/OS 锁)。
  2. 实现了 IDisposable 的类型,就是在声明"我持有需显式释放的资源,用完请 Dispose 我"。
  3. 对 IDisposable 对象用 using(或显式 Dispose),作用域结束/异常时确定性释放,用完即还。
  4. 持有 IDisposable 字段的类,自己也要实现 IDisposable,在 Dispose 里链式释放。
  5. 终结器(Finalizer)不确定何时跑、拖慢 GC,是兜底不是依赖;优先 Dispose。
  6. HttpClient 等要长生命周期复用,别每次 new+Dispose(会耗尽 socket);看具体资源最佳实践。
  7. GC 语言也会"内存泄漏":长期持有引用(静态集合/未退订事件)照样回收不了。

附:排查句柄/连接泄漏的手段

最后,把我现在排查"是不是有 IDisposable 资源泄漏"常用的手段贴成一份清单。

# 1. 看进程打开的文件句柄数(Linux), 持续增长就是泄漏
ls /proc//fd | wc -l           # 当前打开的fd数
cat /proc//limits | grep "open files"   # 上限(soft/hard limit)
lsof -p  | wc -l               # 同样看打开的句柄
# 让程序跑一会, 反复看这个数: 只增不减 → 有句柄泄漏

# 2. 数据库连接池监控
#   - 看连接池的活跃连接数/等待数(各驱动都有指标);
#   - "活跃连接持续接近上限、还有大量等待" → 连接借了不还(没Dispose);
#   - DB侧: SELECT count(*) FROM pg_stat_activity;  (PostgreSQL看连接数)

# 3. .NET 诊断工具
#   dotnet-counters monitor -p            # 看GC、句柄数等运行时计数器
#   dotnet-dump / dotnet-gcdump                # 抓内存快照, 分析是否有大量未释放的Stream/Connection

# 4. 静态分析 / 编译器警告
#   - 开启 CA2000(Dispose objects before losing scope)等代码分析规则;
#   - Roslyn分析器能在编译期提示"这个IDisposable没被Dispose";
#   - 让"忘记Dispose"在写代码时就被工具发现, 而非线上才暴露。

# 排查思路: 服务跑久了报 too many open files / 连接池满 →
#   监控fd数/连接数是否只增不减 → 是 → 找出哪里new了IDisposable没using → 补上using。

这份清单的核心,是一个特征判断:句柄数 / 连接数只增不减(同 596 日志只增不减),就是"借了不还"的泄漏信号而最好的防线是把它前移到编译期:开启 CA2000 这类代码分析规则,让"忘记 Dispose"在你写下那行代码时就被工具揪出来,而不是等线上 too many open files 才追悔莫及。

写在最后

回头看,这场由"没释放 IDisposable 资源"引发的、句柄和连接耗尽的事故,真正教给我的,远不止"对 IDisposable 用 using"这一个技巧。它让我对"一个'自动化的、帮你兜底的机制'(垃圾回收), 会给你一种'什么都被照顾到了'的安全错觉; 可任何自动化机制都有它明确的、有限的覆盖边界——它只负责它该负责的那部分, 边界之外的责任依然在你身上, 而你却因为'有它兜底'的错觉而忘了去承担",有了一次刻骨的体会。我栽跟头,是因为我把"GC 帮我管了内存"过度地推广成了"GC 帮我管了一切"——我享受着自动内存管理的便利, 便想当然地以为所有资源都被这套魔法照顾到了;可 GC 的职责边界清清楚楚只画在"托管内存"这一块; 文件句柄、数据库连接这些"外部世界的资源", 从来不在它的责任范围内;我误以为"有了自动挡, 连方向盘都不用握了"——可自动挡只管换挡, 方向还得我自己把着; 我对"边界外那部分仍属于我的责任"失去了警觉, 于是那些没人释放的资源, 就悄悄泄漏、累积、直至耗尽这让我领悟到一个关于"自动化、边界与责任"的深刻认知:每一个"帮我们自动处理某事的机制"(GC、自动备份、自动重试、框架的默认行为、平台的托管服务),都只覆盖一个特定的、有限的范围; 它带来的最大风险, 不是"它没做好它该做的", 而是"它营造的'一切都被管好了'的安全感, 让我们忽略了那些它压根不负责、却依然需要我们亲自负责的部分";"自动化解决了一部分问题" 很容易被心理上夸大成 "自动化解决了全部问题", 从而在自动化覆盖不到的盲区里毫无防备地栽跟头;真正会用自动化的人, 恰恰是清楚地知道"它管什么、不管什么", 并主动接管它管不到的那部分的人这给了我一种依赖任何"自动机制"时的清醒:每当我享受一个"自动化机制"带来的便利时,要刻意地问一句"它的覆盖边界到底在哪?有哪些事是它负责、却依然需要我亲自负责的?"——不让"有它兜底"的安全感, 麻痹我对"边界之外仍属于我的责任"的警觉; 主动识别并接管自动化的盲区;"认清每个自动机制的覆盖边界、主动承担边界之外的责任、不被'它管了一部分'的安全感骗成'它管了全部'",是用好一切自动化而不被其盲区反噬的关键认清自动机制只覆盖有限边界、边界外的责任仍在自己、别被"管了一部分"的安全感骗成"管了全部"——这,是我用一次 IDisposable 没释放的事故,换来的、关于 C#、也关于如何正确依赖自动化的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次 new 一个 FileStreamSqlConnection 时,手指条件反射地敲下那个 using,那我对着 too many open files 排查的这段时间,就值了。

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

我们追求微服务把一个本来内聚的业务拆成了十几个小服务,本想解耦独立部署,结果一个下单流程要串着调八个服务、链路又长又慢、改一个小需求要协调好几个服务一起发版、出了问题谁也说不清的深度复盘

2026-6-3 1:19:28

技术教程

我用逻辑或给配置项设默认值,用户明明传了一个合法的 0,却被我当成没传替换成了默认值,因为 || 判断的是假值不是是否缺失,而 0、空字符串、false 都是合法却为假的值的深度复盘

2026-6-3 1:31:08

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