编辑
2026-01-16
C#
00

目录

💥 你是否遇到过这样的"灾难"?
🎯 问题核心:为什么会出现重复编号?
🛠️ 解决方案一:数据库约束 + 重试机制(⭐推荐)
🔧 首先添加数据库约束
💻 核心代码实现
🔐 解决方案二:分布式锁(适合高并发)
⚡ 解决方案三:数据库原子操作(性能最佳)
📊 方案对比与选择建议
💭 实战经验分享
🎯 总结:选对方案,一劳永逸

💥 你是否遇到过这样的"灾难"?

想象一下:你辛辛苦苦开发了一套WMS系统,用户在高峰期批量入库时,突然发现同一个箱号被生成了两次!数据库报错、业务逻辑混乱、用户投诉不断... 这种并发环境下生成唯一编号的问题,几乎每个C#开发者都会遇到。

今天就来彻底解决这个让人头疼的技术难题,3种经过生产验证的解决方案**,从简单到复杂,总有一种适合你的项目!

image.png

🎯 问题核心:为什么会出现重复编号?

在多线程或分布式环境中,传统的"查询最大值+1"方案存在经典的竞态条件

c#
// 危险的传统做法 ❌ public string GenerateBoxNo() { // 线程A和B同时执行到这里 var maxNo = GetMaxBoxNo(); // 都获得相同的最大值 return IncrementBoxNo(maxNo); // 生成相同的新编号! }

问题根源:操作不是原子性的,存在时间间隙让并发请求"钻空子"。

🛠️ 解决方案一:数据库约束 + 重试机制(⭐推荐)

这是最简单有效的方案,利用数据库的ACID特性来保证唯一性。

🔧 首先添加数据库约束

sql
-- 为 box_no 字段添加唯一索引 ALTER TABLE PdsBox ADD CONSTRAINT UK_PdsBox_BoxNo UNIQUE (box_no);

💻 核心代码实现

c#
public async Task<bool> InBox(InBoxAndLabel input) { const int maxRetries = 3; int retryCount = 0; // 实际业务中我基本不会这么干,只是简单的设置主键应对大多数场景了 while (retryCount < maxRetries) { try { return await ProcessInBox(input); } catch (Exception ex) when (IsUniqueConstraintViolation(ex)) { retryCount++; if (retryCount >= maxRetries) throw new InvalidOperationException("生成唯一编号失败,请稍后重试"); // 指数退避算法,避免重试风暴 await Task.Delay(50 * retryCount + new Random().Next(0, 100)); } } return false; } private async Task<bool> ProcessInBox(InBoxAndLabel input) { PdsBox box = new PdsBox(); if (string.IsNullOrEmpty(input.box.box_no)) { box = input.box; box.Id = SnowFlakeSingle.instance.NextId(); box.box_no = await MaxFinalBoxAsync(); // 可能冲突的地方 await _rep.InsertAsync(box); // 数据库约束在此处生效 } return true; } private bool IsUniqueConstraintViolation(Exception ex) { // 支持主流数据库的约束违反判断 return ex.Message.Contains("UNIQUE constraint") || ex.Message.Contains("Duplicate entry") || ex.Message.Contains("违反了 UNIQUE KEY 约束") || ex.Message.Contains("duplicate key value"); }

💡 优势:实现简单,数据库层面保证一致性,适用于大部分场景

⚠️ 注意:需要合理设置重试次数和延迟,避免性能问题

🔐 解决方案二:分布式锁(适合高并发)

当重试成本较高时,使用锁机制确保串行生成,我实际业务中这个用的比较多。

c#
private static readonly SemaphoreSlim _boxNoSemaphore = new SemaphoreSlim(1, 1); public async Task<bool> InBox(InBoxAndLabel input) { PdsBox box = new PdsBox(); if (string.IsNullOrEmpty(input.box.box_no)) { // 关键:整个生成和插入过程都在锁内 await _boxNoSemaphore.WaitAsync(); try { box = input.box; box.Id = SnowFlakeSingle.instance.NextId(); box.box_no = MaxFinalBox(); // 在锁内生成 await _rep.InsertAsync(box); // 在锁内插入 } finally { _boxNoSemaphore.Release(); } } return true; }

⚡ 解决方案三:数据库原子操作(性能最佳)

直接利用数据库的原子性,避免应用层的复杂逻辑,这种事务用的也比较多。

c#
public async Task<string> MaxFinalBoxAsync() { string datePrefix = DateTime.Now.ToString("yyMMdd") + "BOX"; using var tran = await _rep.Ado.BeginTranAsync(); try { // 使用 SELECT FOR UPDATE 锁定行 var maxBoxNo = await _rep.AsSugarClient().Ado.SqlQuerySingleAsync<string>(@" SELECT box_no FROM PdsBox WHERE box_no LIKE @prefix ORDER BY box_no DESC LIMIT 1 FOR UPDATE", new { prefix = datePrefix + "%" } ); string newBoxNo; if (string.IsNullOrEmpty(maxBoxNo)) { newBoxNo = datePrefix + "001"; } else { int sequence = int.Parse(maxBoxNo.Substring(maxBoxNo.Length - 3)) + 1; newBoxNo = datePrefix + sequence.ToString("D3"); } await tran.CommitAsync(); return newBoxNo; } catch { await tran.RollbackAsync(); throw; } }

💡 优势:性能好,并发安全,事务保证

⚠️ 注意:FOR UPDATE 可能造成锁等待,需要监控性能

📊 方案对比与选择建议

方案适用场景优势劣势推荐指数
数据库约束+重试中小型系统简单可靠有重试开销⭐⭐⭐⭐⭐
分布式锁高并发系统零冲突性能瓶颈⭐⭐⭐⭐
数据库原子操作读多写少性能好实现复杂⭐⭐⭐⭐

💭 实战经验分享

🚨 常见踩坑点:

  1. 忘记处理时区问题:跨时区部署时,日期前缀可能不一致
  2. 序列号溢出:单日超过999个编号时的处理逻辑
  3. 数据库连接池不足:高并发时连接耗尽

✨ 性能优化技巧:

  • 使用连接池和异步操作
  • 合理设置重试间隔,避免"雪崩"
  • 定期清理过期的序列记录

🎯 总结:选对方案,一劳永逸

对于并发生成唯一编号这个经典问题,关键在于:

  1. 小项目优先选择"数据库约束+重试",简单可靠
  2. 中型项目考虑"专用序列表",为未来扩展留空间
  3. 高并发场景必须用"分布式锁",保证性能和一致性

记住:没有银弹,只有最适合的方案。根据你的业务规模、团队技术栈和维护成本来选择。


💬 你在项目中是如何处理并发生成唯一编号的?遇到过什么奇葩的bug?

欢迎在评论区分享你的实战经验,让我们一起交流学习!觉得这篇文章对你有帮助,请点赞并转发给更多的C#同行 👍

关注我,获取更多C#开发实战技巧和最佳实践分享!

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!