C#之高并發(fā)處理過程
高并發(fā)本質(zhì)是系統(tǒng)在單位時間內(nèi)處理大量并行請求的能力。
在C#中處理這個問題需要分層解決:首先是架構(gòu)層面,比如是否采用分布式;然后是語言特性層面,比如異步編程;最后是基礎(chǔ)設(shè)施層面,比如數(shù)據(jù)庫優(yōu)化。
1.異步編程(async/await)
核心思想
避免阻塞線程。當(dāng)一個操作(如 I/O - 文件讀寫、網(wǎng)絡(luò)請求、數(shù)據(jù)庫查詢)需要等待外部資源時,釋放當(dāng)前線程去處理其他請求,待操作完成后再由線程池分配線程繼續(xù)執(zhí)行。
優(yōu)勢
顯著提高線程池線程的利用率(一個線程可處理多個請求),用更少的線程服務(wù)更多的并發(fā)請求,減少線程上下文切換開銷,提高系統(tǒng)吞吐量和響應(yīng)能力。
C# 實現(xiàn)
使用 async 關(guān)鍵字標(biāo)記異步方法,在需要等待的操作前使用 await 關(guān)鍵字。
public async Task<ActionResult> GetData()
{
var data = await _dbContext.Data.ToListAsync(); // 異步數(shù)據(jù)庫操作
return Ok(data);
}關(guān)鍵點:
所有 I/O 操作(數(shù)據(jù)庫、網(wǎng)絡(luò)、文件)必須異步
避免
Task.Wait()或Task.Result(會導(dǎo)致死鎖)
2. 分布式架構(gòu)
(1) 消息隊列(削峰填谷)
核心思想:
- 將耗時的、非實時的操作(如發(fā)送郵件、生成報告、復(fù)雜數(shù)據(jù)處理)異步化。
- 請求到達后,將任務(wù)信息放入消息隊列(如 RabbitMQ, Azure Service Bus, Kafka, Amazon SQS),立即返回響應(yīng)。
- 后臺有專門的“消費者”進程從隊列中取出消息并處理。
優(yōu)勢:
- 削峰填谷: 突發(fā)的高流量可以被隊列緩沖,消費者按自身能力消費,避免后端服務(wù)瞬時過載崩潰。
- 解耦: 生產(chǎn)者和消費者完全解耦,互不影響,提高系統(tǒng)可靠性和可維護性。
- 異步處理: 釋放 Web 服務(wù)器線程,使其專注于處理用戶請求。
- 重試機制: 消息隊列通常支持消息傳遞失敗后的重試。
// 使用 RabbitMQ.Client 發(fā)送
using var channel = _connection.CreateModel();
channel.QueueDeclare("orders");
var body = Encoding.UTF8.GetBytes(orderJson);
channel.BasicPublish("", "orders", body); // 異步解耦- 推薦工具:RabbitMQ、Kafka、Azure Service Bus
(2)分布式緩存
核心思想:
將頻繁讀取但不經(jīng)常變化的數(shù)據(jù)(如配置、熱門商品信息、會話狀態(tài))存儲在獨立于應(yīng)用服務(wù)器、高性能的內(nèi)存緩存服務(wù)(如 Redis, Memcached)中。
優(yōu)勢:
- 極大減少對后端數(shù)據(jù)庫(通常是性能瓶頸)的訪問次數(shù)。
- 數(shù)據(jù)存儲在內(nèi)存中,訪問速度極快。
- 支持分布式部署,多個應(yīng)用實例共享同一緩存,保證數(shù)據(jù)一致性。
- 提高應(yīng)用的可擴展性(水平擴展應(yīng)用服務(wù)器時,緩存層通常更容易擴展)
推薦工具:
- Redis, Memcached
// 使用 StackExchange.Redis
IDatabase cache = Connection.GetDatabase();
await cache.StringSetAsync("key", "value", TimeSpan.FromMinutes(10));
string value = await cache.StringGetAsync("key");3. 數(shù)據(jù)庫優(yōu)化
連接池:
ADO.NET 和 ORM(如 EF Core)默認(rèn)管理數(shù)據(jù)庫連接池。
確保配置合理的
MinPoolSize和MaxPoolSize。
異步數(shù)據(jù)庫操作:
務(wù)必使用 ORM 或 ADO.NET 提供的異步方法(如
ToListAsync(),SaveChangesAsync(),ExecuteReaderAsync())來執(zhí)行數(shù)據(jù)庫查詢和命令。
優(yōu)化查詢:
- 使用索引避免全表掃描。
- 只查詢需要的字段(
Select)。 - 避免 N+1 查詢問題(EF Core 中注意使用
Include或投影)。 - 合理設(shè)計數(shù)據(jù)模型。
- 考慮讀寫分離、分庫分表(在數(shù)據(jù)量極大時)。
NoSQL 考慮:
- 對于某些特定場景(如文檔存儲、鍵值對、寬列存儲、圖數(shù)據(jù)庫),NoSQL 數(shù)據(jù)庫(如 MongoDB, Cassandra, Cosmos DB)可能比關(guān)系型數(shù)據(jù)庫(如 SQL Server, PostgreSQL)更適合高并發(fā)讀寫和水平擴展。
4. 使用鎖(lock)和互斥量(Mutex)
核心思想: 當(dāng)多個線程需要訪問共享資源(如靜態(tài)變量、單例實例、文件句柄)時,必須協(xié)調(diào)它們的訪問,防止數(shù)據(jù)損壞或狀態(tài)不一致。
常用機制:
lock語句: 最常用,基于Monitor類,提供互斥鎖。
雖然鎖在高并發(fā)場景下可能會引起性能瓶頸,但在某些情況下仍然需要使用。確保只在必要時使用,并盡可能減少鎖定范圍。
private object lockObject = new object();
public void ProcessData(int data)
{
lock (lockObject)
{
// 執(zhí)行需要同步的代碼塊
}
}樂觀鎖和Redis分布式鎖都是處理高并發(fā)場景的核心方案
樂觀鎖
樂觀鎖 通過版本號(Version)或時間戳(Timestamp)實現(xiàn)“無鎖”并發(fā)控制:
- 讀階段:讀取數(shù)據(jù)時記錄當(dāng)前版本號。
- 寫階段:提交更新前校驗版本號是否未變化,若變化則重試或失敗。
典型實現(xiàn)包括:
- 數(shù)據(jù)庫樂觀鎖:通過SQL語句(如
UPDATE ... WHERE version=old_version)。 - Redis的WATCH/MULTI:監(jiān)視Key變化,事務(wù)中執(zhí)行CAS操作
Redis分布式鎖
原理:
基于Redis的原子操作(如SET key value NX PX)實現(xiàn)互斥訪問:
- 加鎖:通過
SETNX設(shè)置唯一值并附加超時時間。 - 續(xù)期:Redisson的
Watchdog線程自動延長鎖有效期。 - 釋放:校驗持有者身份后刪除Key。
適用場景
低沖突場景:如讀多寫少的業(yè)務(wù)(商品瀏覽、配置讀?。?/p>
突發(fā)流量:秒殺系統(tǒng)中庫存扣減(配合重試機制)。
需高吞吐:避免鎖競爭,提升并發(fā)能力
樂觀鎖和Redis分布式鎖是處理高并發(fā)的互補方案而非互斥:
- 樂觀鎖:輕量、高吞吐,適合低沖突場景,需防范ABA問題。
- Redis分布式鎖:強一致、易用,需優(yōu)化主從容錯與鎖粒度。
實際系統(tǒng)中常組合使用(如Redis鎖攔截請求+數(shù)據(jù)庫樂觀鎖保證最終一致)
Monitor 類: lock 的底層實現(xiàn),提供更細粒度控制(如 TryEnter, Wait, Pulse)。
Mutex: 進程間或跨 AppDomain 的互斥鎖,重量級。
//當(dāng)前電腦只能啟動一個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("數(shù)據(jù)庫連接失敗,請檢查原因!");
}
else
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new FrmMain());
}
}
else
{
LibManager.WriteLog("WCS程序啟動失敗,與運行中WCS程序的主界面名稱重復(fù)", RecordLogTypeEnum.Error.ToString(), "");
MessageBox.Show("當(dāng)前WCS程序已啟動!");
}
}關(guān)鍵原則:
最小化鎖范圍: 只在絕對必要的地方加鎖,并盡快釋放鎖。
避免鎖嵌套: 容易導(dǎo)致死鎖。
鎖粒度: 根據(jù)共享資源的范圍選擇合適的鎖(細粒度鎖通常并發(fā)性更好)。
優(yōu)先使用并發(fā)集合: 在可能的情況下,用
ConcurrentDictionary等代替自己用鎖實現(xiàn)的集合
5. 使用線程池(ThreadPool)和Task.Run
對于需要大量并發(fā)線程的場景,可以使用Task.Run來啟動一個新任務(wù)到線程池中。
.NET 運行時維護一個預(yù)先創(chuàng)建的線程池,用于執(zhí)行后臺任務(wù)和處理異步回調(diào)。
這比手動創(chuàng)建和銷毀線程更高效。
Task[] tasks = new Task[10];
for (int i = 0; i < 10; i++)
{
tasks[i] = Task.Run(() => DoWork());
}
await Task.WhenAll(tasks);典型誤區(qū)
過度使用
Task.Run:I/O 操作直接用async而非封裝線程全局靜態(tài)鎖:導(dǎo)致無謂的線程阻塞
同步調(diào)用異步方法:
GetAwaiter().GetResult()引發(fā)死鎖忽略連接池配置:數(shù)據(jù)庫連接成為瓶頸
黃金法則:
異步化所有 I/O 路徑 + 避免共享狀態(tài) + 隊列解耦
通過負(fù)載測試(如 JMeter/LoadRunner)持續(xù)驗證系統(tǒng)極限。
6. 并發(fā)集合 (System.Collections.Concurrent)
核心思想
提供線程安全的集合類,允許多個線程安全地添加、移除或訪問集合中的元素,而無需開發(fā)者手動加鎖。
常用類
ConcurrentQueue<T>: 先進先出 (FIFO) 隊列。ConcurrentStack<T>: 后進先出 (LIFO) 棧。ConcurrentDictionary<TKey, TValue>: 鍵值對字典。ConcurrentBag<T>: 無序集合,適用于對象池等場景。BlockingCollection<T>: 提供阻塞和限制功能的集合,常用于生產(chǎn)者-消費者模式。
優(yōu)勢:
簡化多線程編程,避免鎖競爭(內(nèi)部使用高效的鎖或無鎖技術(shù)),提高并發(fā)訪問性能。
示例:
使用 ConcurrentQueue 實現(xiàn)簡單的生產(chǎn)者-消費者。
using System.Collections.Concurrent; private static readonly ConcurrentDictionary<Type, string> assemblyQualifiedNameCache = new ConcurrentDictionary<Type, string>();
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
C#/VB.NET實現(xiàn)在PDF文檔中插入,替換或刪除圖片
這篇文章主要為大家詳細介紹了如何使用 Spire.PDF for .NET 通過程序在 PDF 文檔中插入、替換或刪除圖片,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-12-12
C# WPF實現(xiàn)讀寫CAN數(shù)據(jù)
這篇文章主要介紹了C# WPF實現(xiàn)讀寫CAN數(shù)據(jù),文中通過代碼示例給大家講解的非常詳細,對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-06-06
C#對XtraGrid控件實現(xiàn)主從表關(guān)系綁定
這篇文章介紹了C#對XtraGrid控件實現(xiàn)主從表關(guān)系綁定的方法,文中通過示例代碼介紹的非常詳細。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-06-06
c#判斷網(wǎng)絡(luò)連接狀態(tài)的示例分享
這篇文章主要介紹了使用c#判斷網(wǎng)絡(luò)連接狀態(tài)的示例,需要的朋友可以參考下2014-02-02

