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

基于表生成事务ID的高并发控制优化方案咨询

高并发场景下事务ID生成的优化方案

背景

现有系统基于表生成事务ID,因ID需符合特定格式无法使用数据库原生序列;且事务类型数量可变,需支持动态创建序列。表结构定义如下:

public class TransactionSequence : BaseEntity<int>, IAggregateRoot
{
    public int Year { get; set; }
    public string Prefix { get; set; }
    public long Sequence { get; set; }        
    public string Service { get; set; }
    public int Length { get; set; }
    public bool IsCurrent { get; set; }
}

事务ID生成服务核心代码片段:

public class NumberingService : INumberingService
{
    public static int yearLength = 2;
    public static int monthLength = 2;
    public static int dayLength = 2;

    private readonly IRepository<TransactionSequence> _repository;
    private readonly NumberingConfiguration _numbering;
    
    
    public NumberingService(IRepository<TransactionSequence> repository, 
        NumberingConfiguration numbering)
    {
        _repository = repository;
        _numbering = numbering;
    }
    
    public async Task<Result<string>> GetNextNumberAsync(string service, int maxlength, UserEntity sysUser, string prefix = "")
    {
        string transactionId = string.Empty;

        try
        {
            var spec = new CurrentYearNumberingSpec(service);
            var sequence = await _repository.GetBySpecAsync(spec);
            if (sequence == null)
            {
                await AddServiceNumberingAsync(service, maxlength, sysUser, prefix);
                sequence = await _repository.GetBySpecAsync(spec);
            }

            sequence.Sequence = sequence.Sequence + 1;
            await _repository.UpdateAsync(sequence);
            int month = DateTime.Now.Month;
            int day = DateTime.Now.Day;
            var length = GetLength(sequence);

            transactionId = sequence.Prefix + (sequence.Year % 100).ToString("D" + 2) + month.ToString("D" + 2) + day.ToString("D" + 2) + sequence.Sequence.ToString("D" + length);
        }
        catch (Exception ex)
        {
            return Result<string>.Error(ex.Message);
        }
        return Result<string>.Success(transactionId, "Retrieved the next number in the sequence succesfully!");
    }
    
    private static int GetLength(TransactionSequence sequence)
    {
        return sequence.Length - sequence.Prefix.Length - dayLength - monthLength - yearLength;
    }
}

核心问题

系统处于高并发场景,每个请求都需获取事务ID,导致当前活跃的TransactionSequence行存在严重锁竞争,性能瓶颈明显。

已尝试方案

  • 带重试的ROWVERSION乐观并发:冲突频繁触发,性能最差
  • SemaphoreSlim锁:单实例性能尚可,但无法在负载均衡多实例场景下扩展
  • Redis分布式锁:性能与SemaphoreSlim接近,但未达到预期最优效果
  • 预取大小为1的RabbitMQ队列:性能优于上述方案,但仍有优化空间
  • HiLo算法:仅了解概念,尚未实现

推荐优化方案

基于ASP.NET Core 6、EF Core 6、SQL Server 2019环境,以下是成熟的高优解决方案:

1. 批量预取式HiLo算法(最优推荐)

核心逻辑是减少数据库交互次数:每次从数据库批量获取一段连续的序列范围(如一次取100个),应用内维护本地计数器,用完该批次后再去数据库更新序列。这种方式能将行锁竞争频率降低99%以上。

实现步骤:

  1. 表结构复用:无需新增字段,直接用Sequence字段记录当前已分配的最大值(即最后一个被取走的序列值)。
  2. 原子批量更新:使用EF Core 6的ExecuteUpdateAsync实现原子性的序列范围获取与更新,避免长时间持有行锁:
public async Task<(long BatchStart, long BatchEnd)> GetBatchSequenceAsync(string service, int batchSize = 100, UserEntity sysUser = null, string prefix = "")
{
    var spec = new CurrentYearNumberingSpec(service);
    var sequence = await _repository.GetBySpecAsync(spec);
    
    if (sequence == null)
    {
        await AddServiceNumberingAsync(service, /* 对应maxlength参数 */, sysUser, prefix);
        sequence = await _repository.GetBySpecAsync(spec);
    }

    // 原子更新并获取更新前的旧值
    var oldSequenceValue = sequence.Sequence;
    await _repository.Set()
        .Where(s => s.Id == sequence.Id)
        .ExecuteUpdateAsync(s => s.SetProperty(e => e.Sequence, e => e.Sequence + batchSize));

    // 返回本次可用的序列范围
    return (oldSequenceValue + 1, oldSequenceValue + batchSize);
}
  1. 应用内本地缓存:为每个service+Year组合维护本地计数器,当本地序列耗尽时,再调用批量获取接口。多实例场景下,每个实例获取不同批次,不会出现ID重复。

2. SQL Server序列+自定义格式拼接

若ID格式允许通过拼接实现,可直接利用SQL Server原生序列的高性能:

  • 为每个事务类型动态创建序列(可通过存储过程自动创建),序列支持按年重置(使用ALTER SEQUENCE ... RESTART WITH 1)。
  • 在应用层或数据库视图中,将序列值与前缀、年/月/日拼接成最终事务ID。
  • 优势:完全利用数据库原生高并发支持,实现简单,无锁竞争问题。

3. 变种Snowflake分布式ID生成

如果ID格式可灵活调整,可采用Snowflake算法变种:

  • 将Snowflake结构中的时间戳部分替换为年/月/日的格式化字符串,前缀保留业务标识,机器ID用实例唯一标识(如容器ID、IP哈希),序列号保证单实例内并发唯一性。
  • 优势:完全无数据库依赖,分布式场景下性能极高,无锁竞争。

方案对比

方案性能扩展性实现复杂度适用场景
批量预取HiLo极高支持多实例负载均衡中等ID格式严格固定,高并发场景
SQL Server序列+拼接高支持多实例低ID格式可通过拼接实现
变种Snowflake极高全分布式无依赖中等ID格式可调整,超高并发场景

内容的提问来源于stack exchange,提问作者Saleh Omar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 11:50:32