想象一下:你辛辛苦苦开发了一套WMS系统,用户在高峰期批量入库时,突然发现同一个箱号被生成了两次!数据库报错、业务逻辑混乱、用户投诉不断... 这种并发环境下生成唯一编号的问题,几乎每个C#开发者都会遇到。
今天就来彻底解决这个让人头疼的技术难题,3种经过生产验证的解决方案**,从简单到复杂,总有一种适合你的项目!

在多线程或分布式环境中,传统的"查询最大值+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 可能造成锁等待,需要监控性能
| 方案 | 适用场景 | 优势 | 劣势 | 推荐指数 |
|---|---|---|---|---|
| 数据库约束+重试 | 中小型系统 | 简单可靠 | 有重试开销 | ⭐⭐⭐⭐⭐ |
| 分布式锁 | 高并发系统 | 零冲突 | 性能瓶颈 | ⭐⭐⭐⭐ |
| 数据库原子操作 | 读多写少 | 性能好 | 实现复杂 | ⭐⭐⭐⭐ |
🚨 常见踩坑点:
✨ 性能优化技巧:
对于并发生成唯一编号这个经典问题,关键在于:
记住:没有银弹,只有最适合的方案。根据你的业务规模、团队技术栈和维护成本来选择。
💬 你在项目中是如何处理并发生成唯一编号的?遇到过什么奇葩的bug?
欢迎在评论区分享你的实战经验,让我们一起交流学习!觉得这篇文章对你有帮助,请点赞并转发给更多的C#同行 👍
关注我,获取更多C#开发实战技巧和最佳实践分享!
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!