隨着互聯網的的高速發展,大多數的公司因爲一開始使用的傳統的硬件/軟件架構,致使在業務不斷髮展的同時,系統也逐漸地逼近傳統結構的極限。git
因而,系統也急需進行結構上的升級換代。github
在服務端,系統的I/O是很大的瓶頸。其中數據庫的I/O最容易成爲限制系統效率的一環。在優化數據庫I/O這一環中,能夠從優化系統調用數據庫效率、數據庫自身效率等多方面入手。sql
通常狀況下,經過升級數據庫服務器的硬件是最容易達到的。可是服務器資源不可能無限擴大,因而從調用數據庫的效率方面入手是目前主流的優化方向。數據庫
因而讀寫分離、分庫分表成爲了軟件系統的重要一環。而且須要在傳統的系統架構下,是須要作強入侵性的修改。api
多租戶的英文是Multiple Tenancy,不少人把多租戶和Saas劃上等號,其實這仍是有區別的。咱們今天不討論Sass這種如此普遍的議題。安全
如今不少的系統都是to B的,它們面向的是組織、公司和商業機構等。每一個機構會有獨立的組織架構,獨立的訂單結構,獨立的服務等級和收費。服務器
這就形成了各個機構間的數據是自然獨立的,特別是部分的公司對數據的獨立和安全性會有較高要求,每每數據是須要獨立存儲的。架構
因爲多租戶數據的自然獨立,形成了系統能夠根據機構的不一樣進行分庫分表。因此這裏討論的多租戶,僅限於數據層面的!app
其實因爲一個羣的朋友問到了相關的問題,因爲當時我並無dotnet環境,因此簡單地寫了幾句代碼,我自己是不知道代碼是否正確的。asp.net
在我有空的時候,試了一下原來是可實施的。我貼上當時隨手寫的核心代碼,其中connenctionResolver是須要本身建立的。
這代碼是能用的,若是對於asp.net core很熟悉的人,把這段代碼放入到ConfigureServices方法內便可。
可是我仍是強烈建議你們跟着個人介紹逐步實施。
1 services.AddDbContext<MyContext>((serviceProvider, options)=> 2 { 3 var connenctionResolver = serviceProvider.GetService<IConnectionResolver>(); 4 options.UseSqlServer(connenctionResolver.ConnectionString); 5 });
這個Demo,主要經過根據http request header來獲取不一樣的租戶的標識,從而達到區分租戶最終實現數據的隔離。
1. .net core app 3.1。在機器上安裝好.net core SDK, 版本3.1
2. Mysql. 使用 Pomelo.EntityFrameworkCore.MySql 包
3. EF core,Microsoft.EntityFrameworkCore, 版本3.1.1。這裏必需要用3.1的,由於ef core3.0是面向.net standard 2.1.
1. DbContext和對應數據庫對象
2. ConnenctionResolver, 用於獲取鏈接字符串
3. TenantInfo, 用於表示租戶信息
4. TenantInfoMiddleware,用於在asp.net core管道根據http的內容從而解析出TenantInfo
5. Controller, 用於實施對應的
1. 建立TenanInfo 和 TenantInfoMiddleware. TenanInfo 做爲租戶的信息,經過IOC建立,而且在TenantInfoMiddleware經過解析http request header,修改TenantInfo
1 using System; 2 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 4 { 5 public class TenantInfo 6 { 7 public string Name { get; set; } 8 } 9 }
1 using System; 2 using System.Threading.Tasks; 3 using Microsoft.AspNetCore.Http; 4 using Microsoft.Extensions.DependencyInjection; 5 6 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 7 { 8 public class TenantInfoMiddleware 9 { 10 private readonly RequestDelegate _next; 11 12 public TenantInfoMiddleware(RequestDelegate next) 13 { 14 _next = next; 15 } 16 17 public async Task InvokeAsync(HttpContext context) 18 { 19 var tenantInfo = context.RequestServices.GetRequiredService<TenantInfo>(); 20 var tenantName = context.Request.Headers["Tenant"]; 21 22 if (string.IsNullOrEmpty(tenantName)) 23 tenantName = "default"; 24 25 tenantInfo.Name = tenantName; 26 27 // Call the next delegate/middleware in the pipeline 28 await _next(context); 29 } 30 } 31 }
2. 建立HttpHeaderSqlConnectionResolver而且實現ISqlConnectionResolver接口。這裏要作的事情很簡單,直接同TenantInfo取值,而且在配置文件查找對應的connectionString。
其實這個實現類在正常的業務場景是須要包含邏輯的,可是在Demo裏爲了簡明扼要,就使用最簡單的方式實現了。
1 using System; 2 using Microsoft.Extensions.Configuration; 3 4 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 5 { 6 public interface ISqlConnectionResolver 7 { 8 string GetConnection(); 9 10 } 11 12 public class HttpHeaderSqlConnectionResolver : ISqlConnectionResolver 13 { 14 private readonly TenantInfo tenantInfo; 15 private readonly IConfiguration configuration; 16 17 public HttpHeaderSqlConnectionResolver(TenantInfo tenantInfo, IConfiguration configuration) 18 { 19 this.tenantInfo = tenantInfo; 20 this.configuration = configuration; 21 } 22 public string GetConnection() 23 { 24 var connectionString = configuration.GetConnectionString(this.tenantInfo.Name); 25 if(string.IsNullOrEmpty(connectionString)){ 26 throw new NullReferenceException("can not find the connection"); 27 } 28 return connectionString; 29 } 30 } 31 }
3. 建立類MultipleTenancyExtension,裏面包含最重要的配置數據庫鏈接字符串的方法。其中裏面的DbContext並無使用泛型,是爲了更加簡明點
1 using kiwiho.Course.MultipleTenancy.EFcore.Api.DAL; 2 using Microsoft.Extensions.Configuration; 3 using Microsoft.Extensions.DependencyInjection; 4 using Microsoft.EntityFrameworkCore; 5 6 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Infrastructure 7 { 8 public static class MultipleTenancyExtension 9 { 10 public static IServiceCollection AddConnectionByDatabase(this IServiceCollection services) 11 { 12 services.AddDbContext<StoreDbContext>((serviceProvider, options)=> 13 { 14 var resolver = serviceProvider.GetRequiredService<ISqlConnectionResolver>(); 15 16 options.UseMySql(resolver.GetConnection()); 17 }); 18 19 return services; 20 } 21 } 22 }
4. 在Startup類中配置依賴注入和把TenantInfoMiddleware加入到管道中。
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddScoped<TenantInfo>(); 4 services.AddScoped<ISqlConnectionResolver, HttpHeaderSqlConnectionResolver>(); 5 services.AddConnectionByDatabase(); 6 services.AddControllers(); 7 }
在Configure內,在UseRouting前把TenantInfoMiddleware加入到管道
1 app.UseMiddleware<TenantInfoMiddleware>();
5. 配置好DbContext和對應的數據庫對象
1 using Microsoft.EntityFrameworkCore; 2 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.DAL 4 { 5 public class StoreDbContext : DbContext 6 { 7 public DbSet<Product> Products { get; set; } 8 public StoreDbContext(DbContextOptions options) : base(options) 9 { 10 } 11 } 12 13 }
1 using System.ComponentModel.DataAnnotations; 2 3 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.DAL 4 { 5 public class Product 6 { 7 [Key] 8 public int Id { get; set; } 9 10 [StringLength(50), Required] 11 public string Name { get; set; } 12 13 [StringLength(50)] 14 public string Category { get; set; } 15 16 public double? Price { get; set; } 17 } 18 }
6. 建立ProductController, 而且在裏面添加3個方法,分別是建立,查詢全部,根據id查詢。在構造函數經過EnsureCreated以達到在數據庫不存在是自動建立數據庫。
1 using System; 2 using System.Collections.Generic; 3 using System.Threading.Tasks; 4 using kiwiho.Course.MultipleTenancy.EFcore.Api.DAL; 5 using Microsoft.AspNetCore.Mvc; 6 using Microsoft.EntityFrameworkCore; 7 8 namespace kiwiho.Course.MultipleTenancy.EFcore.Api.Controllers 9 { 10 [ApiController] 11 [Route("api/Products")] 12 public class ProductController : ControllerBase 13 { 14 private readonly StoreDbContext storeDbContext; 15 16 public ProductController(StoreDbContext storeDbContext){ 17 this.storeDbContext = storeDbContext; 18 this.storeDbContext.Database.EnsureCreated(); 19 } 20 21 [HttpPost("")] 22 public async Task<ActionResult<Product>> Create(Product product){ 23 var rct = await this.storeDbContext.Products.AddAsync(product); 24 25 await this.storeDbContext.SaveChangesAsync(); 26 27 return rct?.Entity; 28 29 } 30 31 [HttpGet("{id}")] 32 public async Task<ActionResult<Product>> Get([FromRoute] int id){ 33 34 var rct = await this.storeDbContext.Products.FindAsync(id); 35 36 return rct; 37 38 } 39 40 [HttpGet("")] 41 public async Task<ActionResult<List<Product>>> Search(){ 42 var rct = await this.storeDbContext.Products.ToListAsync(); 43 return rct; 44 } 45 } 46 }
1. 啓動項目
2. 經過postman在store1中建立一個Orange,在store2中建立一個cola。要注意的是Headers仲的Tenant:store1是必須的。
圖片就只截了store1的例子
3. 分別在store1,store2中查詢全部product
store1:只查到了Orange
store2: 只查到了cola
4. 經過查詢數據庫驗證數據是否已經隔離。可能有人會以爲爲何2個id都是1。是由於Product的Id使用 [Key] ,數據庫的id是自增加的。
其實這是故意爲之的,爲的是更好的展現這2個對象是在不一樣的數據庫
store1:
store2:
這是一個很簡單的例子,彷佛把前言讀完就已經能實現,那麼爲何還要花費那麼長去介紹呢。
這實際上是一個系列文章,這裏只作了最簡單的介紹。具體來講,它真的是一個Demo。
在不少實際場景中,其實一個機構一個數據庫,這種模式彷佛過重了,並且每一個機構都須要部署數據庫服務器和實例好像很難自動化。
而且,大多數的機構,其實徹底沒有必要獨立一個數據庫的。能夠經過分表,分Schema實現數據隔離。
因此接下來我會介紹怎麼利用EFCore的現有接口實施。而且最終把核心代碼作成類庫,而且結合MySql,SqlServer作成擴展
文章中的代碼並不是所有代碼,若是僅僅拷貝文章的代碼可能還不足以實施。可是關鍵代碼已經所有貼出
代碼所有放到github上了。這是part1,請checkout分支part1. 或者在master分支上的part1文件夾內。
能夠查看master上commit tag是part1 的部分
https://github.com/woailibain/EFCore.MultipleTenancyDemo/tree/part1