一. 分區、分表、分庫
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.ChangeToMaster(_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.ChangeToMaster(_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 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。