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

如何通过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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 13:38:32