You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 02:50:33