C# 高并发处理方式
高并发本质是系统在单位时间内处理大量并行请求的能力。
在C#中处理这个问题需要分层解决:首先是架构层面,比如是否采用分布式;然后是语言特性层面,比如异步编程;最后是基础设施层面,比如数据库优化。
目录
1. 异步编程(async/await)
核心思想:
优势:
C# 实现:
2. 分布式架构
(1) 消息队列(削峰填谷)
(2) 分布式缓存
3. 数据库优化
4. 使用锁(lock)和互斥量(Mutex)
乐观锁
Redis分布式锁
原理
适用场景
5. 使用线程池(ThreadPool)和Task.Run
典型误区
6. 并发集合 (System.Collections.Concurrent)
核心思想:
常用类:
1. 异步编程(async/await)
核心思想:
避免阻塞线程。当一个操作(如 I/O - 文件读写、网络请求、数据库查询)需要等待外部资源时,释放当前线程去处理其他请求,待操作完成后再由线程池分配线程继续执行。
优势:
显著提高线程池线程的利用率(一个线程可处理多个请求),用更少的线程服务更多的并发请求,减少线程上下文切换开销,提高系统吞吐量和响应能力。
C# 实现:
使用 async
关键字标记异步方法,在需要等待的操作前使用 await
关键字。
public async Task<ActionResult> GetData()
{var data = await _dbContext.Data.ToListAsync(); // 异步数据库操作return Ok(data);
}
-
关键点:
-
所有 I/O 操作(数据库、网络、文件)必须异步
-
避免
Task.Wait()
或Task.Result
(会导致死锁)
-
2. 分布式架构
(1) 消息队列(削峰填谷)
-
核心思想: 将耗时的、非实时的操作(如发送邮件、生成报告、复杂数据处理)异步化。请求到达后,将任务信息放入消息队列(如 RabbitMQ, Azure Service Bus, Kafka, Amazon SQS),立即返回响应。后台有专门的“消费者”进程从队列中取出消息并处理。
-
优势:
-
削峰填谷: 突发的高流量可以被队列缓冲,消费者按自身能力消费,避免后端服务瞬时过载崩溃。
-
解耦: 生产者和消费者完全解耦,互不影响,提高系统可靠性和可维护性。
-
异步处理: 释放 Web 服务器线程,使其专注于处理用户请求。
-
重试机制: 消息队列通常支持消息传递失败后的重试。
-
// 使用 RabbitMQ.Client 发送
using var channel = _connection.CreateModel();
channel.QueueDeclare("orders");
var body = Encoding.UTF8.GetBytes(orderJson);
channel.BasicPublish("", "orders", body); // 异步解耦
-
推荐工具:RabbitMQ、Kafka、Azure Service Bus
(2) 分布式缓存
-
核心思想: 将频繁读取但不经常变化的数据(如配置、热门商品信息、会话状态)存储在独立于应用服务器、高性能的内存缓存服务(如 Redis, Memcached)中。
-
优势:
-
极大减少对后端数据库(通常是性能瓶颈)的访问次数。
-
数据存储在内存中,访问速度极快。
-
支持分布式部署,多个应用实例共享同一缓存,保证数据一致性。
-
提高应用的可扩展性(水平扩展应用服务器时,缓存层通常更容易扩展)
-
推荐工具:Redis, Memcached
// 使用 StackExchange.Redis
IDatabase cache = Connection.GetDatabase();
await cache.StringSetAsync("key", "value", TimeSpan.FromMinutes(10));
string value = await cache.StringGetAsync("key");
3. 数据库优化
-
连接池: ADO.NET 和 ORM(如 EF Core)默认管理数据库连接池。确保配置合理的
MinPoolSize
和MaxPoolSize
。 -
异步数据库操作: 务必使用 ORM 或 ADO.NET 提供的异步方法(如
ToListAsync()
,SaveChangesAsync()
,ExecuteReaderAsync()
)来执行数据库查询和命令。 -
优化查询:
-
使用索引避免全表扫描。
-
只查询需要的字段(
Select
)。 -
避免 N+1 查询问题(EF Core 中注意使用
Include
或投影)。 -
合理设计数据模型。
-
考虑读写分离、分库分表(在数据量极大时)。
-
- NoSQL 考虑: 对于某些特定场景(如文档存储、键值对、宽列存储、图数据库),NoSQL 数据库(如 MongoDB, Cassandra, Cosmos DB)可能比关系型数据库(如 SQL Server, PostgreSQL)更适合高并发读写和水平扩展。
4. 使用锁(lock
)和互斥量(Mutex
)
-
核心思想: 当多个线程需要访问共享资源(如静态变量、单例实例、文件句柄)时,必须协调它们的访问,防止数据损坏或状态不一致。
-
常用机制:
lock
语句: 最常用,基于Monitor
类,提供互斥锁。
虽然锁在高并发场景下可能会引起性能瓶颈,但在某些情况下仍然需要使用。确保只在必要时使用,并尽可能减少锁定范围。
private object lockObject = new object();
public void ProcessData(int data)
{lock (lockObject){// 执行需要同步的代码块}
}
乐观锁和Redis分布式锁都是处理高并发场景的核心方案
乐观锁
乐观锁 通过版本号(Version)或时间戳(Timestamp)实现“无锁”并发控制:
-
读阶段:读取数据时记录当前版本号。
-
写阶段:提交更新前校验版本号是否未变化,若变化则重试或失败。
-
典型实现包括:
-
数据库乐观锁:通过SQL语句(如
UPDATE ... WHERE version=old_version
)。 -
Redis的WATCH/MULTI:监视Key变化,事务中执行CAS操作
-
Redis分布式锁
原理
基于Redis的原子操作(如SET key value NX PX
)实现互斥访问:
-
加锁:通过
SETNX
设置唯一值并附加超时时间。 -
续期:Redisson的
Watchdog
线程自动延长锁有效期。 -
释放:校验持有者身份后删除Key。
适用场景
-
低冲突场景:如读多写少的业务(商品浏览、配置读取)。
-
突发流量:秒杀系统中库存扣减(配合重试机制)。
-
需高吞吐:避免锁竞争,提升并发能力
乐观锁和Redis分布式锁是处理高并发的互补方案而非互斥:
-
✅ 乐观锁:轻量、高吞吐,适合低冲突场景,需防范ABA问题。
-
✅ Redis分布式锁:强一致、易用,需优化主从容错与锁粒度。
实际系统中常组合使用(如Redis锁拦截请求+数据库乐观锁保证最终一致)
Monitor
类: lock
的底层实现,提供更细粒度控制(如 TryEnter
, Wait
, Pulse
)。
Mutex
: 进程间或跨 AppDomain 的互斥锁,重量级。
//当前电脑只能启动一个WCS程序using (System.Threading.Mutex m = new System.Threading.Mutex(true, AppConfig.Instance.GetConfig("主界面名称"), out Started)){if (Started){if (DbManagerBase.Instance.GetDbTime() == false){MessageBox.Show("数据库连接失败,请检查原因!");}else{Application.EnableVisualStyles();Application.SetCompatibleTextRenderingDefault(false);Application.Run(new FrmMain());}}else{LibManager.WriteLog("WCS程序启动失败,与运行中WCS程序的主界面名称重复", RecordLogTypeEnum.Error.ToString(), "");MessageBox.Show("当前WCS程序已启动!");}}
关键原则:
-
最小化锁范围: 只在绝对必要的地方加锁,并尽快释放锁。
-
避免锁嵌套: 容易导致死锁。
-
锁粒度: 根据共享资源的范围选择合适的锁(细粒度锁通常并发性更好)。
-
优先使用并发集合: 在可能的情况下,用
ConcurrentDictionary
等代替自己用锁实现的集合
5. 使用线程池(ThreadPool
)和Task.Run
对于需要大量并发线程的场景,可以使用Task.Run
来启动一个新任务到线程池中。
.NET 运行时维护一个预先创建的线程池,用于执行后台任务和处理异步回调。
这比手动创建和销毁线程更高效。
Task[] tasks = new Task[10];
for (int i = 0; i < 10; i++)
{tasks[i] = Task.Run(() => DoWork());
}
await Task.WhenAll(tasks);
典型误区
-
过度使用
Task.Run
:I/O 操作直接用async
而非封装线程 -
全局静态锁:导致无谓的线程阻塞
-
同步调用异步方法:
GetAwaiter().GetResult()
引发死锁 -
忽略连接池配置:数据库连接成为瓶颈
💡 黄金法则:
异步化所有 I/O 路径 + 避免共享状态 + 队列解耦
通过负载测试(如 JMeter/LoadRunner)持续验证系统极限。
6. 并发集合 (System.Collections.Concurrent)
-
核心思想:
-
提供线程安全的集合类,允许多个线程安全地添加、移除或访问集合中的元素,而无需开发者手动加锁。
-
常用类:
-
ConcurrentQueue<T>
: 先进先出 (FIFO) 队列。 -
ConcurrentStack<T>
: 后进先出 (LIFO) 栈。 -
ConcurrentDictionary<TKey, TValue>
: 键值对字典。 -
ConcurrentBag<T>
: 无序集合,适用于对象池等场景。 -
BlockingCollection<T>
: 提供阻塞和限制功能的集合,常用于生产者-消费者模式。 -
优势: 简化多线程编程,避免锁竞争(内部使用高效的锁或无锁技术),提高并发访问性能。
-
示例: 使用
ConcurrentQueue
实现简单的生产者-消费者。
-
using System.Collections.Concurrent;private static readonly ConcurrentDictionary<Type, string> assemblyQualifiedNameCache = new ConcurrentDictionary<Type, string>();