EF Core 6如何实现字母数字属性自增解决并发重复问题
问题根因
你当前的实现逻辑是先查询全表交易记录总数,再基于总数拼接生成编码,整个过程没有任何并发控制:多个并发请求会同时读到相同的计数值,最终生成重复的ReferenceCode,且没有数据库层面的唯一约束兜底,重复数据会直接写入库中。
以下是适配EF Core 6 + PostgreSQL Code First模式的可靠落地方案,按推荐优先级排序:
方案1:PostgreSQL原生序列+计算列(首推,性能最高,无并发冲突)
PostgreSQL的序列对象本身是数据库层面实现的原子递增结构,并发请求调用序列取值时永远不会拿到重复值,配合计算列可以完全把编码生成逻辑下沉到数据库,不需要业务代码写计数逻辑。
实现步骤
在你的DbContext的OnModelCreating方法中做如下配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 定义交易编码专用序列,从1开始递增,步长为1 modelBuilder.HasSequence<int>("TransactionRefSequence") .StartsAt(1) .IncrementsBy(1); // 配置ReferenceCode为数据库自动生成的计算列 modelBuilder.Entity<Transaction>() .Property(t => t.ReferenceCode) // 自动拼接TR前缀,按5位数字补零,可根据需要调整补零位数(比如你之前用的10位就填FM0000000000) .HasComputedColumnSql("'TR' || to_char(nextval('\"TransactionRefSequence\"'), 'FM00000')", stored: true) .IsRequired(); // 兜底:给ReferenceCode加唯一索引,极端情况也不会插入重复值 modelBuilder.Entity<Transaction>() .HasIndex(t => t.ReferenceCode) .IsUnique(); }
注意事项
- 业务代码创建
Transaction实体时,不需要手动给ReferenceCode赋值,数据库在插入记录时会自动生成符合格式的编码,SaveChanges完成后EF Core会自动把生成的编码回填到实体属性中。 - 序列特性决定了如果插入事务回滚,已经取出的序列值不会回退,可能出现编码跳号的情况,这对交易类业务属于正常现象(交易单号本身不要求严格连续,仅要求唯一可追溯)。
- 该方案无锁等待,并发写入性能远高于业务层计数方案。
方案2:序号跟踪表+行级排他锁(适用于要求编码严格连续的场景)
如果你的业务要求交易编码必须严格连续、不允许跳号,可以用独立序号表加数据库行锁的方式保证取值原子性。
实现步骤
- 先定义序号跟踪实体:
public class TransactionSequence { public int Id { get; set; } public int CurrentValue { get; set; } }
- 配置表结构后,预先插入一条初始化记录,
CurrentValue设为0。 - 生成编码、创建交易的逻辑放到事务中执行:
using var dbTransaction = await applicationDbContext.Database.BeginTransactionAsync(); // 加FOR UPDATE排他锁查询当前序号,查询期间其他并发请求会阻塞等待锁释放,不会读到相同值 int nextValue = await applicationDbContext.Database .SqlQueryRaw<int>("SELECT \"CurrentValue\" + 1 FROM \"TransactionSequence\" FOR UPDATE") .FirstAsync(); // 更新序号值 await applicationDbContext.Database.ExecuteSqlRawAsync( "UPDATE \"TransactionSequence\" SET \"CurrentValue\" = {0}", nextValue ); // 拼接生成编码 string referenceCode = "TR" + nextValue.ToString("00000"); // 按业务逻辑创建Transaction实体,给ReferenceCode赋值 var transaction = new Transaction(/* 构造参数传referenceCode */); applicationDbContext.Transactions.Add(transaction); await applicationDbContext.SaveChangesAsync(); await dbTransaction.CommitAsync();
注意事项
- 该方案能保证编码严格连续,但并发写入时会因为锁等待降低吞吐量,仅适合并发量不高、对连续性有强要求的场景。
- 同样需要给
ReferenceCode配置唯一索引做兜底。
不推荐的方案
不要尝试用应用层内存锁、分布式锁解决该问题:锁本身会增加系统复杂度,一旦锁失效还是会出现重复编码,且性能不如数据库原生方案。
内容的提问来源于stack exchange,提问作者Durga Prasad
相关产品推荐
相关产品推荐

