2、数据库上下文与连接:DbContext生命周期、连接字符串配置、多种数据库提供程序、连接池管理

好,咱们开始聊第二个核心话题——数据库上下文和连接管理。

说实话,很多EF Core初学者甚至老手,都在DbContext生命周期上栽过跟头。我见过有人把DbContext当成单例用,结果并发一上来直接崩了。也见过有人每个方法都new一个DbContext,性能差得离谱。嗯,这里面的门道,咱们得好好捋一捋。

2.1 DbContext的生命周期:别乱用,也别省着用

DbContext本质上是一个工作单元(Unit of Work)模式的实现。它跟踪实体状态,管理变更,最后一次性提交。所以它的生命周期,直接决定了你的数据一致性、内存占用和并发能力。

核心原则:DbContext应该是短暂的。

我个人习惯,在ASP.NET Core里,用依赖注入把DbContext注册为作用域(Scoped)服务。什么意思呢?就是一个HTTP请求进来,创建一个DbContext实例;请求结束,自动释放。这是最稳妥的做法。

生命周期对比表

注册方式 生命周期 适用场景 风险
AddDbContext<T>(默认Scoped) 每个请求一个实例 Web应用、API 低,推荐
AddDbContext<T>(optionsLifetime: ServiceLifetime.Singleton) 整个应用一个实例 几乎不适用 极高,并发冲突、内存泄漏
手动new DbContext 由你控制 控制台、后台任务 需手动释放,容易忘

我曾经在一个遗留项目里,看到有人把DbContext注册成了单例。你想想看,多个用户同时操作,所有变更都堆积在同一个上下文里,最后SaveChanges的时候,要么死锁,要么数据错乱。那次排查花了我整整两天,最后发现是生命周期搞错了。嗯,从那以后,我每次写DI注册都会多看一眼。

避坑指南:千万不要在长时间运行的后台服务里,把一个DbContext实例用到底。一定要按批次创建和释放。我曾经见过一个定时任务,每5分钟跑一次,但DbContext一直不释放,结果内存涨到2个G,最后OOM了。

2.2 连接字符串配置:别硬编码,也别太随意

连接字符串这东西,看起来简单,但坑也不少。我见过有人直接把连接字符串写在代码里,然后提交到Git仓库。嗯,后果你懂的。

推荐做法:用配置文件 + 用户机密(User Secrets)或环境变量。

// appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MyDb;User Id=sa;Password=YourPassword;TrustServerCertificate=True;"
  }
}

// 在Program.cs中注册
builder.Services.AddDbContext<MyDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

这里有个细节:TrustServerCertificate=True。开发环境用这个没问题,但生产环境一定要配正式的SSL证书。我有个朋友,生产环境忘了配这个,结果数据库连接一直报证书错误,查了半天才发现。

另外,连接字符串里还可以加一些性能相关的参数。比如:

  • Max Pool Size:连接池最大连接数,默认100。高并发场景可以调大,但别超过数据库上限。
  • Min Pool Size:最小连接数,默认0。建议设成5-10,避免冷启动时频繁创建连接。
  • Connection Lifetime:连接最大存活时间,默认0(无限)。建议设成300秒,防止连接池里的连接太久没刷新。
  • Pooling:是否启用连接池,默认true。千万别关,关了性能直接掉一个数量级。

小技巧:我习惯在开发环境用User Secrets存连接字符串,这样不会误提交到代码库。命令是:dotnet user-secrets set "ConnectionStrings:DefaultConnection" "你的连接字符串"

2.3 多种数据库提供程序:EF Core的跨平台能力

EF Core支持多种数据库,这是它的一大优势。你可以在开发时用SQLite,测试时用SQL Server,生产时用PostgreSQL。代码几乎不用改,只需要换一下提供程序和连接字符串。

常用的提供程序有:

数据库 NuGet包 Use方法
SQL Server Microsoft.EntityFrameworkCore.SqlServer UseSqlServer()
PostgreSQL Npgsql.EntityFrameworkCore.PostgreSQL UseNpgsql()
MySQL Pomelo.EntityFrameworkCore.MySql UseMySql()
SQLite Microsoft.EntityFrameworkCore.Sqlite UseSqlite()
InMemory(测试用) Microsoft.EntityFrameworkCore.InMemory UseInMemoryDatabase()

我个人建议,生产环境优先选SQL Server或PostgreSQL。MySQL虽然也能用,但Pomelo提供程序在复杂查询和迁移上偶尔会有些小坑。我在一个项目里用过MySQL,结果有个复杂的GroupBy查询,在MySQL上生成的SQL效率极低,最后不得不手写SQL。

另外,InMemory数据库只适合做单元测试,千万别用在生产环境。它不支持事务、不支持并发控制,说白了就是个假的数据库。

2.4 连接池管理:别让数据库成为瓶颈

连接池是数据库访问的命脉。每次打开一个数据库连接,都需要三次握手、认证、分配资源,开销很大。连接池就是把这些连接缓存起来,用完了放回去,下次直接复用。

EF Core默认使用底层ADO.NET的连接池。你不需要额外配置,但有几个参数值得关注:

  • 连接池大小:默认Max Pool Size=100,Min Pool Size=0。如果你的应用并发超过100,连接池会排队等待,这时候请求就会变慢。我建议根据并发量调整,比如并发200,就设Max Pool Size=200。
  • 连接泄漏:这是最常见的坑。如果你没有正确释放DbContext,连接就不会归还到池里。久而久之,连接池耗尽,应用就挂了。我曾经排查过一个线上故障,发现连接池里的连接数一直在涨,最后定位到是一个BackgroundService里忘了调用Dispose。
  • 连接池清理:连接池会自动清理长时间不用的连接。但如果你设了Connection Lifetime,连接会在达到时间后被强制关闭并重建,这样可以避免连接状态不一致。

连接池配置示例:

"ConnectionStrings": {
  "DefaultConnection": "Server=localhost;Database=MyDb;User Id=sa;Password=YourPassword;Max Pool Size=200;Min Pool Size=10;Connection Lifetime=300;Pooling=true;"
}

你想想看,如果连接池设得太小,高峰期请求一多,数据库连接就得排队。反过来,设得太大,数据库服务器内存又扛不住。所以这个值需要根据实际压测来调整。我一般先设一个保守的值,比如100,然后压测看响应时间,再逐步调大。

避坑指南:我曾经在一个高并发项目里,发现数据库CPU飙升,但连接数并不高。后来排查发现,是连接池里的连接被长时间占用,但实际并没有执行SQL。原因是某个事务没有及时提交或回滚,导致连接一直处于活跃状态。所以,一定要确保事务尽快提交或回滚,别让连接闲着占坑。

2.5 实战建议:DbContext的最佳实践

最后,我总结几条实战经验,供你参考:

  1. 用依赖注入管理DbContext生命周期,默认Scoped,别乱改。
  2. 连接字符串用配置文件 + 用户机密,别硬编码。
  3. 连接池参数要调优,特别是Max Pool Size和Connection Lifetime。
  4. 多数据库支持,开发用SQLite,测试用InMemory,生产用SQL Server或PostgreSQL。
  5. 注意连接泄漏,每个DbContext用完都要释放,或者交给DI容器管理。
  6. 事务要短,别在一个事务里做太多操作,避免长时间占用连接。

嗯,这一章的内容就到这里。说白了,DbContext和连接管理,是EF Core性能调优的基础。基础打好了,后面的查询优化、索引设计才能发挥效果。下一章,咱们聊聊实体配置和Fluent API,到时候我会分享一些我在实际项目中踩过的坑。