在C#项目里,当业务量增长导致数据库读写压力增大时,读写分离是常用的优化方案,核心思路是将写操作指向主库,读操作指向多个从库,从而分散数据库负载。EF Core作为常用的ORM框架,支持通过多数据库上下文配置实现读写分离。

读写分离的核心原理
读写分离依赖主从数据库架构,主库负责处理增删改等写操作,从库通过主从复制同步主库数据,负责处理查询等读操作。在EF Core中实现读写分离,需要解决两个核心问题:一是区分当前操作是读还是写,二是根据操作类型选择对应的数据库连接。
操作类型判断规则
通常我们可以通过以下规则判断操作类型:
- 写操作:调用
SaveChanges、SaveChangesAsync方法,或者执行INSERT、UPDATE、DELETE语句 - 读操作:调用
Where、FirstOrDefault、ToList等查询方法,或者执行SELECT语句
EF Core读写分离基础配置
首先我们需要定义主从库的配置信息,然后在EF Core中注册多个数据库上下文,分别对应主库和从库。
配置主从库连接字符串
在appsettings.json中添加主从库的连接配置:
{
"ConnectionStrings": {
"MasterDb": "Server=127.0.0.1;Database=TestDb;User Id=sa;Password=123456;",
"SlaveDb1": "Server=192.168.0.1;Database=TestDb;User Id=sa;Password=123456;",
"SlaveDb2": "Server=192.168.0.2;Database=TestDb;User Id=sa;Password=123456;"
}
}
定义数据库上下文基类
先定义一个基础的数据库上下文,后续主库和从库上下文都继承该类:
using Microsoft.EntityFrameworkCore;
namespace ReadWriteSplitDemo.Data
{
public class BaseDbContext : DbContext
{
public BaseDbContext(DbContextOptions options) : base(options)
{
}
// 定义测试用的实体集合
public DbSet<User> Users { get; set; }
}
// 用户实体
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
}
实现读写分离上下文工厂
我们需要一个工厂类来根据操作类型返回对应的数据库上下文,这里通过判断当前是否处于写操作状态来选择主库还是从库。
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System;
namespace ReadWriteSplitDemo.Data
{
// 操作类型枚举
public enum DbOperationType
{
Read,
Write
}
// 上下文工厂类
public class DbContextFactory
{
private readonly IServiceProvider _serviceProvider;
// 存储当前请求的操作类型,默认是读操作
private static AsyncLocal<DbOperationType> _currentOperationType = new AsyncLocal<DbOperationType>();
public DbContextFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
// 获取当前操作类型
public static DbOperationType CurrentOperationType
{
get => _currentOperationType.Value;
set => _currentOperationType.Value = value;
}
// 获取对应的数据库上下文
public BaseDbContext GetDbContext()
{
if (CurrentOperationType == DbOperationType.Write)
{
// 写操作返回主库上下文
var options = _serviceProvider.GetRequiredService<DbContextOptions<MasterDbContext>>();
return new MasterDbContext(options);
}
else
{
// 读操作随机选择一个从库上下文,实现负载均衡
var slaveDbs = new[] { "SlaveDb1", "SlaveDb2" };
var random = new Random();
var selectedSlave = slaveDbs[random.Next(slaveDbs.Length)];
var options = _serviceProvider.GetRequiredService<DbContextOptions<SlaveDbContext>>();
return new SlaveDbContext(options, selectedSlave);
}
}
}
// 主库上下文
public class MasterDbContext : BaseDbContext
{
public MasterDbContext(DbContextOptions options) : base(options)
{
}
}
// 从库上下文
public class SlaveDbContext : BaseDbContext
{
private readonly string _connectionName;
public SlaveDbContext(DbContextOptions options, string connectionName) : base(options)
{
_connectionName = connectionName;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
// 这里实际项目中可以通过配置获取对应从库的连接字符串
// 示例简化,实际需结合IConfiguration获取
}
base.OnConfiguring(optionsBuilder);
}
}
}
注册服务与中间件配置
在Program.cs中注册相关服务,并添加中间件来自动识别写操作,切换操作类型。
using Microsoft.EntityFrameworkCore;
using ReadWriteSplitDemo.Data;
var builder = WebApplication.CreateBuilder(args);
// 注册主库上下文
builder.Services.AddDbContext<MasterDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("MasterDb"));
});
// 注册从库上下文
builder.Services.AddDbContext<SlaveDbContext>(options =>
{
// 从库连接字符串后续在上下文内部动态选择
options.UseSqlServer(builder.Configuration.GetConnectionString("SlaveDb1"));
});
// 注册上下文工厂
builder.Services.AddScoped<DbContextFactory>();
var app = builder.Build();
// 添加中间件,识别写操作
app.Use(async (context, next) =>
{
// 判断请求是否为写操作,这里简单通过请求方法和路径判断,实际可结合业务调整
if (context.Request.Method == "POST" || context.Request.Method == "PUT" || context.Request.Method == "DELETE")
{
DbContextFactory.CurrentOperationType = DbOperationType.Write;
}
else
{
DbContextFactory.CurrentOperationType = DbOperationType.Read;
}
await next();
});
app.MapGet("/", () => "Hello World!");
// 测试读接口
app.MapGet("/users", async (DbContextFactory factory) =>
{
using var dbContext = factory.GetDbContext();
return await dbContext.Users.ToListAsync();
});
// 测试写接口
app.MapPost("/users", async (DbContextFactory factory, User user) =>
{
using var dbContext = factory.GetDbContext();
dbContext.Users.Add(user);
await dbContext.SaveChangesAsync();
return user;
});
app.Run();
注意事项与优化建议
实际生产环境中使用EF Core实现读写分离还需要注意以下几点:
- 主从复制存在延迟,写操作后立刻读的场景可能需要强制走主库,避免读取到旧数据
- 从库可以配置多个,通过加权轮询、最少连接数等算法实现更合理的负载均衡
- 事务操作必须走主库,避免事务跨库导致一致性问题
- 可以结合AOP切面编程,在方法层面标记操作类型,更灵活地控制读写路由
读写分离只是数据库优化的一种方式,当单表数据量过大时,还需要结合分库分表等方案进一步提升性能。