并行处理与Entity Framework:锁定含EntityState.Added状态的实体
核心问题根源
Entity Framework 中的DbContext在设计上就是非线程安全的,你直接在Parallel.ForEach中共享同一个上下文实例,哪怕对上下文本身加锁,也无法避免EF内部状态管理的逻辑出现不可预期的状态冲突,这是你之前加锁方案不生效的核心原因。
可行的两种解决方案
方案1:保留单上下文结构,修复锁逻辑
不要锁EF提供的上下文、DbSet、ObjectStateManager这类内部对象,这些对象的内部实现可能会变更锁状态,可靠性极低,你应该使用独立的锁对象保证「检查-新增」操作的原子性,还可以搭配线程安全的缓存提升性能:
- 定义独立的锁对象和缓存字典:
// 独立锁对象,不要和任何EF内部对象复用 private static readonly object _guidMappingLock = new object(); // 线程安全缓存,避免每次都遍历EF的状态管理器,性能更高 private static readonly ConcurrentDictionary<string, Guid> _identGuidCache = new ConcurrentDictionary<string, Guid>();
- 修改查询和新增逻辑,保证整个操作在锁内完成:
private static GuidMapping GetOrCreateGuidMapping(MappingEntities context, string itemIdent) { // 先查缓存,命中直接返回 if (_identGuidCache.TryGetValue(itemIdent, out var existedGuid)) { return new GuidMapping { ItemIdent = itemIdent, Guid = existedGuid }; } lock (_guidMappingLock) { // 锁内二次检查缓存,避免多线程等待时已被其他线程新增 if (_identGuidCache.TryGetValue(itemIdent, out existedGuid)) { return new GuidMapping { ItemIdent = itemIdent, Guid = existedGuid }; } // 查数据库已有记录 var dbExisted = context.GuidMapping.FirstOrDefault(m => m.ItemIdent == itemIdent); if (dbExisted != null) { _identGuidCache.TryAdd(itemIdent, dbExisted.Guid); return dbExisted; } // 查内存中已添加未保存的记录 var addedExisted = ((IObjectContextAdapter)context) .ObjectContext .ObjectStateManager .GetObjectStateEntries(EntityState.Added) .Where(ent => ent.Entity is GuidMapping) .Select(ent => ent.Entity as GuidMapping) .FirstOrDefault(m => m.ItemIdent == itemIdent); if (addedExisted != null) { _identGuidCache.TryAdd(itemIdent, addedExisted.Guid); return addedExisted; } // 都不存在则创建新记录 var newMapping = new GuidMapping { ItemIdent = itemIdent, Guid = Guid.NewGuid() }; context.GuidMapping.Add(newMapping); _identGuidCache.TryAdd(itemIdent, newMapping.Guid); return newMapping; } }
方案2:规避单上下文多线程问题(更推荐)
DbContext本身是轻量级对象,创建销毁开销极低,你可以给并行处理的每个线程单独创建独立的上下文实例,最后汇总所有生成的GuidMapping记录,全局去重后再统一批量插入:
- 并行处理阶段每个线程自己查自己的上下文,不需要任何锁,性能更高
- 全部处理完成后把所有生成的映射记录按
ItemIdent去重,再用单上下文批量插入到数据库,完全避免并发冲突
额外注意事项
无论用哪种方案,最后调用SaveChanges之前都建议再做一次全局的ItemIdent去重校验,避免边缘场景下的重复数据问题。
内容的提问来源于stack exchange,提问作者CarenRose
相关产品推荐
相关产品推荐

