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. 关键注意事项
- 延迟处理:重要业务读取建议强制走主库
conn.ReloadTypes(); // 强制刷新元数据
- 连接池配置:建议主从独立配置连接池
builder.ConnectionString = masterConn; builder.MaxPoolSize = 100; // 主库连接数
- 监控指标:需要监控的关键指标:
- 主从延迟时间
- 连接池利用率
- 路由错误次数
9. 最佳实践建议
- 生产环境建议至少配置2个从库
- 读写操作比例建议控制在1:5以上
- 定期执行从库健康检查
- 重要业务操作添加重试机制
10. 总结与展望
通过Npgsql实现读写分离就像给数据库系统装上智能导航,既保持了代码的简洁性,又获得了性能的显著提升。随着.NET 8对PostgreSQL的深度支持,未来可以期待更智能的路由策略和自动扩缩容能力。建议开发者在实际应用中结合具体业务场景,灵活调整负载策略和参数配置。