我的 C# 服务跑一段时间就报连接池耗尽、超时连不上数据库,代码看着都正常、连接也都"用完了",我对着 IDisposable 没释放排查了大半天的复盘

用 C# 写的数据服务,刚启动正常,跑上几小时到一天就报 The connection pool has been exhausted(连接池耗尽)、获取连接超时、数据库操作全线卡死,重启又好、过段时间又复发。盯着代码看了又看,每个查询都好好地拿连接、执行SQL、拿结果,逻辑上连接都用完了啊怎么会耗尽?排查大半天才理解 C# 里对资源管理至关重要却极易忽略的概念——IDisposable 与 using,以及"用完了"和"释放了"是两码事。根因是我 new 的 SqlConnection/Command/DataReader 这些 IDisposable 对象用完都没 Dispose:SqlConnection 背后是有限连接池的连接,Open 是借、用完必须 Dispose 才是还;我没还,而 GC 不会及时回收非托管资源,于是每次调用借一个不还,连接越借越少直到借光(默认100)耗尽。这篇从托管/非托管资源与 IDisposable、用 using 确保一定释放(编译成try-finally异常也释放)/自己的类也实现IDisposable/HttpClient要复用的特例的正解、手动Dispose为何不如using、常见IDisposable资源速查、GC的能力边界、决策图与铁律,到附上一段把连接池设小快速复现泄漏vs正确释放的对比代码。核心领悟:自动化(GC)有明确能力边界,它管托管内存但管不好需及时归还的非托管资源,过度信任自动化会在它盲区栽跟头;泄漏型bug潜伏期长难复现,可主动调小资源上限放大后果快速逼出来。

我的 C# 服务跑一段时间就报连接池耗尽、超时连不上数据库,代码看着都正常、连接也都"用完了",我对着 IDisposable 没释放排查了大半天的复盘

那是我用 C# 写的一个数据服务。上线后表现诡异:刚启动时一切正常,可跑上一段时间(几小时到一天不等)就开始报错:The connection pool has been exhausted(连接池耗尽)、获取连接超时,数据库操作全线卡死。重启一下又能好,然后过段时间又复发。我盯着代码看了又看:每个查询都好好地拿了连接、执行了 SQL、拿到了结果——逻辑上"连接都用完了"啊,怎么会耗尽?我甚至怀疑是数据库配置或连接池大小的问题。排查了大半天,我才真正理解了 C# 里那个对资源管理至关重要、却极易被忽略的概念:IDisposable 与 using——以及"用完了"和"释放了"是两码事。这篇就把这场"连接泄漏、池子耗尽"的事故,从头复盘一遍。

故障现场:连接"用完了",却没"还回去"

先看现场。问题就藏在那段"拿了连接却没释放"的代码里:

// 我的数据访问代码: 拿连接、查询、返回 —— 看起来很正常
public List<User> GetUsers()
{
    var conn = new SqlConnection(connectionString);  // ✗ 创建了连接
    conn.Open();
    var cmd = new SqlCommand("SELECT * FROM users", conn);
    var reader = cmd.ExecuteReader();                 // ✗ reader 也是 IDisposable
    var users = new List<User>();
    while (reader.Read())
    {
        users.Add(MapUser(reader));
    }
    return users;
    // ✗✗✗ 致命问题: conn / cmd / reader 都【没有被释放(Dispose)】!
    //   方法结束了, 但这些对象持有的"非托管资源"(数据库连接)没有归还!
}

// 事故是怎么发生的:
// 1. SqlConnection 背后是"数据库连接", 来自一个有限的"连接池"。
// 2. conn.Open() = 从池里"借"一个连接出来用。
// 3. 用完后, 必须 conn.Dispose()(或 Close())= 把连接"还"回池里。
// 4. 但我没有 Dispose! 方法返回后, conn 变量虽然不用了, 但:
//    - C# 的 GC 不会"立刻"回收它(GC 是不确定时机的)。
//    - 在 GC 回收它(并触发析构释放连接)之前, 这个连接【一直被占着】,
//      没还回池里!
// 5. 每次调用 GetUsers() 都"借"一个连接、却"不还" → 池里的连接越借越少。
// 6. 连接池有上限(默认100), 借光了 → 新请求拿不到连接 → 连接池耗尽!
//    (GC 偶尔跑一下能回收一批, 所以"过段时间复发"、重启就好。)

// 现象拼图:
//   - "用完了" ≠ "释放了"! 我逻辑上用完了连接, 但没显式释放它。
//   - IDisposable 的对象(连接/文件/网络流), 持有的是"非托管资源",
//     GC 不会及时回收, 必须【显式 Dispose】才能立刻归还。
//   - 没 Dispose → 资源泄漏 → 有限的资源(连接池)被慢慢耗尽。
//   - ★ 根因: 我依赖了 GC 去回收"本该我手动释放"的非托管资源 ——
//     而 GC 对这类资源, 既不及时、也不可靠。

看清真相后,我恍然大悟。问题的根源,是我创建的 SqlConnectionSqlCommandSqlDataReader 这些 IDisposable 对象,用完后都没有被释放(Dispose)关键在于:SqlConnection 背后是来自有限"连接池"的数据库连接,conn.Open() 是从池里"借"一个,用完必须 Dispose/Close 才能"还"回池里而我没有 Dispose,方法返回后,C# 的 GC 不会"立刻"回收它(GC 时机不确定),在 GC 回收它之前,这个连接一直被占着、没还回池里于是每次调用都"借"一个却"不还",池里的连接越借越少,借光了(默认上限 100)就连接池耗尽(GC 偶尔跑一下回收一批,所以"过段时间复发、重启就好")。我犯的根本认知错误是:"用完了" ≠ "释放了"——IDisposable 对象持有的是"非托管资源"(连接/文件/网络流),GC 不会及时回收,必须显式 Dispose 才能立刻归还;我错误地依赖了 GC 去回收"本该我手动释放"的非托管资源,而 GC 对这类资源既不及时也不可靠

第一件事:搞懂托管资源、非托管资源与 IDisposable

要解决它,得先搞懂 C# 的内存管理:GC 管什么、不管什么,以及 IDisposable 为何存在。

GC、非托管资源 与 IDisposable

# 一、C# 的 GC(垃圾回收)管什么?
#   - GC 自动回收"托管内存"(你 new 的普通对象占的内存)。
#   - 特点: 自动、但【时机不确定】—— 它在它觉得合适时才跑, 你无法预知。
#   - 对"纯内存对象", 这没问题: 晚点回收内存, 影响不大。

# 二、什么是"非托管资源"? GC 管不好它们!
#   - 数据库连接、文件句柄、网络套接字、操作系统句柄... 这些不是"内存",
#     而是"操作系统/外部系统的有限资源"。
#   - 它们的特点: 数量有限(连接池就那么大、句柄就那么多)、
#     且"不用了要尽快归还", 否则别人没得用。
#   - GC 对它们的问题:
#     * GC 只盯着"内存压力", 不知道"连接池快空了"这种外部资源压力。
#     * 所以 GC 不会因为"连接池要满了"就赶紧跑 → 回收不及时。
#     * 在 GC 回收前, 这些宝贵的非托管资源一直被占着 → 泄漏。

# 三、IDisposable: 让你"显式、及时"释放非托管资源
#   - 凡是持有非托管资源的类, 都实现 IDisposable 接口, 提供 Dispose() 方法。
#   - Dispose() = "我用完了, 立刻把非托管资源还回去"(关连接/关文件/释放句柄)。
#   - 关键: 这个 Dispose【必须由你主动调用】, GC 不会及时帮你调!
#   - using 语句 = 自动调用 Dispose 的语法糖(见正解), 保证一定释放。

# 四、一句话总结:
#   - 托管内存: GC 管, 你不用操心(晚点回收无所谓)。
#   - 非托管资源: GC 管不好, 必须你用 IDisposable/Dispose 主动、及时释放。
#   - "实现了 IDisposable 的对象" = "在提醒你: 我有非托管资源, 用完请 Dispose 我!"

# 核心: GC只及时管托管内存, 对数据库连接/文件句柄等有限的非托管资源回收不及时;
#   这类对象实现IDisposable, 必须你主动且及时Dispose(用using)才能归还, 别赖GC。

原来,C# 的"自动内存管理"是有边界的。一、GC 管什么?——GC 自动回收"托管内存"(你 new 的普通对象),但时机不确定;对纯内存对象这没问题(晚点回收影响不大)二、什么是非托管资源?——数据库连接、文件句柄、网络套接字这些不是"内存",而是"操作系统/外部系统的有限资源",数量有限且"不用了要尽快归还";GC 只盯内存压力、不知道"连接池快空了",所以不会及时回收它们,在回收前这些资源一直被占着、泄漏三、IDisposable——持有非托管资源的类都实现它、提供 Dispose();Dispose 是"我用完了立刻把资源还回去";关键是它必须由你主动调用,GC 不会及时帮你调;using 是自动调用 Dispose 的语法糖一句话:托管内存 GC 管你不用操心,非托管资源 GC 管不好、必须你用 IDisposable/Dispose 主动及时释放;"实现了 IDisposable 的对象"就是在提醒你"我有非托管资源,用完请 Dispose 我"

第二件事:正解——用 using 确保 IDisposable 一定被释放

搞懂了原理,正解就清晰了:凡是 IDisposable 的对象,都用 using 包裹,保证无论是否异常都一定 Dispose

// ====== 正解一(推荐, C# 8+): using 声明, 作用域结束自动 Dispose ======
public List<User> GetUsers()
{
    using var conn = new SqlConnection(connectionString);  // ✓ 自动释放
    conn.Open();
    using var cmd = new SqlCommand("SELECT * FROM users", conn);
    using var reader = cmd.ExecuteReader();
    var users = new List<User>();
    while (reader.Read())
        users.Add(MapUser(reader));
    return users;
    // ✓ 方法结束时, conn/cmd/reader 按相反顺序自动 Dispose, 连接归还连接池!
}

// ====== 正解二(经典): using 块, 显式作用域 ======
public List<User> GetUsers2()
{
    using (var conn = new SqlConnection(connectionString))
    {
        conn.Open();
        using (var cmd = new SqlCommand("SELECT * FROM users", conn))
        using (var reader = cmd.ExecuteReader())
        {
            var users = new List<User>();
            while (reader.Read())
                users.Add(MapUser(reader));
            return users;
        }
    } // ← 离开这个块(即使中间抛异常), conn 一定被 Dispose, 连接一定归还!
}
// → using 的本质: 编译成 try-finally, 在 finally 里调 Dispose。
//   所以【无论正常返回还是抛异常】, Dispose 都一定会执行 —— 这是关键!

// ====== 正解三: 自己的类持有非托管资源, 也要实现 IDisposable ======
public class MyResource : IDisposable
{
    private SqlConnection _conn;   // 持有的非托管资源
    public void Dispose()
    {
        _conn?.Dispose();          // 释放持有的资源
        GC.SuppressFinalize(this); // 标准 Dispose 模式
    }
}
// → 如果你的类持有 IDisposable 字段, 你的类通常也该是 IDisposable,
//   并在 Dispose 里释放它们(否则又会泄漏)。

// ====== 正解四: 异步用 await using(C# 8+, 异步释放)======
await using var conn = new SqlConnection(connectionString);
// → 对实现了 IAsyncDisposable 的对象, 用 await using 异步释放。

// ====== 正解五: HttpClient 是特例, 别每次 new + using! ======
// HttpClient 虽然是 IDisposable, 但它【设计为复用】(每次new+using会
//   导致 socket 端口耗尽)。应: 单例复用, 或用 IHttpClientFactory。
// → "是 IDisposable" 不等于 "一定要 using 短生命周期", 看类的设计意图!

// 核心: 凡 IDisposable 都用 using(声明或块)包裹, 它编译成try-finally保证
//   即使异常也一定Dispose; 自己持有非托管资源的类也实现IDisposable; HttpClient例外要复用。

修复的核心,是"用 using 确保每个 IDisposable 对象,无论是否异常都一定被释放"正解一(推荐,C# 8+):using 声明——using var conn = ...,作用域结束时自动按相反顺序 Dispose,连接归还连接池。正解二(经典):using——离开块时(即使中间抛异常)一定 Dispose;using 的本质是编译成 try-finally、在 finally 里调 Dispose,所以无论正常返回还是抛异常 Dispose 都一定执行(这是关键,手动调 Dispose 一旦中间抛异常就漏了)。正解三:自己的类持有非托管资源也要实现 IDisposable(否则又会泄漏)。正解四:异步用 await using(对 IAsyncDisposable)。而一个重要特例:正解五:HttpClient 别每次 new + using——它虽是 IDisposable 但设计为复用(每次 new+using 会导致 socket 端口耗尽),应单例复用或用 IHttpClientFactory;"是 IDisposable"不等于"一定要 using 短生命周期",看类的设计意图归根结底:凡 IDisposable 都用 using 包裹(编译成 try-finally 保证异常也释放);自己持有非托管资源的类也实现 IDisposable;HttpClient 例外要复用。

第三件事:为什么手动 Dispose 不如 using 可靠

排查时我想过"那我手动调 Dispose 不就行了?",但深究后发现,手动调极易出错,using 才是正解。

手动 Dispose vs using

# ✗ 手动调 Dispose 的问题: 异常路径会漏掉!
var conn = new SqlConnection(...);
conn.Open();
DoSomething(conn);        // ← 如果这里抛异常...
conn.Dispose();           // ← 这行就【不会执行】了! 连接泄漏!
# → 只要 Dispose 之前的任何一行抛异常, Dispose 就被跳过 → 泄漏。
# → 你得自己写 try-finally 才能保证, 但这正是 using 帮你做的!

# ✓ using 编译后等价于 try-finally:
using (var conn = new SqlConnection(...)) { ... }
# 等价于:
var conn = new SqlConnection(...);
try { ... }
finally { conn?.Dispose(); }   # ← finally 保证一定执行, 异常也不漏
# → using 把"容易忘、容易因异常漏掉"的释放, 变成了"编译器保证一定执行"。

# 常见的 IDisposable(都该 using):
#   - 数据库: SqlConnection, SqlCommand, DataReader, DbContext...
#   - 文件/流: FileStream, StreamReader/Writer, MemoryStream...
#   - 网络: TcpClient, NetworkStream, (HttpClient 是特例, 见上)...
#   - 其他: 各种 Stream、加密对象、信号量、定时器...

# 怎么不漏? —— 让工具帮你:
#   - 静态分析/IDE 警告: CA2000(对象用完未释放)等规则能检测出来。
#   - 养成习惯: 看到 new 一个 IDisposable, 第一反应就是用 using 包它。

# 核心: 手动Dispose在异常路径会被跳过导致泄漏; using编译成try-finally保证
#   一定释放; 凡IDisposable(连接/流/文件)都用using包, 配静态分析(CA2000)兜底。

排查让我明白,"手动调 Dispose"看似可行,实则极易出错。手动调的致命问题:异常路径会漏掉——conn.Dispose() 之前的任何一行抛异常,这行就不会执行、连接泄漏;你得自己写 try-finally 才能保证,而这正是 using 帮你做的using 编译后就等价于 try-finally——在 finally 里调 Dispose,把"容易忘、容易因异常漏掉"的释放,变成了"编译器保证一定执行"常见的 IDisposable 都该 using:数据库(SqlConnection/Command/Reader/DbContext)、文件流(FileStream/StreamReader)、网络(TcpClient,HttpClient 例外)、各种 Stream/加密/信号量/定时器怎么不漏?让工具帮你——静态分析规则(CA2000 检测对象用完未释放)、养成习惯(看到 new 一个 IDisposable,第一反应就是 using 包它)归根结底:手动 Dispose 在异常路径会被跳过导致泄漏;using 编译成 try-finally 保证一定释放;凡 IDisposable 都用 using 包,配静态分析兜底。下面这张图,是这次连接泄漏的成因与解法:

第四件事:常见 IDisposable 资源速查

这次踩坑后,我把常见的需要 using 释放的 IDisposable 资源整理成一张表,写代码时对照着别漏。

类别 典型类型 泄漏后果
数据库 SqlConnection/Command/DataReader/DbContext 连接池耗尽(本文)
文件 FileStream/StreamReader/StreamWriter 文件句柄耗尽/文件被锁
网络 TcpClient/NetworkStream/Socket 端口/句柄耗尽
内存流 MemoryStream 内存占用(影响小但仍应释放)
同步原语 Semaphore/Mutex/事件句柄 句柄泄漏/死锁
定时器 Timer 回调一直跑/资源不释放
HttpClient(特例) HttpClient 反而要复用,别频繁new+using

这张表,把常见的"用完要释放的资源"列了出来。它们的共同点是:都持有"有限的、外部的"资源(连接、句柄、端口、锁),泄漏了就会让这些有限资源被慢慢耗尽,引发"跑一段时间就崩"的诡异故障它给我的启发是:在 C# 里,看到一个类型实现了 IDisposable,就等于看到一个明确的信号:"我持有需要你主动释放的资源,请用 using 管好我"这其实是 C# 设计上的一种"显式契约":语言通过 IDisposable 接口,把"这个对象需要被显式清理"这件事,从"隐藏在文档里、靠你记住",变成了"明明白白写在类型签名上、IDE 能提示、静态分析能检查"而我之前的错,就是无视了这个清晰的契约信号——明明 SqlConnection 实现了 IDisposable 在向我喊"释放我",我却把它当成普通对象一 new 了之。这让我领悟到:语言/框架提供的"类型层面的契约和约束"(接口、注解、签名),是它在替你"提示重要的注意事项";读代码时,这些信号(实现了什么接口、有什么特性标记)和业务逻辑同样重要,它们往往在无声地告诉你"这里有个你必须正确处理的东西"

第五件事:GC 帮你管什么、不帮你管什么

这次事故让我把"GC 的能力边界"彻底厘清了。我把它该管和不该指望它管的整理了一下。

资源 GC 管吗 你要做什么
普通对象内存 ✓ 自动回收 什么都不用做
数据库连接 △ 最终会,但不及时 using 主动释放
文件/网络句柄 △ 最终会,但不及时 using 主动释放
事件订阅(+=) ✗ 可能导致内存泄漏 用完 -= 取消订阅
静态引用持有的对象 ✗ 一直不回收 别用静态长期持有大对象
非托管内存(Marshal) ✗ 完全不管 手动 FreeHGlobal 等

这张表,彻底厘清了"GC 的能力边界"。核心结论是:GC 只对"纯托管内存"是自动且可靠的;对"非托管资源(连接/句柄)"是"最终会但不及时"(所以要 using 主动释放);而对"事件订阅、静态引用"等,GC 甚至可能根本回收不了(造成内存泄漏)它纠正了我一个根深蒂固的误解:"有了 GC,我就完全不用管内存/资源了"——这是错的。GC 是一个强大的助手,但它有明确的能力边界,且边界之外的事(非托管资源、事件、静态引用)需要你主动管理这让我领悟到一个关于"自动化工具"的普适道理:任何"自动化"(GC、自动内存管理、自动重连、自动伸缩……),都只在它设计覆盖的范围内有效;它的存在,容易让我们产生一种"一切都被自动处理了"的错觉,从而忽略了它覆盖范围之外、仍需我们手动处理的部分所以,用任何自动化工具,都要清楚地知道:"它到底自动处理了什么?又有什么是它不管、需要我自己负责的?"——分清这条界线,才不会因为"过度信任自动化"而在它的盲区里栽跟头。

第六件事:用到一个对象时,我现在的资源管理习惯

现在每当我 new 一个对象,我都会先判断"它要不要我负责释放":

这张图的精髓,是"new 对象前,先判断它要不要我负责释放、以及怎么释放"第一问 "它实现了 IDisposable 吗":没有就是普通对象 GC 管;实现了就说明它持有需要释放的资源然后看生命周期:短生命周期(连接/流/文件)用 using 包裹用完即释放;设计为复用的(HttpClient)单例/工厂复用别频繁 new异步资源用 await using如果我的类持有它作为字段,我的类也要实现 IDisposable、在 Dispose 里释放它最后配 CA2000 静态分析兜底这套习惯,让我用对象时,从"new 完就不管了"变成了"先看它要不要我释放"——核心始终是:看到 IDisposable 就警觉它持有需释放的资源,用 using 管好它,别赖 GC。

我立下的几条规矩

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

  1. "用完了"不等于"释放了"。非托管资源(连接/句柄)用完必须显式释放,GC 不及时。
  2. 凡 IDisposable 都用 using。它编译成 try-finally,保证即使异常也一定 Dispose。
  3. 别手动调 Dispose。异常路径会跳过它导致泄漏,用 using 让编译器保证。
  4. 自己持有非托管资源的类,也实现 IDisposable。否则又是一处泄漏。
  5. HttpClient 是特例,要复用别频繁 new。是 IDisposable 不代表要短生命周期 using。
  6. 看清 GC 的能力边界。它管托管内存,非托管资源/事件订阅/静态引用要你自己管。
  7. 用 CA2000 等静态分析兜底。让工具帮你揪出"用完未释放"的对象。

附:一段能"看见"连接泄漏 vs 正确释放的对比代码

口说无凭。下面用一段对比代码,通过监控连接池,让你亲眼看见"不释放"和"using释放"的天壤之别:

using System;
using System.Data.SqlClient;

class DisposeDemo
{
    static string cs = "Server=...;Max Pool Size=10;";  // 故意把池设小(10)便于复现

    // ✗ 泄漏版: 不释放连接, 很快耗尽池
    static void LeakVersion()
    {
        for (int i = 1; i <= 20; i++)
        {
            try
            {
                var conn = new SqlConnection(cs);   // ✗ 没 using
                conn.Open();                        // 借一个连接
                new SqlCommand("SELECT 1", conn).ExecuteScalar();
                // 不 Dispose, 连接不归还!
                Console.WriteLine($"第{i}次: 成功(但连接没还)");
            }
            catch (Exception e)
            {
                Console.WriteLine($"第{i}次: 失败! {e.GetType().Name}: 连接池耗尽");
                // 大约第11次开始, 池里10个连接全被占, 后续全失败/超时!
            }
        }
    }

    // ✓ 正确版: using 释放, 连接反复复用, 永不耗尽
    static void CorrectVersion()
    {
        for (int i = 1; i <= 20; i++)
        {
            using (var conn = new SqlConnection(cs))  // ✓ using
            {
                conn.Open();                          // 借
                new SqlCommand("SELECT 1", conn).ExecuteScalar();
                Console.WriteLine($"第{i}次: 成功(连接用完即还)");
            } // ← 离开using, 连接归还池里, 下一次循环复用同一批连接
        }
        // → 20次循环, 实际只反复用了池里的少数几个连接, 永不耗尽。
    }

    static void Main()
    {
        Console.WriteLine("=== 泄漏版(不释放)===");
        LeakVersion();      // 第11次左右开始报连接池耗尽
        Console.WriteLine("\n=== 正确版(using释放)===");
        CorrectVersion();   // 20次全部成功
    }
}

/* 输出对比(Max Pool Size=10):
   === 泄漏版(不释放)===
   第1次: 成功 ... 第10次: 成功
   第11次: 失败! InvalidOperationException: 连接池耗尽   ← 借光了!
   第12~20次: 全部失败
   === 正确版(using释放)===
   第1次~第20次: 全部成功(连接反复复用)               ← 永不耗尽!
*/

// 核心: 同样循环20次, 不释放版借满10个就耗尽崩溃, using版反复复用少数连接全成功;
//   把池设小(Max Pool Size=10)就能快速复现"泄漏耗尽", 亲眼看见using的价值。

这段对比代码,把"连接泄漏"这个平时要"跑几小时才暴露"的隐性问题,变成了几秒钟就能亲眼看见的现象它的精妙,在于故意把连接池设小(Max Pool Size=10)——这样"泄漏"的后果就被急剧放大、快速暴露:泄漏版循环到第 11 次,就因为前 10 个连接全被借走没还而耗尽崩溃;而 using 版循环 20 次全部成功,因为每次用完连接都归还池里、被下一次循环复用这,正是我想用这段代码,留给每个 C# 开发者的最后一课:对于"泄漏型"的 bug(连接泄漏、内存泄漏、句柄泄漏),它们在生产里往往"潜伏期很长"(资源充足时看不出来,跑很久慢慢耗尽才爆发),极难复现和排查;而一个有效的办法,是"主动把资源上限调小",人为地压缩潜伏期、放大泄漏后果,让它在测试时就快速暴露这是一种很实用的测试思维:对于"缓慢累积、最终耗尽"型的问题,与其被动等它在生产爆发,不如主动制造"资源紧张"的极端条件,把它逼出来把潜伏的、难复现的问题,通过"调小上限、加大压力"变成"快速、稳定可复现"的现象——这,是我从这场"连接池耗尽"事故里,带走的、关于"如何揪出泄漏型 bug"的、最实用的方法。让问题在测试台上以最快的速度、最明显的方式暴露,远胜过在生产的深夜里苦苦排查。

写在最后

回头看,这场由"IDisposable 未释放"引发的、连接池被慢慢耗尽的事故,真正教给我的,远不止"记得用 using"这一个技巧。它让我对"自动化"这件事,有了更清醒、更立体的认识。我栽跟头,是因为我对 C# 的 GC(垃圾回收)过度信任了:我以为"有了 GC,所有的资源回收都被自动搞定了,我只管 new、不用管收"。可这次事故狠狠地告诉我:GC 这个强大的"自动化",有它明确的能力边界——它擅长回收"内存",却管不好"连接、句柄"这类需要"及时归还"的外部有限资源而我,正是因为太信任这个自动化,而忽略了它边界之外那片"仍需我手动负责"的区域这让我领悟到一个使用一切"自动化工具"时都至关重要的道理:自动化是把双刃剑——它在解放我们的同时,也容易麻痹我们;它让我们习惯于"不用操心",从而在它"恰好不管"的地方,放松了本该有的警惕真正用好自动化的前提,是清醒地理解它的能力边界:它自动处理了什么(让我安心地交给它)、又有什么是它不处理的(让我主动地接管过来)GC 之于内存、连接池之于连接、框架之于种种——每一个自动化机制,都有这样一条"它负责"与"你负责"的分界线;而最危险的 bug,常常就潜伏在我们"误以为自动化会管、其实它不管"的那条边界上既信任自动化、又看清它的边界,在它的盲区主动补位——这,是我用一次"连接池耗尽"的事故,换来的、关于 C#、也关于"自动化的能力边界"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次 new 一个 IDisposable 对象时,条件反射地用 using 把它包起来,那我对着那个反复耗尽的连接池熬的这大半天,就值了。

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

一条扣款消息被消费了两次、用户被重复扣了钱,我查遍代码逻辑都没错、消息也确实只发了一条,我对着幂等性缺失和消息重投排查了大半天的复盘

2026-6-2 6:54:51

技术教程

TypeScript 里我用下标取数组元素、类型明明是 number,运行时却拿到 undefined 还崩了,我对着数组索引访问的类型不安全排查了大半天的复盘

2026-6-2 7:06:10

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