Appearance
分库分表方案(结合第三方组件)
概述
随着数据量增长,单表或单数据库会遇到性能瓶颈。分库分表(Sharding)是将数据水平拆分到多个数据库或表的策略,以提升系统可扩展性和性能。
核心挑战
- 路由策略: 如何确定数据存储在哪个分片?
- 跨分片查询: 如何高效查询所有分片的数据?
- 分布式事务: 如何保证跨分片操作的一致性?
- 数据迁移: 如何在不停机的情况下重新分片?
方案 1: ShardingCore (推荐)
ShardingCore 是专为 EF Core 设计的分片库。
安装
bash
dotnet add package ShardingCore配置分片路由
csharp
public class OrderVirtualRoute : AbstractSimpleShardingModKeyStringVirtualTableRoute<Order>
{
public override void Configure(EntityMetadataTableBuilder<Order> builder)
{
builder.ShardingProperty(o => o.UserId); // 按 UserId 分片
}
public override ModKeyConfig GetModKeyConfig()
{
return new ModKeyConfig { Mod = 10 }; // 10 个分片表
}
}注册服务
csharp
builder.Services.AddShardingDbContext<AppDbContext>((config, options) =>
{
config.UseSqlServer(connectionString);
})
.AddDefaultSharding();
app.UseSharding(); // 启动时初始化使用
csharp
// 自动路由到正确的分片
var orders = await _context.Orders
.Where(o => o.UserId == userId)
.ToListAsync();方案 2: 手动实现分片
分片解析器
csharp
public interface IShardResolver
{
string ResolveShard(string shardingKey);
}
public class HashShardResolver : IShardResolver
{
private readonly int _shardCount;
public HashShardResolver(int shardCount = 10)
{
_shardCount = shardCount;
}
public string ResolveShard(string shardingKey)
{
var hash = Math.Abs(shardingKey.GetHashCode());
var shardIndex = hash % _shardCount;
return $"Shard_{shardIndex:D2}";
}
}分片 DbContext Factory
csharp
public class ShardedDbContextFactory
{
private readonly IShardResolver _resolver;
private readonly IConfiguration _config;
public AppDbContext CreateForShard(string shardingKey)
{
var shardName = _resolver.ResolveShard(shardingKey);
var connectionString = _config.GetConnectionString(shardName);
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer(connectionString)
.Options;
return new AppDbContext(options);
}
}仓储封装
csharp
public class ShardedOrderRepository
{
private readonly ShardedDbContextFactory _factory;
public async Task AddAsync(Order order)
{
using var context = _factory.CreateForShard(order.UserId);
context.Orders.Add(order);
await context.SaveChangesAsync();
}
public async Task<List<Order>> GetByUserIdAsync(string userId)
{
using var context = _factory.CreateForShard(userId);
return await context.Orders
.Where(o => o.UserId == userId)
.ToListAsync();
}
}方案 3: 读写分离
主从复制架构
┌──────────┐
│ Master │ ← 写入
└────┬─────┘
│
┌───────┼───────┐
▼ ▼ ▼
Slave1 Slave2 Slave3 ← 读取实现读写分离
csharp
public class ReadWriteSplittingContext : DbContext
{
private readonly IConfiguration _configuration;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (IsReadOperation())
{
// 随机选择一个从库
var slaveConnection = GetRandomSlaveConnection();
optionsBuilder.UseSqlServer(slaveConnection);
}
else
{
// 使用主库
var masterConnection = _configuration.GetConnectionString("Master");
optionsBuilder.UseSqlServer(masterConnection);
}
}
private bool IsReadOperation()
{
// 检查调用堆栈判断是否为读操作
var stackTrace = new StackTrace();
return stackTrace.ToString().Contains("Get") ||
stackTrace.ToString().Contains("Query");
}
}最佳实践
✅ 推荐做法
- 选择合适的分片键: 常用查询字段、高基数字段
- 避免跨分片JOIN: 尽量在应用层组装数据
- 监控分片均衡: 定期检查各分片数据量
- 预留扩展空间: 设计时考虑未来扩容
❌ 避免的陷阱
- 不要过度分片: 小数据量不需要分片
- 不要忽略分布式事务: 使用 Saga 或 TCC 模式
- 不要忘记索引: 每个分片都需要独立索引
总结
| 方案 | 适用场景 | 复杂度 | 灵活性 |
|---|---|---|---|
| ShardingCore | 中小型项目 | ⭐⭐ 低 | ⭐⭐⭐ 高 |
| 手动实现 | 定制化需求 | ⭐⭐⭐ 高 | ⭐⭐⭐⭐⭐ 最高 |
| 中间件(MyCat) | 大型分布式系统 | ⭐⭐⭐⭐ 很高 | ⭐⭐⭐⭐ 高 |
根据业务规模和团队技术能力选择合适的方案!