.NET 高并发编程全景:从异步到高性能实战

写在前面

高并发是后端绕不开的话题。面试问、生产遇、架构要想。.NET 在高并发这块其实很强——async/await 模型、Channel、Span、Kestrel 都是世界级的实现,只是被 Java/Go 的声量盖住了。

本文系统梳理 .NET 处理高并发的全景:从理论概念,到异步编程、同步原语、并发集合、并行计算、高性能内存、缓存、限流熔断,最后落到数据库和监控。读完能建立完整的 .NET 高并发知识体系。

版本说明:本文基于 .NET 8 LTS(代码风格与 API 均适用),最低要求 .NET 7(内置限流中间件 AddRateLimiter 是 .NET 7 引入)。所有特性向上兼容 .NET 9 / .NET 10。涉及的关键 API 引入版本见各章节注释。


一、什么是高并发

1.1 定义

高并发没有绝对标准,通常指系统在短时间内处理大量请求的能力。但"高"是相对的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
场景              QPS(每秒请求数)    并发连接数
─────────────────────────────────────────────────
个人博客          10 ~ 100              < 100
中型网站          1k ~ 5k               1k ~ 5k
电商秒杀          1万 ~ 10万            1万 ~ 5万
双十一峰值        10万 ~ 百万           十万级
搜索/广告         百万级                百万级

高并发不是单一指标,是 QPS、响应时间、并发数、
错误率、资源占用等多个维度的综合表现。

1.2 核心指标

1
2
3
4
5
6
7
8
9
QPS / TPS        — 每秒请求数 / 事务数(吞吐量)
并发数            — 同时处理的请求数
响应时间(RT)    — P50 / P95 / P99(看尾部延迟,不只看平均)
错误率            — 失败请求占比
资源占用          — CPU、内存、网络、磁盘 I/O

黄金法则:
  吞吐量(QPS)= 并发数 / 平均响应时间
  想提高 QPS:要么提高并发数,要么降低响应时间

1.3 高并发的挑战

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
1. 资源竞争
   多线程访问共享资源 → 需要同步(锁)→ 锁竞争降低性能

2. 数据一致性
   并发写同一数据 → 脏读、丢失更新、超卖

3. 性能瓶颈
   数据库连接、网络 I/O、CPU、内存都可能成为瓶颈

4. 系统稳定性
   流量突增 → 雪崩(一个服务拖垮整个链路)
   需要限流、熔断、降级

5. 可观测性
   高并发下问题难定位 → 需要链路追踪、指标监控

二、概念澄清:并发、并行、异步

这三个词常被混用,但本质不同。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
并发(Concurrency)— 同时应对多件事(可能交替执行)
  一个厨师同时做三道菜:切菜→炒A→切肉→炖汤→炒B...
  单核 CPU 也能并发(时间片轮转)

并行(Parallelism)— 同时做多件事(真正同时)
  三个厨师同时做三道菜
  需要多核 CPU 才能真正并行

异步(Asynchronous)— 不等待,先干别的
  厨师把菜放进烤箱,趁这时间去切菜,烤箱好了再回来
  本质是"不阻塞等待 I/O"

关系:
  异步是实现并发的一种方式(不阻塞线程)
  并行是利用多核同时计算
  高并发系统通常 = 异步 I/O + 适度并行 + 缓存 + 限流
1
2
3
4
.NET 中的对应:
  异步 → async/await、Task(处理 I/O 等待)
  并行 → Parallel.For、PLINQ(利用多核计算)
  并发 → 并发集合、Channel(多线程协作)

三、异步编程:async/await(.NET 的招牌)

async/await 是 .NET 高并发的基石。它让你写出看起来同步、实际异步的代码,不阻塞线程。

3.1 为什么异步能扛高并发

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
同步代码:
  请求1:读数据库(线程等 50ms)→ 处理 → 返回
  请求2:读数据库(线程等 50ms)→ 处理 → 返回

  每个请求占用一个线程,线程在等 I/O 时空转
  线程池默认几十个线程 → 几十个并发就饱和

异步代码:
  请求1:发起读数据库(线程释放)→ I/O 完成回调 → 处理 → 返回
  请求2:发起读数据库(线程释放)→ ...

  等 I/O 时不占线程,少量线程就能处理上万并发
  这就是 Kestrel 单机能扛数十万并发的根本原因

3.2 基本用法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 同步(阻塞线程)
public User GetUser(int id)
{
    return _db.Users.Find(id);   // 线程在这里等数据库
}

// 异步(不阻塞线程)
public async Task<User> GetUserAsync(int id)
{
    return await _db.Users.FindAsync(id);  // 等待时线程释放
}

3.3 async/await 的本质:状态机

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 你写的代码
public async Task<string> GetDataAsync()
{
    var a = await GetDataAAsync();
    var b = await GetDataBAsync();
    return a + b;
}

// 编译器生成的(简化)
// 本质是一个状态机,await 之间是状态切换
public Task<string> GetDataAsync()
{
    var stateMachine = new StateMachine();
    stateMachine.builder = AsyncTaskMethodBuilder<string>.Create();
    stateMachine.MoveNext();  // 执行到第一个 await
    return stateMachine.builder.Task;
}

// MoveNext 内部根据状态决定执行哪一段
// state 0: 调 GetDataAAsync,注册回调,return
// state 1: A 完成,调 GetDataBAsync,注册回调,return
// state 2: B 完成,拼接结果,完成 Task
1
2
3
4
关键理解:
  await 不是"阻塞等待",而是"注册回调后返回"
  线程在 await 处释放,I/O 完成后用线程池线程继续执行后续代码
  这就是为什么异步不占线程

3.4 Task vs ValueTask

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Task — 引用类型,每次都分配对象
public async Task<int> CountAsync()
{
    if (_cached) return _value;   // 即使同步返回,也分配了 Task
    return await ComputeAsync();
}

// ValueTask — 结构体,热路径可避免分配
public ValueTask<int> CountAsync()
{
    if (_cached) return new ValueTask<int>(_value);  // 零分配!
    return new ValueTask<int>(ComputeAsync());
}
1
2
3
4
5
6
7
8
选择:
  Task       — 一般场景,API 简单,默认用它
  ValueTask  — 高频调用、可能同步完成的热路径
               (缓存命中、内存计算)

注意:
  ValueTask 只能 await 一次(不能多次 await)
  不确定就用 Task,性能敏感再上 ValueTask

3.5 CancellationToken(取消)

高并发系统必须有取消机制——超时、用户放弃、服务降级时及时停止。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public async Task<User> GetUserAsync(int id, CancellationToken cancellationToken)
{
    // 传递给下游
    return await _db.Users.FindAsync(id, cancellationToken);
}

// 调用方控制取消
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
    var user = await GetUserAsync(1, cts.Token);
}
catch (OperationCanceledException)
{
    // 超时或主动取消
}

// 手动取消
cts.Cancel();
1
2
3
4
5
最佳实践:
  ✓ 所有公开的异步方法都接受 CancellationToken
  ✓ 传递给下游(数据库、HTTP、延时)
  ✓ 在循环里检查 token.ThrowIfCancellationRequested()
  ✗ 不要吞掉 CancellationToken 参数

3.6 ConfigureAwait

1
2
3
4
5
// 库代码:不要捕获同步上下文(避免死锁、提升性能)
await DoSomethingAsync().ConfigureAwait(false);

// ASP.NET Core:没有同步上下文,ConfigureAwait(false) 无意义
// (但写了也没坏处,库代码建议写)
1
2
3
4
5
6
7
历史背景:
  老的 ASP.NET / WinForms / WPF 有 SynchronizationContext
  await 默认回到原上下文 → 可能死锁
  ConfigureAwait(false) 避免

  ASP.NET Core 没有 SynchronizationContext → 不存在这问题
  但写库时养成习惯,跨平台兼容

3.7 IAsyncEnumerable(异步流)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 异步流:边产生边消费,不用一次性全部加载
public async IAsyncEnumerable<User> GetUsersAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    await foreach (var row in _db.Users.AsAsyncEnumerable().WithCancellation(ct))
    {
        yield return row;  // 产生一个就返回一个
    }
}

// 消费
await foreach (var user in GetUsersAsync())
{
    Process(user);  // 流式处理,内存友好
}

四、同步原语

当多线程必须访问共享资源时,需要同步。.NET 提供了丰富的同步原语。

4.1 选择指南

1
2
3
4
5
6
7
8
9
场景                         推荐
──────────────────────────────────────────────
简单的临界区保护              lock / Monitor
异步代码里的锁                SemaphoreSlim
原子计数/标志                 Interlocked
读多写少                      ReaderWriterLockSlim
跨进程同步                    Mutex
生产者-消费者                 Channel<T>(不用自己加锁)
高并发字典                    ConcurrentDictionary

4.2 lock / Monitor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private readonly object _lock = new();
private int _count;

public void Increment()
{
    lock (_lock)              // 等价于 Monitor.Enter / Exit
    {
        _count++;
    }
}

// 注意:
// 1. lock 对象要是 readonly private,别 lock(this)、lock(typeof(X))
// 2. lock 内不要 await(lock 不支持,会编译错误)
// 3. 持锁时间尽量短

4.3 SemaphoreSlim(异步友好)

lock 不能 await,异步代码用 SemaphoreSlim。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private readonly SemaphoreSlim _semaphore = new(1, 1);  // 初始1,最大1 = 互斥

public async Task UpdateAsync()
{
    await _semaphore.WaitAsync();       // 异步等待锁
    try
    {
        await DoWorkAsync();            // 锁内可以 await
    }
    finally
    {
        _semaphore.Release();           // 必须释放
    }
}

// 也可以做并发数限制(不只是互斥)
private readonly SemaphoreSlim _concurrencyLimit = new(100);  // 最多100并发

4.4 Interlocked(原子操作)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private int _counter;

// 原子自增(无锁,极快)
Interlocked.Increment(ref _counter);
Interlocked.Decrement(ref _counter);
Interlocked.Add(ref _counter, 10);

// 原子读取/赋值(64位在32位系统上需要)
long value = Interlocked.Read(ref _largeValue);
Interlocked.Exchange(ref _counter, 0);

// CAS(Compare-And-Swap)— 无锁编程基础
int original;
do
{
    original = _counter;
} while (Interlocked.CompareExchange(ref _counter, original + 1, original) != original);

4.5 ReaderWriterLockSlim(读多写少)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private readonly ReaderWriterLockSlim _rwLock = new();

// 多个读线程可以同时进入
public User Read(int id)
{
    _rwLock.EnterReadLock();
    try { return _cache[id]; }
    finally { _rwLock.ExitReadLock(); }
}

// 写线程独占
public void Write(int id, User user)
{
    _rwLock.EnterWriteLock();
    try { _cache[id] = user; }
    finally { _rwLock.ExitWriteLock(); }
}

五、并发集合与 Channel

多线程协作时,优先用并发集合而不是自己加锁

5.1 并发集合一览

1
2
3
4
5
6
7
集合                     特点                          适用
──────────────────────────────────────────────────────────────
ConcurrentDictionary     线程安全字典                    高并发读写、缓存
ConcurrentQueue<T>       无锁 FIFO 队列                 生产者-消费者
ConcurrentStack<T>       无锁 LIFO 栈                   工作窃取
ConcurrentBag<T>         无序,线程本地存储              无顺序要求的并行
BlockingCollection<T>    带阻塞的集合(封装上面)         经典生产者-消费者

5.2 ConcurrentDictionary

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var cache = new ConcurrentDictionary<string, User>();

// 原子的 GetOrAdd(不存在才添加)
var user = cache.GetOrAdd("key", k => LoadFromDb(k));

// 原子的 AddOrUpdate
cache.AddOrUpdate("key",
    addValue: k => new User(),
    updateValueFactory: (k, old) => UpdateUser(old));

// 注意:GetOrAdd 的工厂可能被多次调用(非原子)
// 高性能场景用 TryGetValue + TryAdd 手动控制

5.3 Channel(.NET 高并发的明星)

System.Threading.Channels 是 .NET 专门为高并发生产者-消费者场景设计的,性能远超 ConcurrentQueue + BlockingCollection

1
2
3
4
5
6
7
8
// 安装
// dotnet add package System.Threading.Channels

// 创建 channel(无界)
var channel = Channel.CreateUnbounded<Order>();

// 创建 channel(有界,背压控制)
var bounded = Channel.CreateBounded<Order>(1000);  // 最多缓冲 1000

生产者

1
2
3
4
5
6
// 写入
await channel.Writer.WriteAsync(order);     // 满了会等待(有界)
channel.Writer.TryWrite(order);             // 不等待,返回是否成功

// 完成
channel.Writer.Complete();                  // 通知没有更多数据

消费者

1
2
3
4
5
6
7
// 读取
await foreach (var order in channel.Reader.ReadAllAsync())
{
    await ProcessAsync(order);
}

// channel 完成且读完时,循环自动结束

完整示例:订单处理管道

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class OrderPipeline
{
    private readonly Channel<Order> _channel;

    public OrderPipeline(int capacity = 1000)
    {
        _channel = Channel.CreateBounded<Order>(new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait,    // 满了等待
            SingleReader = false,
            SingleWriter = false
        });
    }

    // 生产者:接收订单
    public async Task EnqueueAsync(Order order, CancellationToken ct)
    {
        await _channel.Writer.WriteAsync(order, ct);
    }

    // 消费者:多 worker 并行处理
    public async Task ConsumeAsync(int workerCount, CancellationToken ct)
    {
        var workers = Enumerable.Range(0, workerCount)
            .Select(_ => Task.Run(async () =>
            {
                await foreach (var order in _channel.Reader.ReadAllAsync(ct))
                {
                    await ProcessOrderAsync(order);
                }
            }));

        await Task.WhenAll(workers);
    }

    public void Complete() => _channel.Writer.Complete();
}
1
2
3
4
5
6
7
8
Channel 的优势:
  ✓ 无锁或细粒度锁,性能极高
  ✓ 支持 async/await(不阻塞线程)
  ✓ 支持背压(BoundedChannel 控制内存)
  ✓ 支持 SingleReader/SingleWriter 优化
  ✓ 是 ASP.NET Core 内部用的(SignalR、Quartz 等)

  可以说:Channel 是 .NET 替代 Go channel 的方案

5.4 不可变集合

1
2
3
4
5
6
7
8
9
// System.Collections.Immutable
// 每次修改返回新实例,天然线程安全

var list = ImmutableList<int>.Empty;
var list2 = list.Add(1);     // list 不变,list2 是新的
var list3 = list2.Add(2);

// 适合:配置、只读缓存、共享状态
// 修改开销大(复制),但读取绝对安全

六、并行计算:TPL

CPU 密集型任务要利用多核,用 TPL(Task Parallel Library)。

6.1 Parallel.For / Parallel.ForEach

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 并行处理(CPU 密集型)
var data = Enumerable.Range(0, 1_000_000).ToArray();

// 并行 For
Parallel.For(0, data.Length, i =>
{
    data[i] = Compute(data[i]);
});

// 并行 ForEach
Parallel.ForEach(data, item =>
{
    Process(item);
});

// 控制并发度
var options = new ParallelOptions
{
    MaxDegreeOfParallelism = Environment.ProcessorCount
};
Parallel.ForEach(data, options, item => Process(item));

6.2 PLINQ(并行 LINQ)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// AsParallel() 把 LINQ 变成并行
var result = data
    .AsParallel()
    .Where(x => x % 2 == 0)
    .Select(x => x * x)
    .OrderBy(x => x)
    .ToList();

// 控制并发度
var result2 = data
    .AsParallel()
    .WithDegreeOfParallelism(8)
    .Select(Compute)
    .ToList();
1
2
3
4
5
注意:
  ✗ 小数据集别用并行(调度开销 > 收益)
  ✗ I/O 密集型别用 Parallel/PLINQ(它们是为 CPU 设计的)
     I/O 用 Task.WhenAll
  ✓ 数据量大 + 计算密集才用

6.3 Task.WhenAll(并发 I/O)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 并发请求多个 I/O(不是并行计算,是并发等待)
var urls = new[] { "url1", "url2", "url3" };

// 错误:串行(一个等一个)
var results = new List<string>();
foreach (var url in urls)
{
    results.Add(await httpClient.GetStringAsync(url));  // 串行!
}

// 正确:并发(一起发起,一起等)
var tasks = urls.Select(url => httpClient.GetStringAsync(url));
var results = await Task.WhenAll(tasks);   // 总耗时 ≈ 最慢的那个
1
2
3
4
5
区分:
  Task.WhenAll  — 并发 I/O(等网络,不占 CPU)
  Parallel.For  — 并行计算(占 CPU 多核)
  
  这是新手最常搞错的点:I/O 密集用 WhenAll,CPU 密集用 Parallel

七、高性能内存:Span、Pipelines、Pool

.NET Core 之后引入了一系列高性能内存原语,这是 .NET 能和原生 C++ 掰手腕的资本。

7.1 Span / Memory(零拷贝)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Span<T> — 连续内存的视图,不复制数据
// 堆栈上分配(ref struct),极快

// 切分数组,零拷贝
byte[] buffer = new byte[1000];
Span<byte> slice = buffer.AsSpan(100, 50);  // 从100开始,取50个

// stackalloc — 栈上分配(不进堆,无 GC)
Span<int> stackData = stackalloc int[100];  // 栈上,方法结束自动回收

// 字符串操作零拷贝
string text = "Hello, World";
ReadOnlySpan<char> hello = text.AsSpan(0, 5);  // "Hello",不新建字符串
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 高性能解析示例
// 旧:每步都分配新字符串
var parts = input.Split(',');   // 分配数组 + 多个字符串

// 新:用 Span 零拷贝解析
var reader = new SpanReader(input.AsSpan());
while (!reader.Done)
{
    var token = reader.ReadUntil(',');
    Process(token);   // token 是原始数据的切片,不分配
}
1
2
3
4
5
6
Span 限制(ref struct):
  ✗ 不能作为类的字段
  ✗ 不能跨 await
  ✗ 不能装箱
  
  需要跨 await / 存字段 → 用 Memory<T>(堆上,可异步)

7.2 System.IO.Pipelines

专门为高性能网络 I/O 设计,Kestrel 内部就用它。解决"数据不完整、粘包"的问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 安装
// dotnet add package System.IO.Pipelines

async Task ProcessPipeAsync(PipeReader reader)
{
    while (true)
    {
        ReadResult result = await reader.ReadAsync();
        ReadOnlySequence<byte> buffer = result.Buffer;

        // 尝试解析完整消息
        while (TryParseMessage(ref buffer, out var message))
        {
            ProcessMessage(message);
        }

        // 告诉 PipeReader 消费了多少、还剩多少
        reader.AdvanceTo(buffer.Start, buffer.End);

        if (result.IsCompleted) break;
    }
}
1
2
3
4
5
6
Pipelines 解决的痛点:
  ✓ 自动管理缓冲区(不用自己 new byte[])
  ✓ 处理消息不完整(等更多数据)
  ✓ 处理消息粘包(一次读多条)
  ✓ 内存复用(零拷贝、少分配)
  ✓ 背压(消费慢时通知生产者暂停)

7.3 ArrayPool / ObjectPool

减少 GC 压力的利器——复用对象而不是反复分配。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ArrayPool — 复用数组
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);  // 租借(可能更大)
try
{
    ProcessData(buffer);
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);  // 归还(不是 GC 回收)
}

// ObjectPool — 复用对象(Microsoft.Extensions.ObjectPool)
var pool = new DefaultObjectPool<StringBuilder>(
    new DefaultPooledObjectPolicy<StringBuilder>());

var sb = pool.Get();
try
{
    sb.Append("...");
    var result = sb.ToString();
}
finally
{
    sb.Clear();
    pool.Return(sb);
}
1
2
3
4
5
6
何时用 Pool:
  ✓ 高频分配 + 大对象(大 byte[]、StringBuilder)
  ✓ 热路径(每秒成千上万次)
  ✗ 低频场景(开销 > 收益)

  原理:对象复用,减少 GC,降低内存分配压力

八、线程池与 Kestrel

8.1 线程池机制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.NET 线程池(ThreadPool):
  - 托管所有工作线程
  - 按需增长(IO 密集型增长快)
  - 复用线程(避免频繁创建销毁)

线程池的"饥饿"问题:
  - 所有线程都被阻塞(同步 I/O)
  - 新任务排队等待
  - 响应时间飙升

  这就是为什么强调异步:
  异步释放线程 → 线程池可用线程多 → 抗并发
  同步阻塞线程 → 线程池耗尽 → 系统卡死
1
2
3
4
5
6
7
// 配置线程池(.NET 中通常用环境变量或 ThreadPool.SetMinThreads)
// 设置最小线程数,避免冷启动时线程增长慢
ThreadPool.SetMinThreads(workerThreads: 100, completionPortThreads: 100);

// 推荐:在 Program.cs 早期设置
// 或用环境变量
// DOTNET_ThreadPool_MinThreads=100

8.2 Kestrel(ASP.NET Core 服务器)

Kestrel 是 ASP.NET Core 的内置 Web 服务器,性能世界级。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Kestrel 为什么快:
  ✓ 基于 libuv / IOCP(异步 I/O 完成端口)
  ✓ 完全异步架构(async/await 全链路)
  ✓ Pipelines 处理网络数据
  ✓ 内存池(ArrayPool)减少分配
  ✓ HTTP/2、HTTP/3 支持

性能参考(官方基准):
  简单 JSON 接口:单机 10万+ RPS
  Plaintext(TechEmpower):单机百万级 RPS
  位居 TechEmpower 前列(和 Rust/C++ 一个梯队)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Program.cs 配置 Kestrel
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.MaxConcurrentConnections = 10000;       // 最大连接数
    options.Limits.MaxConcurrentUpgradedConnections = 10000; // WebSocket
    options.Limits.MaxRequestBodySize = 10 * 1024 * 1024;  // 10MB
    options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
    options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});

九、缓存

缓存是高并发的第一道防线——能用缓存的绝不打数据库。

9.1 多级缓存

1
2
3
4
5
6
7
8
浏览器缓存      — 客户端,静态资源
CDN            — 边缘节点,静态 + 动态加速
Nginx 缓存     — 反向代理层,减少打到应用的请求
内存缓存        — 进程内(IMemoryCache),最快
分布式缓存      — Redis/Memcached,跨实例共享
数据库缓存      — MySQL 查询缓存(已废弃)、缓冲池

层级越靠前,速度越快,容量越小

9.2 IMemoryCache(进程内缓存)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 注册
builder.Services.AddMemoryCache();

// 使用
public class UserService
{
    private readonly IMemoryCache _cache;

    public UserService(IMemoryCache cache) => _cache = cache;

    public async Task<User> GetUserAsync(int id)
    {
        // 缓存键
        var key = $"user:{id}";

        // GetOrCreateAsync:不存在则加载并缓存
        return await _cache.GetOrCreateAsync(key, async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);  // 30分钟过期
            entry.SlidingExpiration = TimeSpan.FromMinutes(10);                // 10分钟无访问过期
            return await _db.Users.FindAsync(id);
        });
    }
}

9.3 IDistributedCache + Redis

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 注册 Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "myapp:";
});

// 使用(和 IMemoryCache 接口类似)
public class CacheService
{
    private readonly IDistributedCache _cache;

    public async Task<string> GetAsync(string key)
    {
        return await _cache.GetStringAsync(key);
    }

    public async Task SetAsync(string key, string value)
    {
        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
        };
        await _cache.SetStringAsync(key, value, options);
    }
}
1
2
3
4
5
IMemoryCache vs IDistributedCache:
  IMemoryCache     — 进程内,最快,但多实例不共享,重启丢失
  IDistributedCache — Redis 等,跨实例共享,持久,但网络开销

  实战:本地缓存做一级(抗热点),Redis 做二级(一致性)

十、限流与熔断

高并发系统必须保护自己——流量超载时主动拒绝,而不是被拖垮。

10.1 限流(.NET 7+ 内置)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// .NET 7+ 内置限流中间件
builder.Services.AddRateLimiter(options =>
{
    // 全局并发限制
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
        httpContext => RateLimitPartition.GetConcurrencyLimiter(
            partitionKey: "global",
            factory: _ => new ConcurrencyLimiterOptions
            {
                PermitLimit = 1000,         // 全局最多 1000 并发
                QueueLimit = 100
            }));

    // 按 IP 限流
    options.AddPolicy("per-ip", httpContext =>
        RateLimitPartition.GetTokenBucketLimiter(
            partitionKey: httpContext.Connection.RemoteIpAddress!.ToString(),
            factory: _ => new TokenBucketRateLimiterOptions
            {
                TokenLimit = 100,
                TokensPerPeriod = 100,
                ReplenishmentPeriod = TimeSpan.FromSeconds(1)
            }));

    options.OnRejected = async (context, ct) =>
    {
        context.HttpContext.Response.StatusCode = 429;
        await context.HttpContext.Response.WriteAsync("Too Many Requests", ct);
    };
});

var app = builder.Build();
app.UseRateLimiter();
1
2
3
4
5
限流算法:
  并发限流(ConcurrencyLimiter)  — 限制同时在处理的请求数
  令牌桶(TokenBucket)           — 允许突发,平均速率限制
  固定窗口(FixedWindow)         — 每个时间窗口固定配额
  滑动窗口(SlidingWindow)       — 更平滑的窗口算法

10.2 熔断、重试、超时(Polly)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 安装
// dotnet add package Microsoft.Extensions.Http.Polly
// dotnet add package Polly

// HttpClient 熔断 + 重试
builder.Services.AddHttpClient("api")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.WaitAndRetryAsync(3, attempt =>
            TimeSpan.FromSeconds(Math.Pow(2, attempt))))        // 重试 3 次,指数退避
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.CircuitBreakerAsync(
            handledEventsAllowedBeforeBreaking: 5,              // 连续失败 5 次
            durationOfBreak: TimeSpan.FromSeconds(30)));        // 熔断 30 秒

// 超时
    .AddPolicyHandler(Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)));
1
2
3
4
5
6
熔断器三种状态:
  Closed(关闭)  — 正常请求
  Open(打开)    — 失败率达阈值,直接拒绝(快速失败)
  Half-Open       — 试探性放行几个请求,成功则恢复

  作用:下游服务挂了,快速失败,不把整个链路拖死

十一、数据库层面

数据库往往是高并发系统的瓶颈所在。

11.1 连接池

1
2
3
4
5
6
7
8
// ADO.NET / EF Core 默认开启连接池
// 连接字符串配置
"Server=...;Database=...;Pooling=true;Max Pool Size=100;Min Pool Size=10;"

// 注意:
// 连接用完必须 Dispose(using 或 await using)
// 否则连接泄漏,连接池耗尽
await using var conn = new SqlConnection(connStr);  // 自动归还连接池

11.2 异步 EF Core

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 全异步,不阻塞线程
var users = await _db.Users
    .Where(u => u.IsActive)
    .ToListAsync();   // 异步查询

// 注意 N+1 问题
// 错误(N+1 查询)
foreach (var user in await _db.Users.ToListAsync())
{
    var orders = await _db.Orders.Where(o => o.UserId == user.Id).ToListAsync();
    // 100 个用户 = 101 次查询
}

// 正确(Include / Join)
var users = await _db.Users
    .Include(u => u.Orders)
    .ToListAsync();   // 1 次查询

11.3 Dapper(高性能 ORM)

1
2
3
4
5
6
7
// Dapper 比 EF Core 快 5-10 倍(轻量,接近原生 ADO.NET)
// 适合:性能敏感的查询、复杂 SQL

using var conn = new SqlConnection(connStr);
var users = await conn.QueryAsync<User>(
    "SELECT * FROM Users WHERE IsActive = @isActive",
    new { isActive = true });
1
2
3
4
5
EF Core vs Dapper:
  EF Core — 开发效率高,功能全,性能中等
  Dapper  — 性能极致,SQL 自己写
  
  实战:核心 CRUD 用 EF Core,热点查询用 Dapper

11.4 数据库层面优化

1
2
3
4
5
6
✓ 索引优化(避免全表扫描)
✓ 读写分离(主写从读)
✓ 分库分表(数据量大时)
✓ 慢查询日志 + EXPLAIN
✓ 批量操作(减少往返)
✓ 连接池合理配置

十二、监控与诊断

高并发系统必须有可观测性,否则出问题两眼一抹黑。

12.1 性能计数器(dotnet-counters)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 实时监控 .NET 应用指标
dotnet-counters monitor -p 12345 \
  System.Runtime \
  Microsoft.AspNetCore.Hosting

# 关注指标:
#   cpu-usage        CPU 使用率
#   gc-heap-size     GC 堆大小
#   gen-0/1/2-gc-count  GC 次数(频繁 GC = 分配过多)
#   time-in-gc       GC 占用时间
#   threadpool-queue-length  线程池队列(长了 = 线程不足/阻塞)
#   working-set      内存占用

12.2 火焰图(dotnet-trace)

1
2
3
# 录制性能数据
dotnet-trace collect -p 12345 --format Speedscope -d 30
# 用 https://speedscope.app 打开,看 CPU 时间花在哪

12.3 链路追踪(OpenTelemetry)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 安装
// dotnet add package OpenTelemetry.Extensions.Hosting
// dotnet add package OpenTelemetry.Instrumentation.AspNetCore
// dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.AddAspNetCoreInstrumentation()    // HTTP 请求
               .AddHttpClientInstrumentation()     // 外部调用
               .AddEntityFrameworkCoreInstrumentation()  // 数据库
               .AddOtlpExporter();                // 导出到 Jaeger/Tempo
    });
1
2
3
4
5
6
7
可观测性三支柱:
  Metrics(指标)   — CPU、QPS、错误率(聚合数据)
  Tracing(追踪)   — 一个请求经过的所有服务(链路)
  Logging(日志)   — 具体的事件记录

  工具:OpenTelemetry(标准)+ Prometheus + Grafana + Jaeger
       或商业:Application Insights、Datadog

十三、高并发架构总结

把前面所有技术组合起来,一个典型的高并发 .NET 系统架构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
                        客户端
                    ┌─────▼─────┐
                    │   CDN     │  静态资源缓存
                    └─────┬─────┘
                    ┌─────▼─────┐
                    │  Nginx    │  负载均衡 + 缓存 + 限流 + HTTPS
                    │  集群     │
                    └─────┬─────┘
              ┌───────────┼───────────┐
              │           │           │
        ┌─────▼─────┐ ┌──▼───┐ ┌─────▼─────┐
        │ Kestrel   │ │Kestrel│ │ Kestrel   │  ASP.NET Core 集群
        │ App #1    │ │App #2 │ │ App #3    │  (全异步、Channel、Span)
        └─────┬─────┘ └───┬───┘ └─────┬─────┘
              │           │           │
              └─────┬─────┘───────────┘
        ┌───────────┼───────────┐
        │           │           │
   ┌────▼────┐ ┌────▼────┐ ┌────▼────┐
   │  Redis  │ │ 消息队列 │ │ 数据库  │
   │ 缓存    │ │(削峰)   │ │(读写分离)│
   └─────────┘ └─────────┘ └─────────┘
1
2
3
4
5
6
7
8
每一层的高并发手段:
  CDN/Nginx    — 缓存、负载均衡、限流(前文 Nginx 系列讲过)
  Kestrel      — 异步架构、Pipelines、内存池
  应用层       — async/await、Channel、并发集合、对象池
  缓存层       — IMemoryCache + Redis 多级缓存
  消息队列     — 削峰填谷,异步解耦
  数据库       — 连接池、读写分离、分库分表、索引
  全链路       — 限流、熔断、重试(Polly)+ 监控(OpenTelemetry)

核心原则

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
1. 异步优先
   I/O 用 async/await,绝不阻塞线程

2. 减少分配
   Span、ArrayPool、对象池,降低 GC 压力

3. 无锁优先
   Channel、并发集合、Interlocked,避免锁竞争

4. 缓存为王
   能缓存的绝不查数据库,多级缓存

5. 保护自己
   限流、熔断、降级,超载时主动拒绝

6. 水平扩展
   无状态设计,随时加机器

7. 可观测
   指标、追踪、日志,问题可定位

十四、小结

本文系统梳理了 .NET 高并发编程的全景:

  • 基础概念:高并发的指标、挑战,并发/并行/异步的区别
  • 异步编程:async/await 状态机、Task/ValueTask、CancellationToken、IAsyncEnumerable
  • 同步原语:lock、SemaphoreSlim、Interlocked、ReaderWriterLockSlim
  • 并发集合:ConcurrentDictionary、Channel(明星)、不可变集合
  • 并行计算:TPL、Parallel、PLINQ,区分 CPU 并行和 I/O 并发
  • 高性能内存:Span/Memory、Pipelines、ArrayPool/ObjectPool
  • 服务器:线程池机制、Kestrel 为什么快
  • 缓存:IMemoryCache、Redis、多级缓存
  • 保护机制:.NET 限流、Polly 熔断重试
  • 数据库:连接池、异步 EF Core、Dapper、读写分离
  • 可观测性:dotnet-counters、dotnet-trace、OpenTelemetry
  • 整体架构:各层高并发手段 + 核心原则

.NET 在高并发领域的能力是被低估的——async/await 模型、Channel、Span、Kestrel 都是顶级实现。掌握这套全景,应对绝大多数高并发场景绰绰有余。

下一篇将深入 .NET 性能优化与 profiling,讲解如何用火焰图定位性能瓶颈。