1. 为什么需要双数据库架构?
在.NET Core应用开发中,单一数据库选型往往难以满足所有场景需求。PostgreSQL作为功能完备的关系型数据库,适合处理复杂查询和高并发写入;而SQLite作为轻量级嵌入式数据库,则在本地存储和快速读写场景中表现优异。两者的组合能覆盖从服务端到客户端的完整数据管理需求。
我最近在一个物联网数据采集项目中就采用了这种架构:用PostgreSQL作为中心服务器存储所有设备的历史数据,同时在每个边缘设备上部署SQLite用于暂存本地采集数据。这种设计既保证了数据可靠性,又避免了网络不稳定时的数据丢失风险。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与基础配置
2.1 项目初始化与依赖安装
首先创建.NET Core Web API项目,并添加必要的NuGet包:
bash复制dotnet new webapi -n DualDatabaseDemo
cd DualDatabaseDemo
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
在appsettings.json中配置双数据库连接字符串:
json复制{
"ConnectionStrings": {
"PostgresConnection": "Host=localhost;Database=app_db;Username=postgres;Password=yourpassword",
"SqliteConnection": "Data Source=localdata.db"
}
}
2.2 DbContext的巧妙设计
核心技巧在于创建支持多数据库的DbContext工厂。我通常会定义接口来抽象数据操作:
csharp复制public interface IAppDbContext
{
DbSet<Device> Devices { get; set; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}
// PostgreSQL实现
public class PostgresDbContext : DbContext, IAppDbContext
{
public PostgresDbContext(DbContextOptions<PostgresDbContext> options)
: base(options) {}
public DbSet<Device> Devices { get; set; }
}
// SQLite实现
public class SqliteDbContext : DbContext, IAppDbContext
{
public SqliteDbContext(DbContextOptions<SqliteDbContext> options)
: base(options) {}
public DbSet<Device> Devices { get; set; }
}
在Program.cs中注册服务时,使用不同的配置:
csharp复制// PostgreSQL配置
builder.Services.AddDbContext<PostgresDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("PostgresConnection")));
// SQLite配置
builder.Services.AddDbContext<SqliteDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("SqliteConnection")));
3. 数据同步策略实现
3.1 双向同步的挑战与解决方案
实现双数据库架构最复杂的部分在于数据同步。经过多次实践,我总结出以下几种可靠模式:
- 定时批处理同步:适合数据量大的场景
csharp复制public class DataSyncService : IHostedService
{
private readonly IAppDbContext _postgres;
private readonly IAppDbContext _sqlite;
public async Task SyncDataAsync()
{
// 从SQLite获取待同步数据
var pendingData = await _sqlite.Devices
.Where(x => !x.IsSynced)
.ToListAsync();
// 批量插入PostgreSQL
await _postgres.Devices.AddRangeAsync(pendingData);
await _postgres.SaveChangesAsync();
// 标记为已同步
pendingData.ForEach(x => x.IsSynced = true);
await _sqlite.SaveChangesAsync();
}
}
- 事件驱动同步:实时性要求高的场景
csharp复制public class DeviceService
{
private readonly IAppDbContext _postgres;
private readonly IAppDbContext _sqlite;
public async Task AddDeviceAsync(Device device)
{
// 先写入SQLite
await _sqlite.Devices.AddAsync(device);
await _sqlite.SaveChangesAsync();
// 触发后台同步
_ = Task.Run(async () => {
try {
await _postgres.Devices.AddAsync(device);
await _postgres.SaveChangesAsync();
device.IsSynced = true;
await _sqlite.SaveChangesAsync();
} catch (Exception ex) {
// 记录失败,下次重试
}
});
}
}
3.2 冲突处理机制
当两端同时修改数据时,需要设计冲突解决策略。我常用的方法包括:
- 时间戳优先:取最新修改的数据
csharp复制if (localEntity.LastModified > serverEntity.LastModified)
{
// 用本地数据覆盖服务端
}
- 人工干预队列:将冲突数据放入特殊表供人工处理
csharp复制public async Task HandleConflictAsync(Device local, Device remote)
{
var conflict = new DataConflict {
LocalData = JsonSerializer.Serialize(local),
RemoteData = JsonSerializer.Serialize(remote),
Resolution = ConflictResolution.Pending
};
await _context.Conflicts.AddAsync(conflict);
await _context.SaveChangesAsync();
}
4. 性能优化实战技巧
4.1 批量操作优化
SQLite在大量写入时性能下降明显,需要特殊处理:
csharp复制// 不好的做法:逐条插入
foreach (var item in dataList)
{
await _sqlite.Devices.AddAsync(item);
await _sqlite.SaveChangesAsync();
}
// 推荐做法:批量事务
using (var transaction = await _sqlite.Database.BeginTransactionAsync())
{
try {
await _sqlite.Devices.AddRangeAsync(dataList);
await _sqlite.SaveChangesAsync();
await transaction.CommitAsync();
} catch {
await transaction.RollbackAsync();
throw;
}
}
4.2 读写分离策略
对于查询密集型操作,可以设计智能路由:
csharp复制public class SmartDbContextRouter
{
private readonly IAppDbContext _postgres;
private readonly IAppDbContext _sqlite;
public IAppDbContext GetDbContext(bool isWriteOperation)
{
return isWriteOperation ? _postgres : _sqlite;
}
}
4.3 连接池管理
PostgreSQL连接池配置建议:
csharp复制services.AddDbContextPool<PostgresDbContext>(options =>
options.UseNpgsql(connectionString, npgsqlOptions =>
npgsqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorCodesToAdd: null
)),
poolSize: 128); // 根据实际负载调整
5. 实际踩坑与解决方案
5.1 事务一致性难题
在分布式场景下维护ACID特性是个挑战。我的解决方案是采用Saga模式:
csharp复制public async Task TransferDataSagaAsync(Guid batchId)
{
// 阶段1:准备
var batch = await _sqlite.Batches.FindAsync(batchId);
batch.Status = BatchStatus.Preparing;
await _sqlite.SaveChangesAsync();
// 阶段2:提交到PostgreSQL
try {
await _postgres.Batches.AddAsync(batch);
await _postgres.SaveChangesAsync();
batch.Status = BatchStatus.Committed;
}
catch {
batch.Status = BatchStatus.Failed;
await _sqlite.SaveChangesAsync();
return;
}
// 阶段3:确认
batch.Status = BatchStatus.Confirmed;
await _sqlite.SaveChangesAsync();
}
5.2 SQLite并发限制
SQLite默认只支持单个写操作,在高并发场景下需要特殊处理:
csharp复制// 使用互斥锁保护写操作
private static readonly SemaphoreSlim _sqliteLock = new(1, 1);
public async Task SafeWriteAsync(Func<Task> operation)
{
await _sqliteLock.WaitAsync();
try {
await operation();
} finally {
_sqliteLock.Release();
}
}
5.3 模式迁移差异
PostgreSQL和SQLite的Schema迁移存在差异,建议:
- 为每个数据库单独维护迁移
- 使用条件判断处理差异:
csharp复制protected override void OnModelCreating(ModelBuilder modelBuilder)
{
if (Database.IsSqlite())
{
// SQLite特有配置
modelBuilder.Entity<Device>()
.Property(x => x.Id)
.HasDefaultValueSql("NEWID()");
}
else
{
// PostgreSQL特有配置
modelBuilder.Entity<Device>()
.Property(x => x.Id)
.HasDefaultValueSql("gen_random_uuid()");
}
}
6. 监控与维护方案
6.1 健康检查配置
在Program.cs中添加:
csharp复制builder.Services.AddHealthChecks()
.AddNpgSql(builder.Configuration.GetConnectionString("PostgresConnection"))
.AddSqlite(builder.Configuration.GetConnectionString("SqliteConnection"));
app.MapHealthChecks("/health");
6.2 性能监控
使用MiniProfiler监控双数据库性能:
csharp复制builder.Services.AddMiniProfiler(options =>
{
options.RouteBasePath = "/profiler";
options.TrackConnectionOpenClose = true;
}).AddEntityFramework();
app.UseMiniProfiler();
6.3 备份策略
SQLite自动备份方案示例:
csharp复制public class SqliteBackupService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var backupPath = $"localdata-backup-{DateTime.Now:yyyyMMdd}.db";
File.Copy("localdata.db", backupPath, overwrite: true);
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
}
}
}
7. 进阶应用场景
7.1 多租户架构实现
结合PostgreSQL的SCHEMA和SQLite的文件特性,可以实现灵活的多租户方案:
csharp复制public class TenantAwareDbContextFactory
{
public IAppDbContext CreateForTenant(string tenantId)
{
if (tenantId == "central")
{
var options = new DbContextOptionsBuilder<PostgresDbContext>()
.UseNpgsql(_config.GetConnectionString("PostgresConnection"))
.Options;
return new PostgresDbContext(options);
}
else
{
var options = new DbContextOptionsBuilder<SqliteDbContext>()
.UseSqlite($"Data Source=tenant_{tenantId}.db")
.Options;
return new SqliteDbContext(options);
}
}
}
7.2 离线优先应用
对于需要离线工作的应用,可以采用以下模式:
- 所有写操作先到SQLite
- 网络恢复时自动同步到PostgreSQL
- 冲突数据进入待处理队列
实现代码参考:
csharp复制public class OfflineFirstRepository
{
public async Task<T> AddAsync<T>(T entity) where T : class, ISyncEntity
{
// 标记为未同步
entity.IsSynced = false;
// 保存到SQLite
_sqlite.Set<T>().Add(entity);
await _sqlite.SaveChangesAsync();
// 尝试后台同步
_ = TrySyncAsync();
return entity;
}
private async Task TrySyncAsync()
{
if (!IsOnline) return;
var unsynced = await _sqlite.Set<T>()
.Where(x => !x.IsSynced)
.ToListAsync();
foreach (var item in unsynced)
{
try {
_postgres.Set<T>().Add(item);
await _postgres.SaveChangesAsync();
item.IsSynced = true;
await _sqlite.SaveChangesAsync();
} catch {
// 记录错误,下次重试
}
}
}
}
在实际项目中,这种双数据库架构已经帮助我们成功处理了日均百万级的数据读写需求,同时保证了在网络不稳定地区的良好用户体验。关键在于根据业务特点选择合适的同步策略,并做好异常处理和监控。
