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的最佳实践
最后,我总结几条实战经验,供你参考:
- 用依赖注入管理DbContext生命周期,默认Scoped,别乱改。
- 连接字符串用配置文件 + 用户机密,别硬编码。
- 连接池参数要调优,特别是Max Pool Size和Connection Lifetime。
- 多数据库支持,开发用SQLite,测试用InMemory,生产用SQL Server或PostgreSQL。
- 注意连接泄漏,每个DbContext用完都要释放,或者交给DI容器管理。
- 事务要短,别在一个事务里做太多操作,避免长时间占用连接。
嗯,这一章的内容就到这里。说白了,DbContext和连接管理,是EF Core性能调优的基础。基础打好了,后面的查询优化、索引设计才能发挥效果。下一章,咱们聊聊实体配置和Fluent API,到时候我会分享一些我在实际项目中踩过的坑。