第三十四节:基于ShardingCore框架实现读写分离
一. 简介
1. 概念扫盲
读写分离、分库分表是完全不同的两回事,不要混为一谈。
分表:数据量大、存储和查询遇到瓶颈
分库:并发量大导致数据库连接数不足
分库且分表:数据库连接数不足且单表数据量大导致查询慢
读写分离:解决高并发读的问题。解决不了海量数据存储的问题。
主从同步:主从同步是一种数据库的架构技术,通过将主库的更新操作(插入/删除/更新)记录到二进制日志Binlog中,然后从库读取并解析这些日志,从而将主库的更改同步到从库中,使从库数据和主库数据保持一致。 主要用于:数据备份、读写分离、高可用场景。
注:mysql默认的主从架构,主库挂了,从库不会自动顶替上,需要借助第三方技术,比如配置MHA架构、MySQL Cluster架构,可以实现自动选举。
2. 什么是读写分离?
主从同步架构
(1). 先在数据库层面上搭建主从同步架构
(2). 然后基于第三方框架,如 shardingjdbc、shardingcore,
3. ShardingCore读写分离功能
(1). 通过配置,db上下文可以自动实现读操作去从库,写操作去主库。
(2). 可以手动切换,将读操作切换到主库、或从库。 但是写操作用于都是操作主库,无法切换的。
(3). 多个从库的读取策略:轮询和随机。
二. 实操
1. 配置1主2从数据库架构
主库为:ShardingDB_Main,两个从库:ShardingDB_Slave1、ShardingDB_Slave2,每个都有Order表,需要手动创建!!!!
SQLServer:详见 https://www.cnblogs.com/yaopengfei/p/13330482.html
MySQL:详见 https://www.cnblogs.com/yaopengfei/p/18197198
特别说明:这里为了测试效果更加明显,下面的测试 三个DB之间,没有配置主从架构。
order表SQL
CREATE TABLE [dbo].[Order](
[Id] [varchar](50) NOT NULL,
[Payer] [varchar](50) NOT NULL,
[Money] [bigint] NOT NULL,
[Area] [varchar](50) NOT NULL,
[OrderStatus] [int] NOT NULL,
[CreationTime] [datetime2](7) NOT NULL,
CONSTRAINT [PK_Order] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
2 DB字符串
{
"ConnectionStrings": {
"SQLServerStr1": "Server=47.101.xx.xx;Database=ShardingDB_Main;User=sa;Password=xxx;TrustServerCertificate=True;",
"SQLServerStr2": "Server=47.101.xx.xx;Database=ShardingDB_Slave1;User=sa;Password=xxx;TrustServerCertificate=True;",
"SQLServerStr3": "Server=47.101.xx.xx;Database=ShardingDB_Slave2;User=sa;Password=xxx;TrustServerCertificate=True;",
"MySQLStr": ""
},
"AllowedHosts": "*"
}
3 Progarm中注入文件
核心代码剖析:
(1). 默认数据源名称 和 读写分离配置中两个从库的名称 必须相同,这里都叫ds0
(2). ReadStrategyEnum.Loop:表示同一个数据源的从库链接读取策略为轮询一个接一个公平读取,
如果是 .Random : 表示针对同一个数据源获取链接采用随机策略
(3). defaultEnable: true 表示默认读取查询操作走从库,false表示默认读取查询操作还是走主库
(4). defaultPriority: 10 表示默认的读写分离优先级,先判断优先级, 再判断确定dbcontext是否启用读写分离(大于0即可)
(5). ReadConnStringGetStrategyEnum.LatestFirstTime: 表示针对同一个dbcontext只取一次从库链接,保证同一个dbcontext下的从库链接都是一样的,不会出现说查询主表数据存在但是第二次查询可能走的是其他从库导致明细表不存在等问题,所以建议大部分情况下使用LatestFirstTime
.LatestEveryTime: 表示每一次查询都是获取一次从库,但是可能会出现比如page的两次操作count+list结果和实际获取的不一致等情况,大部分情况下不会出现问题只是有可能会出现这种情况
//添加默认数据源
op.AddDefaultDataSource("ds0", SQLServerStr1);
//读写分离配置(名称必须和主数据源的一样,如都叫ds0)
op.AddReadWriteSeparation(sp =>
{
return new Dictionary<string, IEnumerable<string>>()
{
{"ds0",new List<string>(){SQLServerStr2,SQLServerStr3 }},
};
}, ReadStrategyEnum.Loop, defaultEnable: true, defaultPriority: 10, ReadConnStringGetStrategyEnum.LatestFirstTime);
完整代码:
using System.Reflection; namespace ShardingCoreDemo4; public class Program { public static readonly ILoggerFactory efLogger = LoggerFactory.Create(builder => { builder.AddFilter((category, level) => category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information).AddConsole(); }); public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); //注册OpenApi builder.Services.AddSwaggerGen(options => { var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); }); var SQLServerStr1 = builder.Configuration.GetConnectionString("SQLServerStr1"); var SQLServerStr2 = builder.Configuration.GetConnectionString("SQLServerStr2"); var SQLServerStr3 = builder.Configuration.GetConnectionString("SQLServerStr3"); /* ShardingCore相关配置 1. AddShardingDbContext配置包含了原生的AddDbContext+AddShardingConfigure,无需单独注入原生DBContext 2. DefaultShardingDbContext继承了原生DBContext,随意具有原生的所有功能 */ builder.Services.AddShardingDbContext<MyDbContext>() .UseRouteConfig(o => { //添加分库路由 //添加分表路由 }) .UseConfig(op => { #region 一些配置 【暂时注释,需要什么开启什么】 //{ // //当查询无法匹配到对应的路由是否抛出异常 true表示抛出异常 false表示返回默认值 // op.ThrowIfQueryRouteNotMatch = false; // //如果开启读写分离是否在savechange commit rollback后自动将dbcontext切换为写库 true表示是 false表示不是 // op.AutoUseWriteConnectionStringAfterWriteDb = false; // //创建表如果出现错误是否忽略掉错误不进行日志出输出 true表示忽略 false表示不忽略 // op.IgnoreCreateTableError = false; // //默认的系统默认迁移并发数,分库下会进行并发迁移 // op.MigrationParallelCount = Environment.ProcessorCount; // //补偿表并发线程数 补偿表用来进行最后一次补偿缺少的分片表 // op.CompensateTableParallelCount = Environment.ProcessorCount; // //默认最大连接数限制 如果出现跨分片查询需要开启n个链接,设置这个值会将x个链接分成每n个为一组进行同库串行执行 // op.MaxQueryConnectionsLimit = Environment.ProcessorCount; // //链接模式的选择系统自动 // op.ConnectionMode = ConnectionModeEnum.SYSTEM_AUTO; //} #endregion //如何通过字符串查询创建DbContext op.UseShardingQuery((conStr, builder) => { builder.UseSqlServer(conStr, o => o.UseCompatibilityLevel(120)).UseLoggerFactory(efLogger); }); //如何通过事务创建DbContext op.UseShardingTransaction((connection, builder) => { builder.UseSqlServer(connection, o => o.UseCompatibilityLevel(120)).UseLoggerFactory(efLogger); }); //添加默认数据源 op.AddDefaultDataSource("ds0", SQLServerStr1); //读写分离配置(名称必须和主数据源的一样,如都叫ds0) op.AddReadWriteSeparation(sp => { return new Dictionary<string, IEnumerable<string>>() { {"ds0",new List<string>(){SQLServerStr2,SQLServerStr3 }}, }; }, ReadStrategyEnum.Loop, defaultEnable: true, defaultPriority: 10, ReadConnStringGetStrategyEnum.LatestFirstTime); }) .AddShardingCore(); /***************************************下面是管道相关***************************************************/ var app = builder.Build(); IServiceProvider serviceProvider = app.Services; if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } //启动进行数据库和表补偿--(自行判断缺少的分片对象(包括分表或者分库),自动创建) serviceProvider.UseAutoTryCompensateTable(); //初始化分库分表数据,可以放在这里,也可以定义接口初始化 app.UseRouting(); app.UseAuthorization(); app.MapControllers(); app.Run(); } }
4. 调用下面接口InsertData
结果:主库里多了一条数据。两个从库里没有数据。
证明:说明写操作是操作主库的。
/// <summary>
/// 插入数据
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> InsertData()
{
//数据1
var order = new Order()
{
Id = Guid.NewGuid().ToString("N"),
Payer = "01",
Money = 100 + new Random().Next(100, 3000),
OrderStatus = (OrderStatusEnum)(11 % 4 + 1),
Area = "A",
CreationTime = DateTime.Now,
};
db.Add(order);
int count = await db.SaveChangesAsync();
return Json(new { status = "ok", msg = $"插入成功,条数为{count}" });
}
5. 调用下面接口SearchSimple,连续执行两次。
结果:都没有数据
证明:说明读操作是从库读取的
/// <summary>
/// 查询--默认配置就是从库查询,两个从库轮询读取
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SearchSimple()
{
var data1 = await db.Set<Order>().ToListAsync();
return Json(new { status = "ok", msg = $"查询成功", data = new { data1 } });
}
6. 手动向ShardingDB_Slave1从库的Order表里插入一条数据,连续执行两次接口SearchSimple
结果:一次有数据(手动录入的那条),一次没有数据。
证明:读操作是从库进行的,且两个从库之间是轮询的
7. 调用下面SearchSimple2接口,连续执行多次
结果:都是主库的数据
证明:可以手动把读操作切换到主库进行
/// <summary>
/// 查询-切换到主库查询
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> SearchSimple2()
{
//切换到主库查询
db.ReadWriteSeparationWriteOnly();
//切换到从库查询
//db.ReadWriteSeparationReadOnly();
var data1 = await db.Set<Order>().ToListAsync();
return Json(new { status = "ok", msg = $"查询成功", data = new { data1 } });
}
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。