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

.NET 6手动递增生成用户名时并发冲突的解决方法咨询

问题描述

我有一个基于.NET 6的后端系统,采用手动递增方式生成用户名。

用户名基于公司与角色设计,假设公司为a1、a2,角色为employee、manager,格式为{company}-{role}{number},例如a1-e0001、a2-m0010等。

相关代码如下:

[HttpPost]
[Route("create")]
public async Task<BaseDto?> Create([FromBody] reqDto)
{
    var lastUser = usersRepository
    .Where(e => e.Company == reqDto.Company && e.Role == reqDto.Role)
    .SortByUserName().LastOrDefault();

    var lastUserName = lastUser?.UserName;
    var lastUserNameInt = int.Parse(lastUserName.Split('-')[1]);
    var newUserName = e.Company + "-" + e.Role + (lastUserNameInt + 1);

    // 创建用户
    // ...
    // ...
}

短时间内多次调用该方法时,会出现用户名重复冲突:第二个请求在第一个请求插入新用户前执行,导致二者生成相同用户名。我曾考虑阻塞后续调用直至前一个请求完成,但不知如何实现,请问有什么可行的解决办法?

解决方案

以下是几种按优先级排序的可行方案,覆盖不同场景需求:

1. 数据库层面实现原子递增(最可靠)

方案A:序列+唯一约束

针对每个(Company, Role)组合创建独立数据库序列,插入用户时直接从数据库获取序列下一个值生成用户名,同时给UserName字段加唯一约束,双重保障唯一性。

以SQL Server为例:

-- 为a1公司的employee角色创建序列
CREATE SEQUENCE a1_employee_seq
START WITH 1
INCREMENT BY 1;

-- 插入时生成用户名
INSERT INTO Users (Company, Role, UserName)
VALUES ('a1', 'employee', CONCAT('a1-e', FORMAT(NEXT VALUE FOR a1_employee_seq, '0000')));

.NET代码中可通过EF Core的FromSqlRaw或存储过程调用序列,彻底避免应用层的并发问题。

方案B:分区自增列+计算列

给表添加按(Company, Role)分区的自增列,再用计算列自动生成符合格式的用户名:

-- SQL Server分区示例
CREATE PARTITION FUNCTION pf_CompanyRole (NVARCHAR(50))
AS RANGE LEFT FOR VALUES ('a1-employee', 'a1-manager', 'a2-employee', 'a2-manager');

CREATE PARTITION SCHEME ps_CompanyRole
AS PARTITION pf_CompanyRole ALL TO ([PRIMARY]);

CREATE TABLE Users (
    Id INT IDENTITY(1,1) NOT NULL,
    Company NVARCHAR(20) NOT NULL,
    Role NVARCHAR(20) NOT NULL,
    -- 自动生成用户名,PERSISTED可让字段参与索引
    UserName AS CONCAT(Company, '-', LEFT(Role, 1), FORMAT(Id, '0000')) PERSISTED,
    -- 其他字段
) ON ps_CompanyRole (CONCAT(Company, '-', Role));

这种方式让数据库完全负责用户名的生成和唯一性校验,从根源消除并发冲突。

2. 应用层加分布式锁(适配无法改数据库的场景)

如果无法调整数据库结构,可针对(Company, Role)组合加分布式锁,确保同一组合下只有一个请求能执行用户名生成逻辑。

以StackExchange.Redis实现为例:

private readonly IDatabase _redisDb;

public async Task<BaseDto?> Create([FromBody] reqDto)
{
    var lockKey = $"user-lock:{reqDto.Company}-{reqDto.Role}";
    var lockToken = Guid.NewGuid().ToString();
    // 设置5秒锁超时,避免死锁
    var lockAcquired = await _redisDb.LockTakeAsync(lockKey, lockToken, TimeSpan.FromSeconds(5));

    if (!lockAcquired)
    {
        return new BaseDto { Success = false, Message = "操作过于频繁,请稍后重试" };
    }

    try
    {
        // 查询最新用户并生成用户名(修正原代码的数字解析问题)
        var lastUser = usersRepository
            .Where(e => e.Company == reqDto.Company && e.Role == reqDto.Role)
            .OrderByDescending(e => e.UserName)
            .LastOrDefault();

        string newUserName;
        if (lastUser == null)
        {
            newUserName = $"{reqDto.Company}-{reqDto.Role[0]}0001";
        }
        else
        {
            var numberPart = lastUser.UserName.Split('-')[1][1..]; // 截取数字部分
            var lastNumber = int.Parse(numberPart);
            newUserName = $"{reqDto.Company}-{reqDto.Role[0]}{(lastNumber + 1).ToString("D4")}";
        }

        // 创建用户逻辑
        // ...
    }
    finally
    {
        // 释放锁
        await _redisDb.LockReleaseAsync(lockKey, lockToken);
    }

    return new BaseDto { Success = true };
}

如果是单服务器部署,也可以用内存锁(比如ConcurrentDictionary<string, SemaphoreSlim>),但多服务器集群必须用分布式锁。

3. 乐观锁+重试机制(兜底方案)

给Users表的UserName加唯一约束,插入时若因冲突报错则自动重试,适合并发量不极高的场景:

public async Task<BaseDto?> Create([FromBody] reqDto)
{
    int retryCount = 3;
    while (retryCount > 0)
    {
        try
        {
            var lastUser = usersRepository
                .Where(e => e.Company == reqDto.Company && e.Role == reqDto.Role)
                .OrderByDescending(e => e.UserName)
                .LastOrDefault();

            string newUserName;
            if (lastUser == null)
            {
                newUserName = $"{reqDto.Company}-{reqDto.Role[0]}0001";
            }
            else
            {
                var numberPart = lastUser.UserName.Split('-')[1][1..];
                var lastNumber = int.Parse(numberPart);
                newUserName = $"{reqDto.Company}-{reqDto.Role[0]}{(lastNumber + 1).ToString("D4")}";
            }

            var user = new User { Company = reqDto.Company, Role = reqDto.Role, UserName = newUserName };
            usersRepository.Add(user);
            await usersRepository.SaveChangesAsync();

            return new BaseDto { Success = true };
        }
        catch (DbUpdateException ex)
        {
            // 判断是否为SQL Server唯一约束冲突(错误码2601)
            if (ex.InnerException is SqlException sqlEx && sqlEx.Number == 2601)
            {
                retryCount--;
                if (retryCount == 0)
                {
                    return new BaseDto { Success = false, Message = "创建失败,请稍后重试" };
                }
                await Task.Delay(100); // 等待100ms后重试
            }
            else
            {
                throw;
            }
        }
    }

    return new BaseDto { Success = false, Message = "创建失败,请稍后重试" };
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 06:50:27