1. 为什么需要读写分离?

当我们开发的系统遇到数据量暴增时,就像高速公路突然涌入大量车辆,单一数据库服务器就像只有一条收费通道,很快就会形成性能瓶颈。特别是电商大促期间,订单表的写入操作和商品浏览的读取操作同时激增,这时读写分离就能像交通分流一样,让写入走专用通道(主库),读取走多个辅道(从库)。

2. 技术栈选择与准备

本方案采用.NET 6 + Npgsql 7.0 + PostgreSQL 14组合:

  • Npgsql是.NET平台最成熟的PostgreSQL驱动
  • PostgreSQL需预先配置好主从复制(可用pgpool或内置流复制)
  • 建议使用连接池(Npgsql内置连接池)

3. 基础版读写分离实现

// 配置主从连接字符串
const string masterConn = "Host=master.pg;Username=admin;Password=123456;Database=shop";
const string replica1Conn = "Host=replica1.pg;Username=admin;Password=123456;Database=shop";
const string replica2Conn = "Host=replica2.pg;Username=admin;Password=123456;Database=shop";

// 创建数据源构建器
var builder = new NpgsqlDataSourceBuilder();
builder.UseLoggerFactory(LoggerFactory.Create(b => b.AddConsole())); // 启用日志

// 配置读写分离策略
builder.UseReplicas(
    replicationMode: ReplicationMode.LoadBalance, // 负载均衡模式
    defaultConnectionString: masterConn,          // 主库连接
    replicaConnectionStrings: new[] { replica1Conn, replica2Conn }, // 从库列表
    loadBalancingDelay: TimeSpan.FromSeconds(30)  // 负载检测间隔
);

// 创建数据源
var dataSource = builder.Build();

// 执行写操作(自动路由到主库)
await using (var conn = dataSource.OpenConnection())
{
    var cmd = new NpgsqlCommand("INSERT INTO orders (user_id, amount) VALUES (@u, @a)", conn);
    cmd.Parameters.AddWithValue("u", 1001);
    cmd.Parameters.AddWithValue("a", 299.99m);
    await cmd.ExecuteNonQueryAsync();
}

// 执行读操作(自动选择从库)
await using (var conn = await dataSource.OpenConnectionAsync())
{
    var cmd = new NpgsqlCommand("SELECT * FROM products WHERE category = @c", conn);
    cmd.Parameters.AddWithValue("c", "electronics");
    using var reader = await cmd.ExecuteReaderAsync();
    while (await reader.ReadAsync())
    {
        Console.WriteLine(reader.GetString(1));
    }
}

4. 进阶负载均衡实现

// 自定义负载均衡策略
public class CustomLoadBalancer : ILoadBalancer
{
    private readonly string[] _replicas;
    private int _counter;

    public CustomLoadBalancer(IEnumerable<string> replicas)
    {
        _replicas = replicas.ToArray();
    }

    public string GetNext()
    {
        // 使用轮询算法选择从库
        var index = Interlocked.Increment(ref _counter) % _replicas.Length;
        return _replicas[index];
    }
}

// 注册自定义策略
builder.UseReplicas(
    replicationMode: ReplicationMode.LoadBalance,
    defaultConnectionString: masterConn,
    replicaConnectionStrings: new[] { replica1Conn, replica2Conn },
    loadBalancer: new CustomLoadBalancer(new[] { replica1Conn, replica2Conn })
);

5. 关键技术点解析

5.1 事务处理策略

在显式事务中自动路由到主库:

await using var conn = await dataSource.OpenConnectionAsync();
using var transaction = await conn.BeginTransactionAsync(); // 自动使用主库
try {
    // 混合操作
    await ExecuteWriteOperation(conn);
    await ExecuteReadOperation(conn);
    await transaction.CommitAsync();
} catch {
    await transaction.RollbackAsync();
}

5.2 健康检查机制

// 配置健康检查参数
builder.UseReplicas(
    healthCheckInterval: TimeSpan.FromMinutes(5),
    healthCheckTimeout: TimeSpan.FromSeconds(10)
);

// 手动触发检查
var firstReplica = dataSource.Replicas.First();
if (await firstReplica.CheckHealthAsync())
{
    Console.WriteLine("从库状态正常");
}

6. 应用场景分析

  • 高并发读取场景:用户评论展示、商品信息查询
  • 报表生成系统:不影响线上交易的批量查询
  • 微服务架构:不同服务按需使用数据库连接
  • AB测试场景:读写流量按比例分配

7. 技术方案优缺点

优势:

  • 自动路由机制减少代码侵入
  • 支持多种负载均衡算法
  • 内置连接池提升性能
  • 完善的异常处理机制

挑战:

  • 主从同步延迟需要特殊处理
  • 复杂事务可能引发路由异常
  • 从库故障切换需要精细控制

8. 关键注意事项

  1. 延迟处理:重要业务读取建议强制走主库
    conn.ReloadTypes(); // 强制刷新元数据
    
  2. 连接池配置:建议主从独立配置连接池
    builder.ConnectionString = masterConn;
    builder.MaxPoolSize = 100; // 主库连接数
    
  3. 监控指标:需要监控的关键指标:
    • 主从延迟时间
    • 连接池利用率
    • 路由错误次数

9. 最佳实践建议

  • 生产环境建议至少配置2个从库
  • 读写操作比例建议控制在1:5以上
  • 定期执行从库健康检查
  • 重要业务操作添加重试机制

10. 总结与展望

通过Npgsql实现读写分离就像给数据库系统装上智能导航,既保持了代码的简洁性,又获得了性能的显著提升。随着.NET 8对PostgreSQL的深度支持,未来可以期待更智能的路由策略和自动扩缩容能力。建议开发者在实际应用中结合具体业务场景,灵活调整负载策略和参数配置。