C#异步并行读写Redis与SQL Server无性能差异问题排查
异步并行与同步读写性能无差异的问题排查
问题背景
使用C# async/await实现了从Redis批量读取数据并写入SQL Server的逻辑,在TeamController类中实现了两个方法用于性能对比:
SaveDataParallel:异步并行处理分块数据SaveDataWithSimple:同步处理分块数据
原本预期异步并行方法的耗时会远低于同步方法,但实际测试中两者性能无显著差异,需要排查原因。
代码实现
[Route("api/[controller]")] [ApiController] public class TeamController : ControllerBase { private ICacheManager cacheManager; private IDBManager dbManager; private IDomainDataConverter _domainDataConverter; public TeamController(ICacheManager cacheManager, IDBManager dbManager, IDomainDataConverter domainDataConverter) { this.cacheManager = cacheManager; this.dbManager = dbManager; this._domainDataConverter = domainDataConverter; } [HttpPost, Route("SaveDataParallel")] public async Task<IActionResult> SaveDataParallel(int parallelDegree, int totalCount) { int chunkeSize = totalCount / parallelDegree; int remainder = totalCount - chunkeSize * parallelDegree; System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch(); st.Start(); try { var tasks = new List<Task>(); for (int i = 0; i < parallelDegree; i++) { tasks.Add(SaveChunkAsync(i, chunkeSize, parallelDegree, remainder)); } await Task.WhenAll(tasks); st.Stop(); } catch { } return Ok(st.ElapsedMilliseconds); } [HttpPost, Route("SaveDataSimple")] public IActionResult SaveDataWithSimple(int parallelDegree, int totalCount) { int chunkeSize = totalCount / parallelDegree; int remainder = totalCount - chunkeSize * parallelDegree; System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch(); st.Start(); try { for (int i = 0; i < parallelDegree; i++) { SaveChunk(i, chunkeSize, parallelDegree, remainder); } st.Stop(); } catch (Exception ex) { } return Ok(st.ElapsedMilliseconds); } private async Task SaveChunkAsync(int i, int pageSize, int parallelDegree, int remainder) { var data = cacheManager.ReadDataAsync<TeamDto>(i * pageSize, (i == parallelDegree - 1 ? remainder : 0) + pageSize); var arr = _domainDataConverter.Convert<Team, TeamDto>(data.Result); await dbManager.BulkInsertAsync(arr); } private void SaveChunk(int i, int pageSize, int parallelDegree, int remainder) { var data = cacheManager.ReadData<TeamDto>(i * pageSize, (i == parallelDegree - 1 ? remainder : 0) + pageSize); var arr = _domainDataConverter.Convert<Team, TeamDto>(data); dbManager.BulkInsert(arr); } }
预期与实际差异
预期耗时公式
- 异步并行方法:
tparallel = (max(td, tr) * n) / 2 + tr - 同步方法:
tsimple = (max(td, tr)) * n
其中: n:读写总次数td:SQL写入单块数据耗时tr:Redis读取单块数据耗时
实际结果
异步并行与同步方法的性能无显著差异,不符合预期。
排查思路与修复指导
1. 异步方法中的同步阻塞问题(核心问题)
SaveChunkAsync方法中直接调用data.Result会同步阻塞当前线程,导致异步方法失去非阻塞特性,和同步方法的执行逻辑本质一致:
// 错误写法:同步阻塞等待异步操作完成 var data = cacheManager.ReadDataAsync<TeamDto>(...); var arr = _domainDataConverter.Convert<Team, TeamDto>(data.Result);
修复方式:用await替代Result,让异步操作真正非阻塞执行:
private async Task SaveChunkAsync(int i, int pageSize, int parallelDegree, int remainder) { var data = await cacheManager.ReadDataAsync<TeamDto>(i * pageSize, (i == parallelDegree - 1 ? remainder : 0) + pageSize); var arr = _domainDataConverter.Convert<Team, TeamDto>(data); await dbManager.BulkInsertAsync(arr); }
2. 验证依赖组件的异步实现真实性
检查ICacheManager.ReadDataAsync和IDBManager.BulkInsertAsync是否为真正的异步非阻塞实现:
- 如果这些方法只是用
Task.Run包裹同步代码(伪异步),并行执行时无法释放线程,无法获得性能提升 - 查看组件源码或文档,确认其异步API是否基于IOCP(输入输出完成端口)实现
3. 排查资源瓶颈
- SQL Server端:检查批量插入的性能瓶颈,比如:
- 是否开启了批量插入的优化(如禁用触发器、事务日志模式设置为简单/大容量日志)
- 数据库的磁盘IO、CPU是否达到瓶颈,并行写入是否引发锁竞争
- Redis端:检查Redis的QPS(每秒查询率)是否达到上限,并行读取是否导致Redis服务器过载
4. 修正计时逻辑的潜在问题
当前代码中,若异步方法执行过程中抛出异常,st.Stop()不会被调用,导致返回的计时结果无效。需调整计时逻辑,确保无论是否发生异常都能停止计时器:
[HttpPost, Route("SaveDataParallel")] public async Task<IActionResult> SaveDataParallel(int parallelDegree, int totalCount) { int chunkeSize = totalCount / parallelDegree; int remainder = totalCount - chunkeSize * parallelDegree; System.Diagnostics.Stopwatch st = new System.Diagnostics.Stopwatch(); st.Start(); try { var tasks = new List<Task>(); for (int i = 0; i < parallelDegree; i++) { tasks.Add(SaveChunkAsync(i, chunkeSize, parallelDegree, remainder)); } await Task.WhenAll(tasks); } catch { // 可添加异常日志逻辑 } finally { st.Stop(); // 确保计时器一定会停止 } return Ok(st.ElapsedMilliseconds); }
5. 调整并行度参数
- 若并行度设置过小,无法充分利用系统资源;若设置过大,会引发线程上下文切换开销、数据库连接池耗尽等问题
- 建议逐步调整
parallelDegree参数(如从2、4、8开始测试),找到最优并行度
内容的提问来源于stack exchange,提问作者Shahrzad Abedi
相关产品推荐
相关产品推荐

