第十七节:分区、分表、分库以及基于EFCore实现读写分离
一. 分区、分表、分库
1. 分区
(1).含义
就是把一张表的数据分成N个区块,在逻辑上看最终只是一张表,但底层是由N个物理区块组成的。
(2).常用到的指令:
alter database <数据库名> add filegroup <文件组名> alter database <数据库名称> add file <数据标识> to filegroup <文件组名称>
详细操作参照: 第十八节:SQLServer剖析表分区(分区函数、分区索引、分区方案等)
2. 分表
(1).含义
就是把一张表按一定的规则分解成N个具有独立存储空间的实体表,系统读写时需要根据定义好的规则得到对应的表名称,然后再操作它。常见的分表方法有:求余法和路由表法。
(2).求余法:
以其中一个字段为例,进行GetHashCode(),得到一个整数,然后“该整数 % 10”进行求余,得到“0-9”十个数,从而建立eg:userInfor0——userInfor9十张表
该方法的局限性:只能建立10的整数倍张表
(3).路由表法:通过新增一张路由表,来配置某张业务表的分表规则,比如:通过Config表来配置order订单表的分表规则。路由表字段如下:
tableName beginTime endTime
order1 2015-01-01 2015-12-31
order2 2016-01-01 2016-12-31
order3 2017-01-01 2017-12-31
解释:比如根据订单的下单时间去Config表中匹配订单表的名称,然后向对应的订单表进行crud操作。
3. 分库
(1). 背景
一旦分表,一个库中的表会越来越多,计算机处理性能是有限的,单机数据库的瓶颈也是显而易见的,分库后可以单独服务器集群部署,更好的提高大数据扩展能力。就目前互联网场景而言,单纯的分库已经很少见了,建议直接微服务了。
(2). 常见的分库依据
按业务分库(最常见,订单库、日志库、产品库等)、按照时间分库、按照IP分库。
(3). 通过同义词来解决
A.分库后的不同数据库中的表访问方式可以通过【同义词】来做别名 CREATE SYNONYM --创建同义词的关键字 [dbo].[TestBTbA] --同义词名称,在select语句的from后面直接被使用 FOR [TestB].[dbo].[TbA] --当前同义词所映射的其他数据中的表 B.如果两个数据库分别存放在不同的物理机器上,那么他们之间通过普通的同义词就不能够互相访问了,这时必须要在创建同义词的时候指定 链接服务器名称 C.如何在一个数据库中建立 另外一个数据服务器的 链接服务器 ? 可以通过系统存储过程 sp_addlinkedserver 来添加其他数据库服务器的链接服务器 例如:exec sp_addlinkedserver '订单DB服务器', '', 'SQLOLEDB', '192.168.10.2',这个时候创建同义词的方式为: CREATE SYNONYM --创建同义词的关键字 [dbo].[TestBTbA] --同义词名称,在select语句的from后面直接被使用 FOR [订单DB服务器].[TestB].[dbo].[TbA] --当前同义词所映射的其他数据中的表
4. 读写分离
(1). 背景
大部分场景中,DB操作80%是读,20%是写,对于时效性要求不高的数据,为了减少磁盘读和写的竞争,引入读写分离的概念,即在数据库上进行主从配置,一个主,多个从,实现主从同步,从而业务上实现读写分离。
读写分离在网站发展初期可以一定程度上缓解读写并发时产生锁的问题,将读写压力分担到多台服务器上。基本原理是让主数据库处理增、删、操作,,而从数据库处理SELECT查询操作。随着系统的业务量不断增长,数据不断增多,数据库的IO操作压力会很大,读写分离也是数据库分库的一种方案。
主库:叫读写库,主要用来处理 增删改,特殊情况也可以查。
从库:叫只读库,主要用来查询数据。
(2). 需要解决的问题
在业务上区分哪些业务是允许一定时间延迟的,可以接受数据同步的耗时。
(3). 常见实现方式
复制模式、镜像传输、日志传输、和 Always On技术
(4).SQLServer中通过本地发布和本地订阅来实现,有两种模式:
A.请求订阅:从数据库按照既定的周期来请求主数据库,将增量数据脚本获取回去执行,从而实现数据的同步。
B.推送订阅:主数据库数据有变更的时候,会将增量数据脚本主动发给各个从数据库(性能优于请求订阅模式,建议使用)。
注:从数据库中表设计的时候,主键不要用自增!!
如何配置详见: 第十九节:SQLServer通过发布订阅实现主从同步(读写分离)详解
二. EFCore实现读写分离
1. 准备
ReadWriteMaster 主库
ReadWriteSlave1 从库1
ReadWriteSlave2 从库2 (从库可以在新建订阅的时候,现场创建即可, 会自动同步表结构)
(如何搭建,详见:第十九节:SQLServer通过发布订阅实现主从同步(读写分离)详解)
2.代码实操
(1). 通过Nuget安装EFCore必备的包,如下:
【Microsoft.EntityFrameworkCore 3.1.5】
【Microsoft.EntityFrameworkCore.SqlServer 3.1.5】
【Microsoft.EntityFrameworkCore.Tools 3.1.5】
(2). 通过下面指令映射主表,生成ReadWriteMasterContext上下文和UserInfor实体。
【Scaffold-DbContext "Server=localhost;Database=ReadWriteMaster;User ID=sa;Password=123456;" Microsoft.EntityFrameworkCore.SqlServer -ContextDir MyContext -OutputDir MyContext -UseDatabaseNames -DataAnnotations】
(3). 参照ReadWriteMasterContext,新建ReadWriteSlave1Context和ReadWriteSlave2Context,处理两个从库。
(4). 在ConfigureService中注册上述三个上下文.
3个EF上下文代码:
public partial class ReadWriteMasterContext : DbContext { public ReadWriteMasterContext(DbContextOptions<ReadWriteMasterContext> options) : base(options) { } public virtual DbSet<UserInfor> UserInfor { get; set; } public static readonly ILoggerFactory MyLogFactory = LoggerFactory.Create(build => { build.AddDebug(); // 用于VS调试,输出窗口的输出 }); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseLoggerFactory(MyLogFactory); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<UserInfor>(entity => { entity.Property(e => e.id).IsUnicode(false); entity.Property(e => e.userName).IsUnicode(false); }); OnModelCreatingPartial(modelBuilder); } partial void OnModelCreatingPartial(ModelBuilder modelBuilder); public partial class ReadWriteSlave1Context : DbContext { public ReadWriteSlave1Context() { } public ReadWriteSlave1Context(DbContextOptions<ReadWriteSlave1Context> options) : base(options) { } public virtual DbSet<UserInfor> UserInfor { get; set; } public static readonly ILoggerFactory MyLogFactory = LoggerFactory.Create(build => { build.AddDebug(); // 用于VS调试,输出窗口的输出 }); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseLoggerFactory(MyLogFactory); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<UserInfor>(entity => { entity.Property(e => e.id).IsUnicode(false); entity.Property(e => e.userName).IsUnicode(false); }); OnModelCreatingPartial(modelBuilder); } partial void OnModelCreatingPartial(ModelBuilder modelBuilder); } public partial class ReadWriteSlave2Context : DbContext { public ReadWriteSlave2Context() { } public ReadWriteSlave2Context(DbContextOptions<ReadWriteSlave2Context> options) : base(options) { } public virtual DbSet<UserInfor> UserInfor { get; set; } public static readonly ILoggerFactory MyLogFactory = LoggerFactory.Create(build => { build.AddDebug(); // 用于VS调试,输出窗口的输出 }); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseLoggerFactory(MyLogFactory); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<UserInfor>(entity => { entity.Property(e => e.id).IsUnicode(false); entity.Property(e => e.userName).IsUnicode(false); }); OnModelCreatingPartial(modelBuilder); } partial void OnModelCreatingPartial(ModelBuilder modelBuilder); }
DB连接字符串:
{ "MasterStr": "Server=localhost;Database=ReadWriteMaster;User ID=sa;Password=123456;", "SlaveStrList": [ "Server=localhost;Database=ReadWriteSlave1;User ID=sa;Password=123456;", "Server=localhost;Database=ReadWriteSlave2;User ID=sa;Password=123456;" ] }
ConfigureSerive配置:
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); //通过选项模式将DB字符串绑定到类上 services.Configure<MyDbStr>(Configuration) .AddSingleton<ReadWriteExtensions>() .AddScoped<BaseService>(); //注入EF上下文 services.AddDbContext<ReadWriteMasterContext>(option => option.UseSqlServer(Configuration["MasterStr"])) .AddDbContext<ReadWriteSlave1Context>(option => option.UseSqlServer(Configuration["SlaveStrList:0"])) .AddDbContext<ReadWriteSlave2Context>(option => option.UseSqlServer(Configuration["SlaveStrList:1"])); }
3. 使用的演变历程
A. 实例化多个上下文,根据需要调用对应的上下文
代码分享:
public void Test1([FromServices]ReadWriteMasterContext _masterContext1, [FromServices]ReadWriteSlave1Context _slave1Context, [FromServices]ReadWriteSlave2Context _slave2Context) { //1. 增加操作,调用主库 { UserInfor user = new UserInfor() { id = Guid.NewGuid().ToString("N"), userName = "ypf", userAge = 20, addTime = DateTime.Now }; _masterContext1.Add(user); int count = _masterContext1.SaveChanges(); } //2. 查询操作,调用从库 { Thread.Sleep(8000); //有一定延迟,这里等待一下 //使用从库的任何一个上下文即可 var data1 = _slave1Context.UserInfor.ToList(); } }
B. 只实例化一个上下文,通过数据库切换来实现
DB切换代码:
/// <summary> /// 读写分离扩展类 /// (在ConfigureService中注册成单例类) /// </summary> public class ReadWriteExtensions { public MyDbStr _myDbStr; public ReadWriteExtensions(IOptions<MyDbStr> optionsAccessor) { _myDbStr = optionsAccessor.Value; } /// <summary> /// 修改数据库连接字符串 /// </summary> /// <param name="dbContext">DB上下文</param> /// <param name="conStr">DB连接字符串</param> public void ChangeDatabase(DbContext dbContext, string conStr) { var connection = dbContext.Database.GetDbConnection(); if (connection.State.HasFlag(ConnectionState.Open)) { //连接未关闭的时候的切换方式 connection.ChangeDatabase(conStr); } else { connection.ConnectionString = conStr; } } /// <summary> /// 切换到主库 /// </summary> /// <param name="dbContext"></param> public void ChangeToMaster(DbContext dbContext) { this.ChangeDatabase(dbContext, _myDbStr.MasterStr); } /// <summary> /// 随机切换到从库 /// </summary> /// <param name="dbContext"></param> public void ChangeToSlave(DbContext dbContext) { Random r = new Random(); int count = r.Next(0, _myDbStr.SlaveStrList.Count()); this.ChangeDatabase(dbContext, _myDbStr.SlaveStrList[count]); } }
测试代码:
public void Test2([FromServices]ReadWriteMasterContext _masterContext1, [FromServices]ReadWriteExtensions _rwExtension) { //1. 增加操作,调用主库 { UserInfor user = new UserInfor() { id = Guid.NewGuid().ToString("N"), userName = "ypf", userAge = 20, addTime = DateTime.Now }; _masterContext1.Add(user); int count = _masterContext1.SaveChanges(); } //2. 查询操作,调用从库 { Thread.Sleep(8000); //有一定延迟,这里等待一下 //切换数据库 _rwExtension.ChangeToSlave(_masterContext1); var str = _masterContext1.Database.GetDbConnection().ConnectionString; var data1 = _masterContext1.UserInfor.ToList(); } //3. 增加操作,调用主库 { //切换数据库 _rwExtension.ChangeToMaster(_masterContext1); var str = _masterContext1.Database.GetDbConnection().ConnectionString; UserInfor user = new UserInfor() { id = Guid.NewGuid().ToString("N"), userName = "ypf", userAge = 20, addTime = DateTime.Now }; _masterContext1.Add(user); int count = _masterContext1.SaveChanges(); } }
C. 进一步封装隔离切换操作(只实例化一个上下文)
PS:此时会遇到一个问题,条件删除方法,要先查询后删除,引进一个新的包【Z.EntityFramework.Plus.EFCore】,首先这个包是免费的,基于这个包就可生成一条 delete+where的语句,省了一步select。
BaseService封装类:
public class BaseService { public ReadWriteMasterContext _masterContext1; public ReadWriteExtensions _rwExtension; public BaseService(ReadWriteMasterContext masterContext1, ReadWriteExtensions rwExtension) { this._masterContext1 = masterContext1; this._rwExtension = rwExtension; } /// <summary> /// 增加(主库操作) /// </summary> /// <typeparam name="T"></typeparam> /// <param name="model"></param> /// <returns></returns> public int Add<T>(T model) where T : class { _rwExtension.ChangeToMaster(_masterContext1); _masterContext1.Entry(model).State = EntityState.Added; return _masterContext1.SaveChanges(); } /// <summary> /// 删除(主库操作) /// </summary> /// <typeparam name="T"></typeparam> /// <param name="whereLambda"></param> /// <returns></returns> public int DelBy<T>(Expression<Func<T, bool>> whereLambda) where T : class { _rwExtension.ChangeToMaster(_masterContext1); return _masterContext1.Set<T>().Where(whereLambda).Delete(); } /// <summary> /// 修改(主库操作) /// </summary> /// <param name="model">修改后的实体</param> /// <returns></returns> public int Modify<T>(T model) where T : class { _rwExtension.ChangeToMaster(_masterContext1); _masterContext1.Entry(model).State = EntityState.Modified; return _masterContext1.SaveChanges(); } /// <summary> /// 查询(从库操作) /// </summary> /// <typeparam name="T"></typeparam> /// <param name="whereLambda"></param> /// <returns></returns> public List<T> GetListBy<T>(Expression<Func<T, bool>> whereLambda) where T : class { _rwExtension.ChangeToSlave(_masterContext1); return _masterContext1.Set<T>().Where(whereLambda).ToList(); } /**************************************下面是提出来savechange的封装***********************************************************/ /// <summary> /// 事务提交 /// </summary> /// <returns></returns> public int SaveChanges() { _rwExtension.ChangeToMaster(_masterContext1); return _masterContext1.SaveChanges(); } /// <summary> /// 增加(主库操作) /// </summary> public void AddNo<T>(T model) where T : class { _rwExtension.ChangeToMaster(_masterContext1); _masterContext1.Entry(model).State = EntityState.Added; } /// <summary> /// 删除1(主库操作) /// </summary> public void DelNo<T>(List<T> list) where T : class { _rwExtension.ChangeToMaster(_masterContext1); foreach (var item in list) { _masterContext1.Entry(item).State = EntityState.Deleted; } } /// <summary> /// 删除2(主库操作) /// </summary> public void DelItemNo<T>(T item) where T : class { _rwExtension.ChangeToMaster(_masterContext1); _masterContext1.Entry(item).State = EntityState.Deleted; } /// <summary> /// 修改(主库操作) /// </summary> /// <param name="model">修改后的实体</param> /// <returns></returns> public void ModifyNo<T>(T model) where T : class { _rwExtension.ChangeToSlave(_masterContext1); _masterContext1.Entry(model).State = EntityState.Modified; } }
测试代码:
public void Test3([FromServices]BaseService _baseService) { //1.增加 UserInfor user1 = new UserInfor() { id = Guid.NewGuid().ToString("N"), userName = "ypf1", userAge = 20, addTime = DateTime.Now }; UserInfor user2 = new UserInfor() { id = Guid.NewGuid().ToString("N"), userName = "ypf2", userAge = 20, addTime = DateTime.Now }; int count1 = _baseService.Add(user1); int count2 = _baseService.Add(user2); //2.查询 Thread.Sleep(8000); //有一定延迟,这里等待一下 var data = _baseService.GetListBy<UserInfor>(u => true); //3.删除 int count3 = _baseService.DelBy<UserInfor>(u => u.id == user1.id); }
D. 自动拦截CRUD操作,调用对应的上下文
(详见 https://www.cnblogs.com/CreateMyself/p/9261435.html, 里面的方案也不成熟,有问题,详见其评论)
4. 如何切换数据库
因为EF Core内部添加了方法实现IRelationalConnection接口,使得我们可以在已存在的上下文实例上重新设置连接字符串即更换数据库,但是其前提是必须保证当前上下文连接已关闭,也就是说比如我们在同一个事务中利用当前上下文进行更改操作,然后更改连接字符串进行更改操作,最后提交事务,因为在此事务内,当前上下文连接还未关闭,所以再更改连接字符串后进行数据库更改操作,将必定会抛出异常。
PS:针对连接开关不同状态,切换字符串的方案详见 ReadWriteExtensions中ChangeDatabase方法。
代码分享:
/// <summary> /// 修改数据库连接字符串 /// </summary> /// <param name="dbContext">DB上下文</param> /// <param name="conStr">DB连接字符串</param> public void ChangeDatabase(DbContext dbContext, string conStr) { var connection = dbContext.Database.GetDbConnection(); if (connection.State.HasFlag(ConnectionState.Open)) { //连接未关闭的时候的切换方式 connection.ChangeDatabase(conStr); } else { connection.ConnectionString = conStr; } }
5. 如何处理事务 ?
可以利用savechange处理事务,因为增删改都是主库,是一个数据库内,可以用savechange事务一体. 查询用从库,这里主从数据完全一致哦,详见Test4方法,模拟事务成功和事务失败的情况
代码分享:
/// <summary> /// 04-事务测试 /// </summary> /// <param name="_baseService"></param> public void Test4([FromServices]BaseService _baseService) { { //1.先准备两条数据 UserInfor user1 = new UserInfor() { id = "001", userName = "ypf1", userAge = 20, addTime = DateTime.Now }; UserInfor user2 = new UserInfor() { id = "002", userName = "ypf2", userAge = 20, addTime = DateTime.Now }; _baseService.AddNo(user1); _baseService.AddNo(user2); int count1 = _baseService.SaveChanges(); Thread.Sleep(8000); //去从库查询,然后基于主库修改和删除 var list = _baseService.GetListBy<UserInfor>(u => u.id == "001" || u.id == "002"); foreach (var item in list) { if (item.id == "001") { //执行删除操作 _baseService.DelItemNo<UserInfor>(item); } if (item.id == "002") { //执行修改操作 item.userAge = 10000; _baseService.ModifyNo<UserInfor>(item); } } //添加错误数据,模拟失败 { UserInfor user3 = new UserInfor() { id = "1111111111111111111111111111111111111111111111111111111111111111111111111111", userName = "ypf2", userAge = 20, addTime = DateTime.Now }; _baseService.AddNo(user3); } //最终提交 int count2 = _baseService.SaveChanges(); } }
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。