如何通过EF Core结合SQL数据库实现无重复无间隙的文档连续编号?
问题原因分析
你遇到的重复编号问题,核心在于先读后改的操作并非原子性,即便使用Serializable隔离级别,也无法完全避免并发场景下的竞态条件:
- Serializable隔离级别主要防止幻读、不可重复读,但对于先读取行到内存、再修改内存值最后提交的流程,多个并发事务仍可能同时读取到相同的
LastNumber值(因为初始查询仅加共享锁,多个事务可同时获取)。 - 当多个事务都完成内存中的递增操作后,提交时可能覆盖彼此的修改,最终导致重复编号。
解决方案
方案1:数据库端原子递增(推荐)
直接通过数据库的原子更新语句完成编号递增与读取,彻底消除竞态条件,无需依赖事务隔离级别:
var documentTypeId = 1; // 原子执行递增并返回新编号 var newDocNumber = dbContext.Database.ExecuteSqlScalar<int>( "UPDATE DocumentTypes SET LastNumber = LastNumber + 1 OUTPUT INSERTED.LastNumber WHERE Id = {0}", documentTypeId ); // 创建新文档 dbContext.Documents.Add(new Document { Id = newDocNumber, // 其他字段赋值 }); dbContext.SaveChanges();
这种方式利用数据库原生的原子操作,确保同一时间只有一个请求能完成递增,完全避免重复。
方案2:显式行锁定+Serializable事务
如果坚持使用EF Core实体操作,需在读取DocumentType时显式添加排他锁,阻止其他并发事务读取该行:
using var transaction = dbContext.Database.BeginTransaction(IsolationLevel.Serializable); // 使用UPDLOCK(更新锁)+ HOLDLOCK(保持锁到事务结束)强制锁定目标行 var documentType = dbContext.DocumentTypes .FromSqlRaw("SELECT * FROM DocumentTypes WITH (UPDLOCK, HOLDLOCK) WHERE Id = {0}", 1) .Single(); documentType.LastNumber++; documentType.Documents.Add(new Document { Id = documentType.LastNumber, // 其他字段赋值 }); dbContext.SaveChanges(); transaction.Commit();
UPDLOCK:给行加更新锁,确保其他事务无法获取该行的共享锁或更新锁,只能等待当前事务释放锁。HOLDLOCK:将锁的持有时间延长至事务结束,避免锁提前释放导致并发问题。
额外注意事项
- 确保事务使用
using语句管理,避免异常情况下锁未释放导致死锁。 - 若使用异步方法,需搭配
await关键字(如await dbContext.Database.BeginTransactionAsync(...)、await dbContext.SaveChangesAsync()),否则可能出现事务管理混乱。
内容的提问来源于stack exchange,提问作者Jimbo
相关产品推荐
相关产品推荐

