如何在三层架构Web API中通过数据验证处理竞态条件
解决用户创建接口的竞态条件与分层错误返回问题
一、先解决竞态条件的根本问题
你当前通过查询数据库判断姓名组合是否存在的方式,在高并发场景下必然会出现竞态——两个请求同时通过查询校验,然后同时插入数据库,导致重复数据。唯一可靠的兜底方案是在数据库层面给First Name和Last Name组合添加唯一约束:
- 比如在SQL Server中执行:
ALTER TABLE Users ADD CONSTRAINT UQ_Users_FirstName_LastName UNIQUE (FirstName, LastName);
这样即使业务层校验通过,数据库也会直接拒绝重复插入的请求,抛出唯一约束冲突异常。
数据注解依然可以保留,作为前端和API的前置校验,用来提前拦截大部分无效请求,减少对数据库的压力,但不能依赖它防止竞态。
二、分层架构下的错误返回设计
针对Repository层预期返回POCO的问题,我们可以通过定义通用结果对象来兼顾成功时返回POCO、失败时返回错误信息的需求,各层分工处理:
1. 定义通用结果类型
先创建一个泛型的OperationResult<T>类,用来封装操作结果:
public class OperationResult<T> { public bool IsSuccess { get; set; } public T Data { get; set; } public string ErrorMessage { get; set; } public string ErrorCode { get; set; } // 可选,用来标识具体错误类型,比如"DuplicateName" }
2. Repository层改造
Repository层不再直接返回POCO,而是返回OperationResult<User>:
- 尝试插入用户时,捕获数据库的唯一约束异常(比如EF Core中的
DbUpdateException,内部包含SqlException且错误码为2601/2627) - 将异常转换为业务可读的错误信息,封装到结果对象中
public class UserRepository : IUserRepository { private readonly AppDbContext _dbContext; public UserRepository(AppDbContext dbContext) { _dbContext = dbContext; } public async Task<OperationResult<User>> CreateUserAsync(User user) { try { _dbContext.Users.Add(user); await _dbContext.SaveChangesAsync(); return new OperationResult<User> { IsSuccess = true, Data = user }; } catch (DbUpdateException ex) { var sqlEx = ex.InnerException as SqlException; // 判断是否为唯一约束冲突 if (sqlEx != null && (sqlEx.Number == 2601 || sqlEx.Number == 2627)) { return new OperationResult<User> { IsSuccess = false, ErrorMessage = "该姓名组合已存在", ErrorCode = "DuplicateName" }; } // 其他数据库异常,返回通用错误 return new OperationResult<User> { IsSuccess = false, ErrorMessage = "数据库操作失败", ErrorCode = "DbError" }; } } }
3. Service层处理
Service层调用Repository,根据结果做业务逻辑处理(如果需要的话),然后将结果传递给Controller:
public class UserService : IUserService { private readonly IUserRepository _userRepository; public UserService(IUserRepository userRepository) { _userRepository = userRepository; } public async Task<OperationResult<UserDto>> CreateUserAsync(UserCreateDto createDto) { // 可选:这里依然可以保留姓名组合查询的前置校验,进一步减少数据库异常的概率 var exists = await _userRepository.CheckNameExistsAsync(createDto.FirstName, createDto.LastName); if (exists) { return new OperationResult<UserDto> { IsSuccess = false, ErrorMessage = "该姓名组合已存在", ErrorCode = "DuplicateName" }; } // 转换DTO为POCO var user = new User { FirstName = createDto.FirstName, LastName = createDto.LastName // 其他字段赋值 }; var repoResult = await _userRepository.CreateUserAsync(user); if (!repoResult.IsSuccess) { return new OperationResult<UserDto> { IsSuccess = false, ErrorMessage = repoResult.ErrorMessage, ErrorCode = repoResult.ErrorCode }; } // 转换POCO为返回用的DTO var userDto = new UserDto { Id = repoResult.Data.Id, FirstName = repoResult.Data.FirstName, LastName = repoResult.Data.LastName }; return new OperationResult<UserDto> { IsSuccess = true, Data = userDto }; } }
4. Controller层返回响应
Controller层接收Service的结果,转换成标准的HTTP响应:
- 成功时返回201 Created,携带用户DTO
- 失败时根据错误码返回对应的HTTP状态码(比如重复姓名返回409 Conflict),携带错误信息
[ApiController] [Route("api/users")] public class UsersController : ControllerBase { private readonly IUserService _userService; public UsersController(IUserService userService) { _userService = userService; } [HttpPost] public async Task<IActionResult> CreateUser(UserCreateDto createDto) { // 先做数据注解的模型验证 if (!ModelState.IsValid) { return BadRequest(ModelState); } var result = await _userService.CreateUserAsync(createDto); if (!result.IsSuccess) { if (result.ErrorCode == "DuplicateName") { return Conflict(new { message = result.ErrorMessage }); } return StatusCode(500, new { message = result.ErrorMessage }); } return CreatedAtAction(nameof(GetUser), new { id = result.Data.Id }, result.Data); } // 其他接口比如GetUser [HttpGet("{id}")] public async Task<IActionResult> GetUser(int id) { // 实现逻辑 } }
5. 前端处理
前端接收API的响应:
- 如果是201状态码,提示创建成功并跳转/刷新
- 如果是409状态码,提示用户“该姓名组合已存在”
- 如果是其他错误状态码,提示通用错误信息
三、可选方案:使用自定义异常
如果你更倾向于用异常处理而不是结果对象,可以定义自定义业务异常:
public class DuplicateNameException : Exception { public DuplicateNameException() : base("该姓名组合已存在") { } }
然后Repository层在捕获唯一约束异常时,抛出这个自定义异常;Service层捕获后可以直接往上抛,或者处理后再抛;Controller层用[ExceptionFilter]或者全局异常过滤器来捕获,转换成对应的HTTP响应。这种方式代码更简洁,但需要统一的异常处理机制。
内容的提问来源于stack exchange,提问作者Qiuzman
相关产品推荐
相关产品推荐

