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

如何在三层架构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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 14:03:19