.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

