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

假设你经营一家外卖平台,每天有10万用户查询餐厅菜单(读操作),但只有1万用户实际下单(写操作)。如果所有请求都打到同一个数据库,就像让一个主厨同时负责炒菜和接单,效率必然低下。读写分离的核心思想是将读操作和写操作分发到不同的数据库实例,从而提升系统吞吐量。本文将通过C#和MySqlConnector驱动,手把手教你实现这一架构。


2. 读写分离的基本原理

  • 主库(Master):处理所有写操作(INSERT/UPDATE/DELETE)
  • 从库(Replica):通过主从复制同步数据,处理读操作(SELECT)
  • 路由策略:根据SQL类型自动选择连接目标

3.为什么选择MySqlConnector?

  • 官方推荐:MySQL官方推出的.NET Core驱动(替代旧版MySql.Data)
  • 性能优势:异步IO支持完善,连接池管理更高效
  • 扩展性:内置负载均衡和故障转移策略

4. 环境准备

MySQL Server 8.0.32(一主一从)
.NET Core 6.0
MySqlConnector 2.2.6

5. 基础实现:四步完成读写分离

5.1 配置连接字符串
// 主库连接字符串
var master = "Server=master.example.com;Database=shop;User=admin;Password=P@ssw0rd;";
// 从库连接字符串(支持多个)
var replicas = new[] {
    "Server=replica1.example.com;Database=shop;User=reader;Password=Reader123;",
    "Server=replica2.example.com;Database=shop;User=reader;Password=Reader123;"
};
5.2 创建连接工厂
using MySqlConnector;

var builder = new MySqlConnectionStringBuilder(master)
{
    // 启用负载均衡
    LoadBalance = MySqlLoadBalance.RoundRobin,
    // 自动从库列表
    ReplicaServers = { new ReplicaServer(replicas[0]), new ReplicaServer(replicas[1]) }
};
5.3 执行查询的示例
public async Task<List<Order>> GetUserOrders(int userId)
{
    using var connection = new MySqlConnection(builder.ConnectionString);
    await connection.OpenAsync();

    // 自动路由到从库
    var cmd = new MySqlCommand("SELECT * FROM orders WHERE user_id=@id", connection);
    cmd.Parameters.AddWithValue("@id", userId);

    var orders = new List<Order>();
    using var reader = await cmd.ExecuteReaderAsync();
    while (await reader.ReadAsync())
    {
        orders.Add(new Order(
            reader.GetInt32("id"),
            reader.GetDateTime("create_time")
        ));
    }
    return orders;
}
5.4 执行写入的示例
public async Task CreateOrder(Order order)
{
    using var connection = new MySqlConnection(builder.ConnectionString);
    await connection.OpenAsync();

    // 自动路由到主库
    var cmd = new MySqlCommand("INSERT INTO orders (...) VALUES (...)", connection);
    // 添加参数...
    
    await cmd.ExecuteNonQueryAsync();
}

6. 高级配置:应对真实场景

6.1 故障转移策略
builder.RetryCount = 3; // 失败时重试次数
builder.ServerRedirectionMode = ServerRedirectionMode.Enabled; // 支持主库重定向
6.2 读写分离的例外处理
// 强制指定主库查询(适用于需要实时数据的场景)
var cmd = new MySqlCommand("SELECT * FROM orders FORCE_MASTER", connection);

7. 关联技术:提升方案健壮性

7.1 连接池优化
// 在Startup.cs中配置全局连接池
services.AddMySqlDataSource(
    builder.ConnectionString,
    static settings => settings.MinimumPoolSize = 10
);
7.2 与Dapper整合
public async Task<User> GetUser(int id)
{
    using var connection = await _dataSource.OpenConnectionAsync();
    return await connection.QueryFirstOrDefaultAsync<User>(
        "SELECT * FROM users WHERE id=@id", 
        new { id }
    );
}

8. 典型应用场景

  • 高并发读场景:新闻类APP的文章浏览
  • 报表分析系统:不影响线上交易的统计查询
  • 微服务架构:订单服务写主库,支付服务读从库

9. 技术方案优缺点分析

优点:

  • 读性能线性扩展
  • 故障隔离,主库崩溃不影响读服务
  • 硬件成本优化(从库可使用低配服务器)

缺点:

  • 主从同步延迟(通常1-5秒)
  • 事务跨库操作复杂
  • 需额外维护从库状态

10. 注意事项与避坑指南

  1. 延迟容忍度:用户个人中心页可接受延迟,但支付状态需实时
  2. 事务陷阱:开启事务后所有操作强制走主库
  3. 连接泄露:务必使用usingDispose()释放连接
  4. 监控指标:重点关注Seconds_Behind_Master和线程池利用率

11. 总结与展望

通过MySqlConnector实现读写分离,就像给数据库装上了交通信号灯。虽然初期配置需要细心调试,但带来的性能提升是显著的。未来可结合ProxySQL实现更智能的路由,或在Kubernetes中动态扩展从库实例。