EF Core中并发插入时的唯一约束冲突问题
我之前处理过类似Lambda+EF Core+Postgres的并发数据导入场景,这个唯一约束冲突问题其实挺常见的,咱们先理清楚根源,再一步步解决:
问题根源分析
- DbContext线程不安全:EF Core的
DbContext本身不是线程安全的,如果在Parallel.ForEach里共享同一个DbContext实例,会导致数据状态混乱,甚至触发并发冲突。 - 并发操作无原子性保障:多个并行线程(或不同Lambda实例)同时处理带有相同唯一键的记录时,都尝试插入就会触发唯一约束;即使是先查后更的逻辑,也会因为查询和更新之间的时间窗口导致竞态条件。
- 并行度过高:
Parallel.ForEach默认的并行度可能超出Postgres的承受能力,导致大量并发请求同时修改数据,加剧冲突概率。
具体解决方案
1. 给每个并行线程分配独立的DbContext
这是基础中的基础,绝对不能在多线程间共享DbContext。在Parallel.ForEach的循环体内创建DbContext实例:
Parallel.ForEach(records, new ParallelOptions { MaxDegreeOfParallelism = 4 }, record => { // 每个线程单独创建DbContext,用完自动释放 using (var context = new YourPostgresDbContext()) { // 这里处理单条记录的插入/更新逻辑 } });
2. 使用Postgres原生UPSERT操作(最推荐)
Postgres支持INSERT ... ON CONFLICT语法,这是数据库层面的原子操作,能完美避免并发插入/更新的唯一约束冲突。在EF Core 2.0中可以直接执行原生SQL:
using (var context = new YourPostgresDbContext()) { var sql = @"INSERT INTO your_target_table (unique_key_col, col1, col2) VALUES ({0}, {1}, {2}) ON CONFLICT (unique_key_col) DO UPDATE SET col1 = EXCLUDED.col1, col2 = EXCLUDED.col2"; context.Database.ExecuteSqlInterpolated( sql, record.UniqueKey, record.Col1, record.Col2 ); }
这种方式跳过了EF Core的变更追踪,直接用数据库的原子操作保障一致性,性能和可靠性都拉满。
3. 给冲突操作添加重试机制
如果必须用EF Core的实体操作而非原生SQL,可以捕获Postgres的唯一约束异常(错误码23505),并通过重试来解决临时冲突。比如用Polly库实现重试:
// 定义重试策略:捕获Postgres唯一约束异常,最多重试3次,每次间隔指数递增 var retryPolicy = Policy .Handle<Npgsql.PostgresException>(ex => ex.SqlState == "23505") .WaitAndRetry(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); Parallel.ForEach(records, new ParallelOptions { MaxDegreeOfParallelism = 4 }, record => { using (var context = new YourPostgresDbContext()) { retryPolicy.Execute(() => { var existingRecord = context.YourEntities.FirstOrDefault(e => e.UniqueKey == record.UniqueKey); if (existingRecord != null) { // 更新现有记录 existingRecord.Col1 = record.Col1; existingRecord.Col2 = record.Col2; } else { // 插入新记录 context.YourEntities.Add(record); } context.SaveChanges(); }); } });
注意:EF Core 2.0没有内置的AddOrUpdate方法,需要自己实现先查后更的逻辑,配合重试来规避竞态条件。
4. 合理控制并行度
不要让Parallel.ForEach的并发数太高,建议根据Postgres的连接池配置(默认是100)和服务器性能来调整MaxDegreeOfParallelism,比如设置为4-8之间,避免数据库被打满。
5. 优化文件分割逻辑
检查你的分割代码,确保带有相同唯一键的记录被分到同一个小文件中,这样就不会出现多个Lambda函数同时处理同一唯一键记录的情况,从根源减少冲突概率。比如分割前先按唯一键分组,再按组拆分文件。
总结
优先推荐用Postgres的UPSERT原生SQL,这是解决并发唯一约束冲突最可靠的方式;同时必须保证每个并行线程使用独立的DbContext,合理控制并行度,必要时添加重试机制。
内容的提问来源于stack exchange,提问作者lmerry213

