ASP.NET Core EF Code-First中实现无重复递增库存请求编号的方案咨询
实现数据库唯一且基于最新记录递增的请求编号方案
问题分析
当前模型里的静态变量_nextSequenceNumber存在致命问题:它是应用域级别的内存变量,应用重启、多实例部署(集群/负载均衡)时会重置或出现多实例各自计数,必然导致重复编号;而且完全没有读取数据库现有记录的最大编号,生成的序列和数据库实际状态完全脱节。
核心实现思路
要保证编号唯一且正确递增,必须从数据库获取当前最新的序列值,而非依赖内存中的静态变量。同时要处理并发场景下的冲突(比如两个请求同时读取到相同最大值,进而生成重复编号)。
具体实现方案
方案1:数据库查询+事务(中小并发场景适用)
在创建新记录前,先查询数据库中当前年月下的最大序列编号,加1后生成新编号,并用事务包裹查询和插入操作,保证原子性。
示例代码(基于EF Core):
public async Task<InventoryRequest> CreateInventoryRequestAsync(InventoryRequest newRequest) { const string fixedString = "31751/PL.711/"; var currentMonth = DateTime.Today.Month; var currentYear = DateTime.Today.Year; // 开启事务,确保查询和插入操作原子性 using var transaction = await _dbContext.Database.BeginTransactionAsync(); try { // 查询当前年月下的最新请求记录 var latestRequest = await _dbContext.InventoryRequests .Where(r => r.RequestNumber != null && r.RequestNumber.StartsWith("B-") && r.RequestNumber.EndsWith($"/{fixedString}{currentMonth}/{currentYear}")) .OrderByDescending(r => r.RequestNumber) .FirstOrDefaultAsync(); int nextSeq = 1; if (latestRequest != null) { // 从现有编号中解析出序列值:比如"B-005/..." 提取005转为int var seqSegment = latestRequest.RequestNumber.Split('/')[0].Split('-')[1]; if (int.TryParse(seqSegment, out int lastSeq)) { nextSeq = lastSeq + 1; } } // 生成新编号 newRequest.RequestNumber = $"B-{nextSeq:D3}/{fixedString}{currentMonth}/{currentYear}"; newRequest.RequestDate = DateTime.Today; _dbContext.InventoryRequests.Add(newRequest); await _dbContext.SaveChangesAsync(); await transaction.CommitAsync(); return newRequest; } catch { await transaction.RollbackAsync(); throw; } }
注意:
- 按年月筛选查询,避免跨年月的序列干扰(比如去年12月最大编号为100,今年1月应从1开始计数)
- 事务能大幅降低并发场景下的重复概率
方案2:数据库序列(高并发场景适用)
如果使用SQL Server、PostgreSQL等支持序列(Sequence)的数据库,可以让数据库负责生成唯一递增的序列值,从根源避免冲突。
以SQL Server为例:
- 创建全局序列(或按年月创建独立序列):
CREATE SEQUENCE InventoryRequestSeq START WITH 1 INCREMENT BY 1 NO CYCLE;
- 在EF Core中调用序列生成值:
public async Task<InventoryRequest> CreateInventoryRequestAsync(InventoryRequest newRequest) { const string fixedString = "31751/PL.711/"; var currentMonth = DateTime.Today.Month; var currentYear = DateTime.Today.Year; // 从数据库获取下一个序列值 var nextSeq = await _dbContext.Database.SqlQuery<int>("SELECT NEXT VALUE FOR InventoryRequestSeq").FirstAsync(); newRequest.RequestNumber = $"B-{nextSeq:D3}/{fixedString}{currentMonth}/{currentYear}"; newRequest.RequestDate = DateTime.Today; _dbContext.InventoryRequests.Add(newRequest); await _dbContext.SaveChangesAsync(); return newRequest; }
若需要按年月重置序列,可在每月初执行重置脚本,或创建动态命名的序列(比如InventoryRequestSeq_202409),每月自动生成新序列。
方案3:唯一约束兜底
无论采用哪种方案,都要给RequestNumber字段添加数据库唯一约束,防止极端情况下的重复数据入库:
在EF Core模型配置中添加:
modelBuilder.Entity<InventoryRequest>() .HasIndex(r => r.RequestNumber) .IsUnique();
这样即使出现意外情况,数据库会直接抛出唯一约束冲突异常,阻止重复数据写入。
必须废弃原构造函数逻辑
原构造函数中的静态变量逻辑完全不适合多实例、持久化场景,会导致大量重复编号问题,必须删除该部分代码。
内容的提问来源于stack exchange,提问作者Haminteu
相关产品推荐
相关产品推荐

