1. 为什么需要双数据库共存?
在.NET Core项目中同时使用PostgreSQL和SQLite的场景其实非常普遍。我最近接手的一个电商后台系统就遇到了典型需求:核心业务数据需要PostgreSQL保证ACID特性,而客户端本地又需要用SQLite缓存部分数据。这种架构组合既能发挥PostgreSQL作为关系型数据库的优势,又能利用SQLite轻量级、零配置的特点。
PostgreSQL适合存储订单、用户信息等需要严格事务支持的数据,它的MVCC机制能很好地处理高并发修改。而SQLite则完美适配客户端设备上的本地存储需求,比如用户浏览历史、草稿数据等。两者配合使用时,SQLite的单个文件特性让数据分发变得极其简单——你甚至可以直接把.db文件打包进应用安装包。
2. 环境准备与NuGet配置
2.1 必备组件安装
首先通过NuGet安装以下核心包:
dotnet add package Microsoft.EntityFrameworkCore dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL dotnet add package Microsoft.EntityFrameworkCore.Sqlite这里有个容易踩的坑:不同数据库的EF Core包版本必须保持一致。我遇到过因为Npgsql版本比Sqlite高导致迁移失败的情况。建议在.csproj中显式指定相同版本:
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" />2.2 开发环境配置
对于PostgreSQL,我推荐使用Docker快速搭建:
docker run --name pg-demo -e POSTGRES_PASSWORD=yourpassword -p 5432:5432 -d postgres:15SQLite则不需要额外服务,但建议安装DB Browser for SQLite作为可视化工具。调试时可以直接查看生成的.db文件内容,比命令行方便很多。
3. 多DbContext实现方案
3.1 独立DbContext类设计
为每个数据库创建单独的DbContext派生类是最清晰的方案:
// PostgreSQL的DbContext public class PgDbContext : DbContext { public DbSet<Order> Orders { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseNpgsql("Host=localhost;Database=commerce;Username=postgres;Password=yourpassword"); } // SQLite的DbContext public class LiteDbContext : DbContext { public DbSet<LocalCache> Caches { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlite("Data Source=local.db"); }3.2 依赖注入配置技巧
在Startup.cs中这样注册多个DbContext:
services.AddDbContext<PgDbContext>(options => options.UseNpgsql(Configuration.GetConnectionString("PgConnection"))); services.AddDbContext<LiteDbContext>(options => options.UseSqlite(Configuration.GetConnectionString("LiteConnection")));注意要使用不同的上下文类型名称,否则DI容器会混淆。我曾经因为都命名为"AppDbContext"导致注入时获取到错误的实例。
4. 跨数据库事务处理
4.1 分布式事务的替代方案
EF Core不支持跨不同数据库类型的事务,但可以通过补偿机制实现最终一致性。比如处理订单时:
try { // 先提交PostgreSQL using var pgTransaction = await _pgContext.Database.BeginTransactionAsync(); _pgContext.Orders.Add(newOrder); await _pgContext.SaveChangesAsync(); // 再处理SQLite _liteContext.LocalCaches.Add(new CacheEntry(orderId)); await _liteContext.SaveChangesAsync(); await pgTransaction.CommitAsync(); } catch { // 记录失败状态,后续通过后台任务补偿 _logger.LogError("Cross-db operation failed"); throw; }4.2 数据同步策略
对于需要双向同步的数据,建议采用以下模式:
- 为每个同步实体添加Version字段
- 客户端修改SQLite数据后标记为NeedSync
- 后台服务定期扫描NeedSync记录批量同步到PostgreSQL
- 使用时间戳解决冲突(以服务器时间为准)
5. 迁移管理实践
5.1 分离迁移项目
在解决方案中创建两个迁移项目:
src/ ├── Migrations/ │ ├── PgMigrations/ (dotnet ef migrations add InitialCreate --context PgDbContext) │ └── LiteMigrations/ (dotnet ef migrations add InitialCreate --context LiteDbContext)这样能避免迁移命令混淆。执行时需要指定--project参数:
dotnet ef database update --project src/Migrations/PgMigrations5.2 迁移文件差异处理
PostgreSQL迁移可能会包含特定语法:
migrationBuilder.AlterColumn<string>( name: "Description", table: "Products", type: "text", nullable: true);而SQLite的迁移会更简单,因为SQLite的类型系统比较宽松。但要注意SQLite不支持ALTER TABLE的某些操作,可能需要手动编写迁移代码。
6. 性能优化要点
6.1 连接池配置
对于PostgreSQL,在连接字符串中添加池大小参数:
Host=localhost;...;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50;SQLite则需要注意并发写入问题,建议启用WAL模式:
options.UseSqlite("Data Source=local.db;Cache=Shared;Mode=ReadWriteCreate;Journal Mode=Wal");6.2 查询优化对比
PostgreSQL的复杂查询:
var orders = await _pgContext.Orders .Include(o => o.Items) .Where(o => o.CreateDate > DateTime.UtcNow.AddDays(-7)) .AsNoTracking() .ToListAsync();SQLite的简单查询:
var localData = await _liteContext.Caches .Where(c => c.UserId == currentUserId) .Take(100) .ToListAsync();注意SQLite对复杂LINQ查询的支持有限,遇到性能问题时应简化查询或切换内存计算。
7. 实际案例:电商系统实现
最近实现的电商平台中,我们这样划分数据存储:
PostgreSQL存储:
- 用户账户
- 商品目录
- 订单记录
- 支付流水
SQLite存储:
- 用户浏览历史
- 购物车草稿
- 离线订单缓存
- 客户端个性化设置
这种架构下,即使用户网络不稳定,也能流畅浏览商品并将订单暂存本地,等网络恢复后自动同步到服务器。
8. 常见问题解决方案
8.1 连接字符串管理
建议使用appsettings.json分层配置:
{ "ConnectionStrings": { "PgConnection": "Host=localhost;...", "LiteConnection": "Data Source=local.db" }, "DbOptions": { "SqlitePageSize": 4096, "PgTimeout": 30 } }8.2 并发冲突处理
PostgreSQL使用行版本控制:
try { var product = await _pgContext.Products.FindAsync(id); product.Stock -= quantity; await _pgContext.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { // 处理库存冲突 }SQLite则需要更主动的检查:
var cache = await _liteContext.Caches.FindAsync(key); if (cache.Version != clientVersion) { // 提示数据已变更 }9. 调试与监控
9.1 日志配置
在DbContext配置中添加日志:
options.UseNpgsql(connectionString) .LogTo(Console.WriteLine, LogLevel.Information); options.UseSqlite(connectionString) .EnableSensitiveDataLogging();9.2 性能监控
对于PostgreSQL,可以使用pg_stat_statements扩展收集查询统计。SQLite则可以通过自定义拦截器记录执行时间:
public class SqliteInterceptor : DbCommandInterceptor { public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync( DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default) { var stopwatch = Stopwatch.StartNew(); return base.ReaderExecutingAsync(command, eventData, result, cancellationToken); } }10. 进阶技巧:动态切换
某些场景需要运行时切换数据库。我们可以创建DbContext工厂:
public interface IDbContextFactory { DbContext CreateDbContext(DbType dbType); } public enum DbType { PostgreSQL, SQLite }实现类根据参数返回不同的DbContext实例。这在多租户系统中特别有用,可以根据租户配置决定使用哪种数据库。
经过多个项目的实践验证,这种双数据库架构既能满足企业级应用的数据完整性要求,又能提供良好的客户端体验。关键是要明确数据边界,建立清晰的同步机制。当PostgreSQL遇到SQLite,它们不是竞争对手,而是互补的合作伙伴。