.NET线上Web应用如何避免并发交易时生成重复交易ID?
这确实是高并发交易场景下非常头疼的问题,先帮你拆解下之前尝试的方案为什么没起作用,再给你几个经过生产环境验证的可靠解决思路:
先分析下现有方案失效的可能原因
- Mutex:它是进程内的同步锁,如果你的Web应用部署在多服务器、或者单服务器多工作进程(比如IIS应用池启用了多进程模式),单个进程里的Mutex根本管不到其他进程的请求,自然无法阻止跨进程的ID重复。
- 表锁(SQL):如果是用了全局表锁,那性能会暴跌到无法使用;如果是行锁,大概率是锁的时机不对——比如你先查询当前最大ID再加1,然后才去锁行插入,这中间的时间窗口已经足够并发请求生成重复ID了。
- 插入数据表时生成ID:如果是手动查询最大ID再加1后插入,那这个操作不是原子性的,高并发下必然冲突;如果是用数据库自增列但还是重复,可能是你误用了(比如手动指定了ID值,或者分表场景下起始值没配置对)。
可行的解决方案
1. 依赖数据库原生自增/序列(最省心的方案)
直接用数据库提供的自增列或序列对象,这是数据库层面保证原子性的,绝对不会出现重复。
- SQL Server:用
IDENTITY列,或者SEQUENCE对象;插入后用SCOPE_IDENTITY()获取生成的ID。 - MySQL:用
AUTO_INCREMENT列,插入后用410145获取。 - PostgreSQL:用
SERIAL/BIGSERIAL,或者CREATE SEQUENCE。 - 优点:不用自己处理并发逻辑,数据库原生支持,性能稳定,运维成本低。
- 注意:如果是分库分表场景,要给每个表设置不同的起始值或步长(比如表1从1开始步长2,表2从2开始步长2),避免跨表ID重复。
2. 分布式ID生成器(适合多节点/微服务架构)
如果你的应用是分布式部署(多服务器、微服务集群),可以用以下几种成熟的分布式ID方案:
- GUID/UUID:.NET里直接调用
Guid.NewGuid().ToString()就能生成,版本4的UUID是完全随机的,碰撞概率极低(几乎可以忽略)。缺点是字符串类型的ID在数据库索引上的性能不如数值型,而且无序,不适合需要按ID排序的场景。 - 雪花算法(Snowflake):生成64位的数值型ID,包含时间戳+机器ID+序列号,既保证全局唯一,又能保证ID有序(按时间递增)。你可以自己实现简化版,也可以用现成的NuGet包比如
IdGen。
这里给个简化版的雪花算法实现参考:public class SnowflakeIdGenerator { // 自定义起始时间戳(比如2021-01-01 00:00:00) private const long Twepoch = 1609459200000L; // 机器ID位数(最多支持32台机器) private const int WorkerIdBits = 5; // 数据中心ID位数(最多支持32个数据中心) private const int DatacenterIdBits = 5; // 序列号位数(每毫秒最多生成4096个ID) private const int SequenceBits = 12; private readonly long _maxWorkerId = -1L ^ (-1L << WorkerIdBits); private readonly long _maxDatacenterId = -1L ^ (-1L << DatacenterIdBits); private readonly long _sequenceMask = -1L ^ (-1L << SequenceBits); private readonly long _workerIdShift = SequenceBits; private readonly long _datacenterIdShift = SequenceBits + WorkerIdBits; private readonly long _timestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits; private long _sequence = 0L; private long _lastTimestamp = -1L; private readonly long _workerId; private readonly long _datacenterId; private readonly object _lockObj = new object(); public SnowflakeIdGenerator(long workerId, long datacenterId) { if (workerId > _maxWorkerId || workerId < 0) throw new ArgumentException($"Worker ID must be between 0 and {_maxWorkerId}"); if (datacenterId > _maxDatacenterId || datacenterId < 0) throw new ArgumentException($"Datacenter ID must be between 0 and {_maxDatacenterId}"); _workerId = workerId; _datacenterId = datacenterId; } public long NextId() { lock (_lockObj) { var timestamp = GetCurrentTimestamp(); if (timestamp < _lastTimestamp) throw new InvalidOperationException("Clock moved backwards. Cannot generate ID."); if (timestamp == _lastTimestamp) { _sequence = (_sequence + 1) & _sequenceMask; if (_sequence == 0) timestamp = WaitNextMillis(_lastTimestamp); } else _sequence = 0L; _lastTimestamp = timestamp; return ((timestamp - Twepoch) << _timestampLeftShift) | (_datacenterId << _datacenterIdShift) | (_workerId << _workerIdShift) | _sequence; } } private long GetCurrentTimestamp() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); private long WaitNextMillis(long lastTimestamp) { var timestamp = GetCurrentTimestamp(); while (timestamp <= lastTimestamp) timestamp = GetCurrentTimestamp(); return timestamp; } } - Redis自增ID:利用Redis的
INCR命令(原子性递增),给交易ID设置一个全局计数器,每次生成ID时调用INCR trade:id:counter,返回的数值就是唯一的交易ID。适合需要数值型有序ID的场景,而且性能极高。
示例代码(用StackExchange.Redis):public async Task<long> GenerateTradeIdAsync(IDatabase redisDb) { // 可以给计数器设置初始值,比如如果之前有数据,先设置到最大ID+1 return await redisDb.StringIncrementAsync("trade:id:counter"); }
3. 唯一约束+重试机制(兜底保障)
不管用哪种ID生成方式,都建议给交易ID字段加上数据库唯一约束,这样即使出现极端情况生成了重复ID,数据库会抛出约束冲突异常,这时你可以在代码里捕获异常并重试生成ID,作为最后一道防线。
- 示例代码(EF Core):
public async Task<long> CreateTradeAsync(Trade trade) { const int maxRetries = 3; int retryCount = 0; while (retryCount < maxRetries) { try { // 用你选择的ID生成方式生成交易ID trade.TradeId = GenerateTradeId(); _dbContext.Trades.Add(trade); await _dbContext.SaveChangesAsync(); return trade.TradeId; } catch (DbUpdateException ex) { // 捕获SQL Server的唯一约束冲突错误(错误码2627) if (ex.InnerException is SqlException sqlEx && sqlEx.Number == 2627) { retryCount++; await Task.Delay(100); // 短暂延迟后重试 continue; } throw; } } throw new InvalidOperationException("Failed to generate unique trade ID after maximum retries."); }
内容的提问来源于stack exchange,提问作者Vishwanath
相关产品推荐
相关产品推荐

