第一版我们这么实现

发布时间:2025-06-24 20:24:03  作者:北方职教升学中心  阅读量:500


  • Assert:在测试完成后,我们验证所有请求是否成功完成,确保所有并发请求都能被正确处理。我们可以通过使用SemaphoreSlim来控制并发线程的数量。

    2.1 安装 NuGet 包

    在项目中安装必要的 NuGet 包,包括 Pomelo.EntityFrameworkCore.MySql(用于支持 MySQL)和 Microsoft.EntityFrameworkCore

    <ItemGroup><PackageReference Include="Microsoft.EntityFrameworkCore"Version="8.0.11"/><PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions"Version="8.0.11"/><PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers"Version="8.0.11"/><PackageReference Include="Microsoft.EntityFrameworkCore.Design"Version="8.0.11"><PrivateAssets>all</PrivateAssets><IncludeAssets>runtime;build;native;contentfiles;analyzers;buildtransitive</IncludeAssets></PackageReference><PackageReference Include="Microsoft.EntityFrameworkCore.Relational"Version="8.0.11"/><PackageReference Include="Microsoft.EntityFrameworkCore.Tools"Version="8.0.11"><PrivateAssets>all</PrivateAssets><IncludeAssets>runtime;build;native;contentfiles;analyzers;buildtransitive</IncludeAssets></PackageReference><PackageReference Include="Pomelo.EntityFrameworkCore.MySql"Version="8.0.2"/><PackageReference Include="Swashbuckle.AspNetCore"Version="6.6.2"/></ItemGroup>
    2.2 配置数据库上下文

    Program.cs中进行数据库配置,确保将 MySQL 服务注册到依赖注入容器中。

  • 数据一致性问题:多个线程同时写入数据库时,可能会造成数据冲突或违反唯一性约束。在终端中运行以下命令:

    dotnet ef migrations addInitialCreatedotnet ef database update

    执行完毕之后 在这里插入图片描述

    这些命令将为的 MySQL 数据库创建初始的 Users表,并将其同步到数据库中。

  • 3. 创建数据库上下文和实体类

    3.1 ApplicationDbContext.cs

    这是数据库上下文类,继承自 DbContext,用于与 MySQL 进行交互。在此项目中,我们将编写针对 Web API 的单元测试代码。这个问题通常源于多个线程同时访问数据库时,可能会导致以下情况:

    1. 数据库连接池耗尽:每个线程都可能创建一个数据库连接,如果并发量过大,可能会导致数据库连接池用尽,从而无法创建新的连接,导致写入失败。

  • 事务冲突:多个线程可能会同时修改相同的数据,导致事务失败。

    示例:多线程并发写入数据库

    为了完整地实现一个基于 ASP.NET Core Web API 的应用,使用 MySQL 数据库并处理多线程并发写入的问题,以下是一个完整的示例代码,包括了 Program.cs中的服务注册、

  • 异步操作:使用异步操作提高并发性能,减少阻塞。可以将数据库上下文注册到 InMemory提供程序中。
  • 限制并发请求:通过信号量或线程池限制并发线程数,防止连接池耗尽。更隔离的单元测试。

    1. 配置数据库连接和服务注册

    首先,确保的 appsettings.json中包含 MySQL 的连接字符串配置:

    1.1 appsettings.json
    {"ConnectionStrings":{"DefaultConnection":"Server=localhost;Database=usersdb;User=root;Password=root;"},"Logging":{"LogLevel":{"Default":"Information","Microsoft":"Warning","Microsoft.Hosting.Lifetime":"Information"}},"AllowedHosts":"*"}

    这个配置中的 DefaultConnection是 MySQL 的连接字符串,确保替换为MySQL 数据库的实际连接信息。

    3.1 ConcurrencyTest.cs

    创建一个 ConcurrencyTest.cs文件来编写测试代码。

  • 并发请求:我们使用 Task.WhenAll来等待多个并发请求同时执行,这模拟了多个线程同时访问 Web API 的场景。

    修改 Program.cs中的数据库注册部分,如下所示:

    // 在测试环境中使用InMemory数据库builder.Services.AddDbContext<ApplicationDbContext>(options =>options.UseInMemoryDatabase("TestDatabase"));// 使用InMemory数据库

    6. 运行测试

    现在,我们已经创建了一个并发测试,用于验证在 Web API 中并发请求的处理是否正确。

  • SendPostRequest:这个辅助方法负责向 Web API 发送 POST请求,将用户数据提交到 /api/users。假设用户的Email字段是唯一的,插入数据之前,可以先检查数据库中是否已经存在相同的Email。如果并发线程过多,以下问题可能会发生:

    1. 数据库连接池耗尽:每个线程都需要获取数据库连接,若并发量过大,可能导致连接池用尽。

      publicclassUserRepository:IUserRepository{privatereadonlyApplicationDbContext_context;publicUserRepository(ApplicationDbContextcontext){_context =context;}publicasyncTaskAddUserAsync(Useruser){// 使用数据库事务using(vartransaction =await_context.Database.BeginTransactionAsync()){try{await_context.Users.AddAsync(user);await_context.SaveChangesAsync();// 提交事务awaittransaction.CommitAsync();}catch(Exception){// 回滚事务awaittransaction.RollbackAsync();throw;}}}}

      2. 限制并发请求

      为了避免数据库连接池耗尽,可以限制API的并发请求数。

    500异常
    在这里插入图片描述
    和断言异常
    在这里插入图片描述

    解决方案

    1. 使用数据库事务保证一致性

    为了保证多个并发线程插入数据库时的一致性,可以使用数据库事务来确保每个写入操作都是原子的。第一版我们这么实现

    publicclassUserRepository:IUserRepository{privatereadonlyApplicationDbContext_context;publicUserRepository(ApplicationDbContextcontext){_context =context;}publicasyncTaskAddUserAsync(Useruser){// 模拟并发场景await_context.Users.AddAsync(user);await_context.SaveChangesAsync();// 写入数据库}}

    5. 完整的 API 控制器

    5.1 UsersController.cs

    这是 API 控制器,负责处理用户新增请求。这个测试将模拟多个线程同时调用 CreateUserAsyncAPI。在命令行中运行以下命令来执行单元测试:

    dotnet test

    7. 测试总结

    我们已经实现了一个针对 POST /api/users接口的并发请求单元测试。

    [ApiController][Route("api/[controller]")]publicclassUsersController:ControllerBase{privatereadonlyIUserRepository_userRepository;publicUsersController(IUserRepositoryuserRepository){_userRepository =userRepository;}[HttpPost]publicasyncTask<IActionResult>CreateUserAsync([FromBody]Useruser){if(user ==null){returnBadRequest("Invalid user data.");}try{await_userRepository.AddUserAsync(user);returnOk("User created successfully.");}catch(Exceptionex){returnStatusCode(StatusCodes.Status500InternalServerError,ex.Message);}}}

    6. 启动和迁移数据库

    6.1 在终端运行迁移命令

    确保已为 ApplicationDbContext添加了迁移并更新了数据库。

  • 配置了Swagger(可选),以便在开发环境下自动生成API文档。第一版我们这么实现。

    usingMicrosoft.EntityFrameworkCore;namespaceSample1215.Models{publicclassApplicationDbContext:DbContext{publicApplicationDbContext(DbContextOptions<ApplicationDbContext>options):base(options){}publicDbSet<User>Users {get;set;}}}
    3.2 User.cs实体类

    这是 User实体类,表示数据库中的用户表。MySQL 配置以及其它相关的服务和依赖注入设置。

    5.1 配置 InMemory 数据库

    Program.cs文件中,我们可以在测试中使用 InMemory数据库,以便进行更快、

    测试验证问题:并发写入导致失败

    为了测试并发请求在 Web API 中的处理,我们可以使用单元测试框架来模拟多个并发请求。

  • 接下来,我们将通过示例来说明如何解决这些问题。

    namespaceSample1215.Repositories{publicinterfaceIUserRepository{TaskAddUserAsync(Useruser);}}
    4.2 UserRepository.cs

    这是 UserRepository类的实现,负责将数据插入 MySQL 数据库。

  • 注册了 IUserRepository接口和 UserRepository实现,确保服务可以通过依赖注入使用。此外,使用 InMemory数据库让我们能够快速进行测试而无需连接到真实数据库,这为开发和调试提供了便利。如果有多个写入操作失败,则可以回滚事务。数据一致性问题以及事务冲突等问题。每个请求都会创建一个新的User并调用AddUserAsync方法将其插入到数据库中。

    3. 使用乐观锁解决数据冲突

    如果并发写入的用户数据存在唯一性约束(例如Email),我们可以在数据库中使用乐观锁或在业务逻辑中检查唯一性。通过使用 xUnitWebApplicationFactory,我们可以模拟多并发请求并测试 API 在高并发场景下的稳定性。

    2. 配置数据库上下文

    需要使用 Entity Framework Core 来访问 MySQL 数据库,首先在 Program.cs中注册数据库上下文。

    在这个简单的例子中,假设API接口被多个并发线程调用。

    在这里插入图片描述

    前言

    在ASP.NET Core Web API应用程序中,当多个并发线程同时调用新增用户数据的接口时,可能会遇到数据库写入失败的问题。如果更多请求到达,后续的请求会等待直到前面的请求完成。

  • 唯一性冲突:如果并发插入的用户具有相同的唯一约束(如Email),可能会出现违反唯一性约束的错误。

    2. 创建测试项目

    我们的Web API 项目名称为 Sample1215,然后我们就创建一个名为 Sample1215.Test的测试项目。

    usingMicrosoft.EntityFrameworkCore;usingMicrosoft.Extensions.DependencyInjection;usingMicrosoft.Extensions.Hosting;usingSample1215.Models;usingSample1215.Repositories;varbuilder =WebApplication.CreateBuilder(args);// 1. 注册数据库上下文服务builder.Services.AddDbContext<ApplicationDbContext>(options =>options.UseMySql(builder.Configuration.GetConnectionString("DefaultConnection"),ServerVersion.AutoDetect(builder.Configuration.GetConnectionString("DefaultConnection"))));// 2. 注册自定义服务builder.Services.AddScoped<IUserRepository,UserRepository>();// 3. 注册控制器服务builder.Services.AddControllers();// 4. 注册Swagger(可选,用于API文档)builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();varapp =builder.Build();// 5. 配置中间件if(app.Environment.IsDevelopment()){app.UseSwagger();app.UseSwaggerUI();}app.UseAuthorization();app.MapControllers();app.Run();

    在上述代码中,我们执行了以下操作:

    • 通过 builder.Services.AddDbContext<ApplicationDbContext>注册了数据库上下文,配置了 MySQL 数据库连接字符串。这样,所有的测试都可以在内存中完成,而不影响实际的数据库。

      usingMicrosoft.AspNetCore.Mvc;usingSample1215.Models;usingSample1215.Repositories;namespaceSample1215.Controllers{[ApiController][Route("api/[controller]")]publicclassUsersController:ControllerBase{privatestaticSemaphoreSlim_semaphore =newSemaphoreSlim(10);// 限制最大并发10个请求privatereadonlyIUserRepository_userRepository;publicUsersController(IUserRepositoryuserRepository){_userRepository =userRepository;}[HttpPost]publicasyncTask<IActionResult>CreateUserAsync([FromBody]Useruser){if(user ==null){returnBadRequest("Invalid user data.");}// 等待直到有空闲的线程await_semaphore.WaitAsync();try{await_userRepository.AddUserAsync(user);returnOk("User created successfully.");}catch(Exceptionex){returnStatusCode(StatusCodes.Status500InternalServerError,ex.Message);}finally{// 完成后释放信号量_semaphore.Release();}}}}

      这样,最多只有10个线程可以同时写入数据库。

    • 这些方法不仅可以提高数据库写入的稳定性,还能提升系统的整体性能和响应能力。

      namespaceSample1215.Models{publicclassUser{publicintId {get;set;}publicstringName {get;set;}publicstringEmail {get;set;}}}

      4. 创建 UserRepository实现

      4.1 IUserRepository.cs

      这是用户数据存储接口。这里我们将使用 xUnit作为单元测试框架,并使用 Microsoft.AspNetCore.Mvc.TestingHttpClient来模拟 HTTP 请求。

    • 乐观锁:通过检查唯一性约束来避免并发写入冲突。

      usingMicrosoft.AspNetCore.Mvc.Testing;usingSample1215.Model;usingSystem.Net.Http.Json;namespaceSample1215.Tests{publicclassConcurrencyTest:IClassFixture<WebApplicationFactory<Program>>{privatereadonlyWebApplicationFactory<Program>_factory;publicConcurrencyTest(WebApplicationFactory<Program>factory){_factory =factory;}[Fact]publicasyncTaskCreateUser_ConcurrentRequests_ShouldBeHandledCorrectly(){// Arrangevarclient =_factory.CreateClient();varuser =newUser{Name ="Test User",Email ="testuser@example.com"};// 通过多个并发请求模拟并发写入vartasks =newTask[20000];// 模拟20000个并发请求for(inti =0;i <tasks.Length;i++){tasks[i]=SendPostRequest(client,user);}// ActawaitTask.WhenAll(tasks);// Assert// 你可以通过检查数据库中的记录数或检查响应状态来确保请求成功// 比如,检查某个唯一标识符是否插入成功,或者返回的状态码是否都为200// 例如,检查每个请求的状态码是否都是200foreach(vartask intasks){Assert.True(task.IsCompletedSuccessfully);}}privateasyncTaskSendPostRequest(HttpClientclient,Useruser){varresponse =awaitclient.PostAsJsonAsync("/api/users",user);response.EnsureSuccessStatusCode();// 确保请求成功}}}

      4. 解释测试代码

      • WebApplicationFactory<Program>:这是一个 Microsoft.AspNetCore.Mvc.Testing提供的工厂类,它允许我们在测试环境中启动 Web API,并创建 HTTP 客户端。

        publicclassUserRepository:IUserRepository{privatereadonlyApplicationDbContext_context;publicUserRepository(ApplicationDbContextcontext){_context =context;}publicasyncTaskAddUserAsync(Useruser){// 检查Email是否已存在varexistingUser =await_context.Users            .FirstOrDefaultAsync(u =>u.Email ==user.Email);if(existingUser !=null){thrownewInvalidOperationException("Email already exists.");}// 如果不存在,插入新用户await_context.Users.AddAsync(user);await_context.SaveChangesAsync();}}

        4. 使用异步操作优化性能

        异步操作有助于避免线程阻塞,从而提高API的并发处理能力。

      5. 数据库模拟

      为了测试并发请求,我们使用了 InMemory数据库来避免在真实 MySQL 数据库中进行操作。

    • 事务问题:没有适当的事务控制,多个线程可能在执行写入时发生数据不一致或冲突。通过以下策略,可以有效解决这些问题:

      1. 使用数据库事务:确保每个插入操作都能原子执行,避免数据不一致。

        usingMicrosoft.EntityFrameworkCore;usingSample1215.Models;namespaceSample1215.Repositories{publicclassUserRepository:IUserRepository{privatereadonlyApplicationDbContext_context;publicUserRepository(ApplicationDbContextcontext){_context =context;}publicasyncTaskAddUserAsync(Useruser){// 使用事务确保操作的原子性using(vartransaction =await_context.Database.BeginTransactionAsync()){try{// 检查Email是否已存在varexistingUser =await_context.Users                        .FirstOrDefaultAsync(u =>u.Email ==user.Email);if(existingUser !=null){thrownewInvalidOperationException("Email already exists.");}await_context.Users.AddAsync(user);await_context.SaveChangesAsync();awaittransaction.CommitAsync();}catch(Exception){awaittransaction.RollbackAsync();throw;}}}}}

        总结

        当多个并发线程访问数据库时,可能会遇到数据库连接池耗尽、确保在数据库操作中使用异步方法。

        3. 编写并发测试代码

        我们将使用 xUnit来编写并发请求的单元测试。

        1. 添加所需的 NuGet 包

        在测试项目中,确保添加以下 NuGet 包:

        <ProjectSdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>disable</Nullable><IsPackable>false</IsPackable><IsTestProject>true</IsTestProject></PropertyGroup><ItemGroup><PackageReferenceInclude="coverlet.collector"Version="6.0.0"/><PackageReferenceInclude="Microsoft.AspNetCore.Mvc.Testing"Version="8.0.11"/><PackageReferenceInclude="Microsoft.EntityFrameworkCore.InMemory"Version="8.0.11"/><PackageReferenceInclude="Microsoft.Extensions.DependencyInjection"Version="8.0.1"/><PackageReferenceInclude="Microsoft.NET.Test.Sdk"Version="17.8.0"/><PackageReferenceInclude="Moq"Version="4.20.72"/><PackageReferenceInclude="xunit"Version="2.9.2"/><PackageReferenceInclude="xunit.runner.visualstudio"Version="2.8.2"><PrivateAssets>all</PrivateAssets><IncludeAssets>runtime;build;native;contentfiles;analyzers;buildtransitive</IncludeAssets></PackageReference></ItemGroup><ItemGroup><ProjectReferenceInclude="..\Sample1215\Sample1215.csproj"/></ItemGroup><ItemGroup><UsingInclude="Xunit"/></ItemGroup></Project>

        这些包将帮助我们执行 Web API 测试,模拟数据库操作,并进行并发请求测试。