You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 06:51:24