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

.NET Core 6.0+SQLite+EF Core百万数据快速同步优化求助

优化.NET Core Web API + SQLite同步100万条数据的最简方案

一、先解决用户无感知问题:后台异步执行同步

你的核心痛点是用户被迫等待同步完成,必须把同步逻辑从请求处理线程剥离,让请求立即返回,同步在后台静默执行。之前的ThreadPool方案存在DbContext生命周期问题(Scoped的DbContext不能在请求上下文外直接复用),以下是正确的最简实现:

1. 用BackgroundService实现后台同步任务

创建后台服务类,通过Channel接收同步请求,避免并发冲突:

public class SyncCitiesBackgroundService : BackgroundService
{
    private readonly Channel<bool> _syncRequests;
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly IHttpClientFactory _httpClientFactory;

    public SyncCitiesBackgroundService(IServiceScopeFactory scopeFactory, IHttpClientFactory httpClientFactory)
    {
        _scopeFactory = scopeFactory;
        _httpClientFactory = httpClientFactory;
        _syncRequests = Channel.CreateUnbounded<bool>(new UnboundedChannelOptions { SingleReader = true });
    }

    // 外部触发同步的入口方法
    public async Task TriggerSync()
    {
        await _syncRequests.Writer.WriteAsync(true);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var _ in _syncRequests.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                await PerformFullSync(stoppingToken);
            }
            catch (Exception ex)
            {
                // 此处添加日志记录逻辑
            }
        }
    }

    private async Task PerformFullSync(CancellationToken stoppingToken)
    {
        // 创建独立Scope,安全获取Scoped服务(如DbContext)
        using var scope = _scopeFactory.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<YourDbContext>();

        var httpClient = _httpClientFactory.CreateClient("Cities");
        var response = await httpClient.GetAsync("API_KEY_WITH_SOME_CREDS", stoppingToken);
        if (!response.IsSuccessStatusCode) return;

        using var contentStream = await response.Content.ReadAsStreamAsync(stoppingToken);
        // 优化JSON反序列化配置
        var jsonOptions = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true,
            IgnoreNullValues = true,
            AllowTrailingCommas = true
        };
        var result = await JsonSerializer.DeserializeAsync<Result>(contentStream, jsonOptions, stoppingToken);

        var datums = result?.Data ?? Enumerable.Empty<DatumEntity>();
        if (!datums.Any()) return;

        await EfficientBulkInsert(context, datums, stoppingToken);
    }

    private async Task EfficientBulkInsert(YourDbContext context, IEnumerable<DatumEntity> datums, CancellationToken stoppingToken)
    {
        // 1. 清空目标表(全量同步场景)
        await context.Database.ExecuteSqlRawAsync("DELETE FROM Datums;", stoppingToken);
        await context.SaveChangesAsync(stoppingToken);

        // 2. 分批次执行原生SQL插入(每1000条一批,避免单条SQL过长)
        var batchSize = 1000;
        var batches = datums.Chunk(batchSize);

        using var transaction = await context.Database.BeginTransactionAsync(stoppingToken);
        try
        {
            foreach (var batch in batches)
            {
                // 动态构建INSERT语句(替换为你的实体列名)
                var columns = string.Join(", ", nameof(DatumEntity.Id), nameof(DatumEntity.CityName), nameof(DatumEntity.Population));
                var values = string.Join(", ", batch.Select(e => 
                    $"({e.Id}, '{e.CityName.Replace("'", "''")}', {e.Population})"));
                var sql = $"INSERT INTO Datums ({columns}) VALUES {values};";

                await context.Database.ExecuteSqlRawAsync(sql, stoppingToken);
            }
            await transaction.CommitAsync(stoppingToken);
        }
        catch
        {
            await transaction.RollbackAsync(stoppingToken);
            throw;
        }
    }
}

在Program.cs中注册该后台服务:

builder.Services.AddHostedService<SyncCitiesBackgroundService>();

2. 修改SyncCities方法,触发同步后立即返回

public async Task<IEnumerable<DatumEntity>> SyncCities([FromServices] SyncCitiesBackgroundService syncService)
{
    // 触发后台同步,不等待完成
    _ = syncService.TriggerSync();
    
    // 返回本地已有数据,让用户立即得到响应
    return await _context.Datums.AsNoTracking().ToListAsync();
}

这样用户请求会瞬间返回,完全感知不到同步过程。

二、优化SQLite写入性能,达到1秒内目标

SQLite的写入瓶颈主要在磁盘IO和EF的额外开销,以下是关键优化:

  • 永久开启WAL模式:在DbContext配置中设置连接字符串,无需每次同步重复配置:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseSqlite("Data Source=your_db.db;Cache=Shared;Journal Mode=WAL;Synchronous=NORMAL");
}

WAL模式允许SQLite读写并发,减少磁盘刷新次数,可将写入性能提升数倍。

  • 使用原生SQL批量插入:EF Core的BulkInsert扩展在SQLite上优化有限,直接生成原生INSERT语句可跳过EF的实体跟踪、验证逻辑,大幅提升速度。

  • 流式处理数据:如果100万条数据内存占用过高,可使用JsonSerializer.DeserializeAsyncEnumerable实现边读边插入,避免一次性加载所有数据到内存:

var datums = JsonSerializer.DeserializeAsyncEnumerable<DatumEntity>(contentStream, jsonOptions, stoppingToken);
await foreach (var datum in datums.WithCancellation(stoppingToken))
{
    // 分批次累积数据后插入
}

三、额外优化建议

  • 增量同步:如果远程API支持按时间戳/ID过滤,只同步上次更新后的数据,比如请求时带上lastSyncTime参数,可大幅减少数据量。
  • 索引优化:同步前临时禁用目标表的非主键索引,完成后重建,避免插入时频繁更新索引的开销。

内容的提问来源于stack exchange,提问作者Mahmoud Kinawy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 04:40:54