.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
相关产品推荐
相关产品推荐

