前言
您是否有以下場景:
- 多租戶系統(tǒng),數(shù)據(jù)庫級別隔離
- 大數(shù)據(jù)量,需要分表分庫(動態(tài)添加),分庫分表全自動維護(hù)處理
- 租戶之前可能需要使用不同的數(shù)據(jù)庫模式,譬如有些租戶要求用oracle,或者mmsql,或者mysql或者pgsql
- 多租戶系統(tǒng)在不同的數(shù)據(jù)庫環(huán)境下需要維護(hù)的表結(jié)構(gòu)復(fù)雜繁瑣,需要維護(hù)許多腳本
- 業(yè)務(wù)代碼需要進(jìn)行大范圍的妥協(xié)來適應(yīng)上述支持
- 系統(tǒng)需要支持讀寫分離(動態(tài)添加)
- 無需停機(jī)狀態(tài)實時添加租戶(租戶線上簽約)
當(dāng)然我是一開始想先寫這篇文章,但是寫著寫著發(fā)現(xiàn)有些時候這個問題就來了,譬如多數(shù)據(jù)庫下efcore默認(rèn)不支持遷移,經(jīng)過不斷地努力,大腦的思維宮殿我下意識就發(fā)現(xiàn)了解決方案,最終用一天時間解決了就是前面的一篇文章 EFCore高級Saas系統(tǒng)下單DbContext如何支持不同數(shù)據(jù)庫的遷移 那么我們話不多說馬上開始
接下來我們將實現(xiàn)A,B,C三個租戶,其中A租戶我們使用MSSQL的訂單表使用按月分表,B租戶我們使用MYSQL的訂單表我們采用Id取模分表,C租戶我們使用MSSQL也是使用訂單按月分表但是起始時間和A不一樣
管理租戶數(shù)據(jù)
首先我們新建一個DbContext用來管理我們的租戶信息
租戶用戶表
首先我們新建一張租戶登錄的用戶表,每個用戶就是我們對外的租戶
public class SysUser { public string Id { get; set; } public string Name { get; set; } public string Password { get; set; } public DateTime CreationTime { get; set; } public bool IsDeleted { get; set; } }
租戶配置表
然后我們新建一張租戶的配置信息表用來后續(xù)初始化配置
public class SysUserTenantConfig { public string Id { get; set; } public string UserId { get; set; } /// /// 添加ShardingTenantOptions的Json包 /// public string ConfigJson { get; set; } public DateTime CreationTime { get; set; } public bool IsDeleted { get; set; } }
定義租戶配置
//為了滿足上述需求我們需要對數(shù)據(jù)庫和訂單分片方式進(jìn)行區(qū)分 public class ShardingTenantOptions { /// /// 默認(rèn)數(shù)據(jù)源名稱 /// public string DefaultDataSourceName { get; set;} /// /// 默認(rèn)數(shù)據(jù)庫地址 /// public string DefaultConnectionString { get; set; } /// /// 數(shù)據(jù)庫類型 /// public DbTypeEnum DbType { get; set; } /// /// 分片模式 取模還是按月 /// public OrderShardingTypeEnum OrderShardingType { get; set; } /// /// 按月分片其實時間 /// public DateTime BeginTimeForSharding { get; set; } /// /// 分片遷移的命名空間 /// public string MigrationNamespace { get; set; } } public enum DbTypeEnum { MSSQL = 1, MYSQL = 2 } public enum OrderShardingTypeEnum { Mod=1, ByMonth=2 }
租戶持久化DbContext
新建一個dbcontext用來存儲我們的租戶信息,當(dāng)然你也可以使用文件或者redis之類的都行
public class IdentityDbContext:DbContext { public IdentityDbContext(DbContextOptions options):base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new SysUserMap()); modelBuilder.ApplyConfiguration(new SysUserTenantConfigMap()); } }
這樣我們就完成了租戶信息的存儲
租戶管理者
我們擁有了租戶信息持久化的數(shù)據(jù)后需要對租戶信息的使用進(jìn)行配置
首先我們新建一個接口可以用來管理租戶信息
public interface ITenantManager { /// /// 獲取所有的租戶 /// /// List GetAll(); /// /// 獲取當(dāng)前租戶 /// /// TenantContext GetCurrentTenantContext(); /// /// 添加租戶信息 /// /// /// /// bool AddTenantSharding(string tenantId, IShardingRuntimeContext shardingRuntimeContext); /// /// 創(chuàng)建租戶環(huán)境 /// /// /// TenantScope CreateScope(string tenantId); } //租戶的默認(rèn)管理實現(xiàn) public class DefaultTenantManager:ITenantManager { private readonly ITenantContextAccessor _tenantContextAccessor; private readonly ConcurrentDictionary _cache = new(); public DefaultTenantManager(ITenantContextAccessor tenantContextAccessor) { _tenantContextAccessor = tenantContextAccessor; } public List GetAll() { return _cache.Keys.ToList(); } public TenantContext GetCurrentTenantContext() { return _tenantContextAccessor.TenantContext; } public bool AddTenantSharding(string tenantId, IShardingRuntimeContext shardingRuntimeContext) { return _cache.TryAdd(tenantId, shardingRuntimeContext); } public TenantScope CreateScope(string tenantId) { if (!_cache.TryGetValue(tenantId, out var shardingRuntimeContext)) { throw new InvalidOperationException(“未找到對應(yīng)租戶的配置”); } _tenantContextAccessor.TenantContext = new TenantContext(shardingRuntimeContext); return new TenantScope(_tenantContextAccessor); } } //當(dāng)前租戶上下文訪問者 public interface ITenantContextAccessor { TenantContext? TenantContext { get; set; } } //當(dāng)前租戶上下文訪問者實現(xiàn) public class TenantContextAccessor:ITenantContextAccessor { private static readonly AsyncLocal _tenantContext = new AsyncLocal(); public TenantContext? TenantContext { get => _tenantContext.Value; set => _tenantContext.Value = value; } } //租戶上下文 public class TenantContext { private readonly IShardingRuntimeContext _shardingRuntimeContext; public TenantContext(IShardingRuntimeContext shardingRuntimeContext) { _shardingRuntimeContext = shardingRuntimeContext; } public IShardingRuntimeContext GetShardingRuntimeContext() { return _shardingRuntimeContext; } } //用來切換實現(xiàn)當(dāng)前操作租戶環(huán)境 public class TenantScope:IDisposable { public TenantScope(ITenantContextAccessor tenantContextAccessor) { TenantContextAccessor = tenantContextAccessor; } public ITenantContextAccessor TenantContextAccessor { get; } public void Dispose() { } }折疊
構(gòu)思ShardingCore如何不通過依賴注入使用
其實ShardingCore可以默認(rèn)不在依賴注入中進(jìn)行依賴注入,首先我們看下普通情況下ShardingCore如何實現(xiàn)非依賴注入獲取分片上下文
var shardingRuntimeContext = new ShardingRuntimeBuilder() .UseRouteConfig(o => { o.AddShardingTableRoute(); }).UseConfig(o => { o.ThrowIfQueryRouteNotMatch = false; o.UseShardingQuery((conStr, builder) => { builder.UseMySql(conStr, new MySqlServerVersion(new Version())) .UseLoggerFactory(efLogger) .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); }); o.UseShardingTransaction((connection, builder) => { builder .UseMySql(connection, new MySqlServerVersion(new Version())) .UseLoggerFactory(efLogger) .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); }); o.AddDefaultDataSource(“ds0”, “server=127.0.0.1;port=3306;database=dbdbd0;userid=root;password=root;”); o.UseShardingMigrationConfigure(b => { b.ReplaceService(); }); }).ReplaceService(ServiceLifetime.Singleton) .Build();
這樣我們就獲得了IShardingRuntimeContext,將不同的IShardingRuntimeContext放到不同的數(shù)據(jù)庫中我們就可以實現(xiàn)不同的租戶了
訂單表
public class Order { public string Id { get; set; } public string Name { get; set; } public DateTime CreationTime { get; set; } public bool IsDeleted { get; set; } }
租戶DbContext
public class TenantDbContext:AbstractShardingDbContext,IShardingTableDbContext { public TenantDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new OrderMap()); } public IRouteTail RouteTail { get; set; } }
創(chuàng)建訂單路由
訂單按月分片路由
注意這邊我們簡單的通過采用一個靜態(tài)字段來實現(xiàn)
public class OrderMonthTableRoute:AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute { private readonly ShardingTenantOptions _shardingTenantOptions; public OrderMonthTableRoute(ShardingTenantOptions shardingTenantOptions) { _shardingTenantOptions = shardingTenantOptions; } public override void Configure(EntityMetadataTableBuilder builder) { builder.ShardingProperty(o => o.CreationTime); } public override bool AutoCreateTableByTime() { return true; } public override DateTime GetBeginTime() { return _shardingTenantOptions.BeginTimeForSharding; } }
訂單取模分片路由
public class OrderModTableRoute:AbstractSimpleShardingModKeyStringVirtualTableRoute{ private readonly ShardingTenantOptions _shardingTenantOptions; public OrderModTableRoute(ShardingTenantOptions shardingTenantOptions) : base(2, 5) { _shardingTenantOptions = shardingTenantOptions; } public override void Configure(EntityMetadataTableBuilder builder) { builder.ShardingProperty(o => o.Id); }}
實現(xiàn)多數(shù)據(jù)庫的code-first遷移
具體參考之前的博客EFCore高級Saas系統(tǒng)下單DbContext如何支持不同數(shù)據(jù)庫的遷移
https://www.cnblogs.com/xuejiaming/p/16510482.html
分片創(chuàng)建者
public interface IShardingBuilder{ IShardingRuntimeContext Build(ShardingTenantOptions tenantOptions);}public class DefaultShardingBuilder:IShardingBuilder{ public static readonly ILoggerFactory efLogger = LoggerFactory.Create(builder => { builder.AddFilter((category, level) => category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information).AddConsole(); }); private readonly IServiceProvider _serviceProvider; public DefaultShardingBuilder(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IShardingRuntimeContext Build(ShardingTenantOptions tenantOptions) { var shardingRuntimeBuilder = new ShardingRuntimeBuilder() .UseRouteConfig(o => { if (tenantOptions.OrderShardingType == OrderShardingTypeEnum.Mod) { o.AddShardingTableRoute(); } if (tenantOptions.OrderShardingType == OrderShardingTypeEnum.ByMonth) { o.AddShardingTableRoute(); } }).UseConfig(o => { o.ThrowIfQueryRouteNotMatch = false; o.UseShardingQuery((conStr, builder) => { if (tenantOptions.DbType == DbTypeEnum.MYSQL) { builder.UseMySql(conStr, new MySqlServerVersion(new Version())) .UseMigrationNamespace(new MySqlMigrationNamespace()); } if (tenantOptions.DbType == DbTypeEnum.MSSQL) { builder.UseSqlServer(conStr) .UseMigrationNamespace(new SqlServerMigrationNamespace()); } builder.UseLoggerFactory(efLogger) .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) .ReplaceService(); }); o.UseShardingTransaction((connection, builder) => { if (tenantOptions.DbType == DbTypeEnum.MYSQL) { builder .UseMySql(connection, new MySqlServerVersion(new Version())); //.UseMigrationNamespace(new MySqlMigrationNamespace());//遷移只會用connection string創(chuàng)建所以可以不加 } if (tenantOptions.DbType == DbTypeEnum.MSSQL) { builder.UseSqlServer(connection); //.UseMigrationNamespace(new SqlServerMigrationNamespace()); } builder.UseLoggerFactory(efLogger) .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); }); o.AddDefaultDataSource(tenantOptions.DefaultDataSourceName,tenantOptions.DefaultConnectionString); //注意這個遷移必須要十分重要 //注意這個遷移必須要十分重要 //注意這個遷移必須要十分重要 //注意這個遷移必須要十分重要 o.UseShardingMigrationConfigure(b => { if (tenantOptions.DbType == DbTypeEnum.MYSQL) { b.ReplaceService(); } if (tenantOptions.DbType == DbTypeEnum.MSSQL) { b.ReplaceService(); } }); }).AddServiceConfigure(s => { //IShardingRuntimeContext內(nèi)部的依賴注入 s.AddSingleton(tenantOptions); }); if (tenantOptions.DbType == DbTypeEnum.MYSQL) { shardingRuntimeBuilder.ReplaceService(ServiceLifetime .Singleton); } if (tenantOptions.DbType == DbTypeEnum.MSSQL) { shardingRuntimeBuilder.ReplaceService(ServiceLifetime .Singleton); } return shardingRuntimeBuilder.Build(_serviceProvider); }}折疊
到此為止基本上我們已經(jīng)完成了多租戶的大部分配置了,jwt部分就不在這邊贅述了因為之前有實現(xiàn)過
Startup
主要關(guān)鍵的啟動點我們應(yīng)該怎么配置呢
啟動初始化租戶
首先我們需要針對程序啟動后進(jìn)行租戶的初始化操作
public static class TenantExtension { public static void InitTenant(this IServiceProvider serviceProvider) { var tenantManager = serviceProvider.GetRequiredService(); var shardingBuilder = serviceProvider.GetRequiredService(); using (var scope = serviceProvider.CreateScope()) { var identityDbContext = scope.ServiceProvider.GetRequiredService(); identityDbContext.Database.Migrate(); var sysUserTenantConfigs = identityDbContext.Set().ToList(); if (sysUserTenantConfigs.Any()) { foreach (var sysUserTenantConfig in sysUserTenantConfigs) { var shardingTenantOptions = JsonConvert.DeserializeObject(sysUserTenantConfig.ConfigJson); var shardingRuntimeContext = shardingBuilder.Build(shardingTenantOptions); tenantManager.AddTenantSharding(sysUserTenantConfig.UserId, shardingRuntimeContext); } } } var tenantIds = tenantManager.GetAll(); foreach (var tenantId in tenantIds) { using(tenantManager.CreateScope(tenantId)) using (var scope = serviceProvider.CreateScope()) { var shardingRuntimeContext = tenantManager.GetCurrentTenantContext().GetShardingRuntimeContext(); //開啟定時任務(wù) shardingRuntimeContext.UseAutoShardingCreate(); var tenantDbContext = scope.ServiceProvider.GetService(); // tenantDbContext.Database.Migrate(); //補(bǔ)償表 shardingRuntimeContext.UseAutoTryCompensateTable(); } } } }
請求租戶中間件
為了讓我們的所有請求都可以使用指定對應(yīng)的租戶數(shù)據(jù)庫
public class TenantSelectMiddleware { private readonly RequestDelegate _next; private readonly ITenantManager _tenantManager; public TenantSelectMiddleware(RequestDelegate next,ITenantManager tenantManager) { _next = next; _tenantManager = tenantManager; } /// /// 1.中間件的方法必須叫Invoke,且為public,非static。 /// 2.Invoke方法第一個參數(shù)必須是HttpContext類型。 /// 3.Invoke方法必須返回Task。 /// 4.Invoke方法可以有多個參數(shù),除HttpContext外其它參數(shù)會嘗試從依賴注入容器中獲取。 /// 5.Invoke方法不能有重載。 /// /// Author : Napoleon /// Created : 2020/1/30 21:30 public async Task Invoke(HttpContext context) { if (context.Request.Path.ToString().StartsWith(“/api/tenant”, StringComparison.CurrentCultureIgnoreCase)) { if (!context.User.Identity.IsAuthenticated) { await _next(context); return; } var tenantId = context.User.Claims.FirstOrDefault((o) => o.Type == “uid”)?.Value; if (string.IsNullOrWhiteSpace(tenantId)) { await DoUnAuthorized(context, “not found tenant id”); return; } using (_tenantManager.CreateScope(tenantId)) { await _next(context); } } else { await _next(context); } } private async Task DoUnAuthorized(HttpContext context, string msg) { context.Response.StatusCode = 403; await context.Response.WriteAsync(msg); } }折疊
編寫登錄注冊操作
startup處配置
[Route(“api/[controller]/[action]”)] [ApiController] [AllowAnonymous] public class PassportController : ControllerBase { private readonly IServiceProvider _serviceProvider; private readonly IdentityDbContext _identityDbContext; private readonly ITenantManager _tenantManager; private readonly IShardingBuilder _shardingBuilder; public PassportController(IServiceProvider serviceProvider, IdentityDbContext identityDbContext, ITenantManager tenantManager, IShardingBuilder shardingBuilder) { _serviceProvider = serviceProvider; _identityDbContext = identityDbContext; _tenantManager = tenantManager; _shardingBuilder = shardingBuilder; } [HttpPost] public async Task Register(RegisterRequest request) { if (await _identityDbContext.Set().AnyAsync(o => o.Name == request.Name)) return BadRequest(“user not exists”); var sysUser = new SysUser() { Id = Guid.NewGuid().ToString(“n”), Name = request.Name, Password = request.Password, CreationTime = DateTime.Now }; var shardingTenantOptions = new ShardingTenantOptions() { DbType = request.DbType, OrderShardingType = request.OrderShardingType, BeginTimeForSharding = request.BeginTimeForSharding.Value, DefaultDataSourceName = “ds0”, DefaultConnectionString = GetDefaultString(request.DbType, sysUser.Id) }; var sysUserTenantConfig = new SysUserTenantConfig() { Id = Guid.NewGuid().ToString(“n”), UserId = sysUser.Id, CreationTime = DateTime.Now, ConfigJson = JsonConvert.SerializeObject(shardingTenantOptions) }; await _identityDbContext.AddAsync(sysUser); await _identityDbContext.AddAsync(sysUserTenantConfig); await _identityDbContext.SaveChangesAsync(); var shardingRuntimeContext = _shardingBuilder.Build(shardingTenantOptions); _tenantManager.AddTenantSharding(sysUser.Id, shardingRuntimeContext); using (_tenantManager.CreateScope(sysUser.Id)) using (var scope = _serviceProvider.CreateScope()) { var runtimeContext = _tenantManager.GetCurrentTenantContext().GetShardingRuntimeContext(); runtimeContext.UseAutoShardingCreate(); //啟動定時任務(wù) var tenantDbContext = scope.ServiceProvider.GetService(); tenantDbContext.Database.Migrate(); runtimeContext.UseAutoTryCompensateTable(); } return Ok(); } [HttpPost] public async Task Login(LoginRequest request) { var sysUser = await _identityDbContext.Set() .FirstOrDefaultAsync(o => o.Name == request.Name && o.Password == request.Password); if (sysUser == null) return BadRequest(“name or password error”); //秘鑰,就是標(biāo)頭,這里用Hmacsha256算法,需要256bit的密鑰 var securityKey = new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(“123123!@#!@#123123”)), SecurityAlgorithms.HmacSha256); //Claim,JwtRegisteredClaimNames中預(yù)定義了好多種默認(rèn)的參數(shù)名,也可以像下面的Guid一樣自己定義鍵名. //ClaimTypes也預(yù)定義了好多類型如role、email、name。Role用于賦予權(quán)限,不同的角色可以訪問不同的接口 //相當(dāng)于有效載荷 var claims = new Claim[] { new Claim(JwtRegisteredClaimNames.Iss, “https://localhost:5000”), new Claim(JwtRegisteredClaimNames.Aud, “api”), new Claim(“id”, Guid.NewGuid().ToString(“n”)), new Claim(“uid”, sysUser.Id), }; SecurityToken securityToken = new JwtSecurityToken( signingCredentials: securityKey, expires: DateTime.Now.AddHours(2), //過期時間 claims: claims ); var token = new JwtSecurityTokenHandler().WriteToken(securityToken); return Ok(token); } private string GetDefaultString(DbTypeEnum dbType, string userId) { switch (dbType) { case DbTypeEnum.MSSQL: return #34;Data Source=localhost;Initial Catalog=DB{userId};Integrated Security=True;”; case DbTypeEnum.MYSQL: return #34;server=127.0.0.1;port=3306;database=DB{userId};userid=root;password=L6yBtV6qNENrwBy7;”; default: throw new NotImplementedException(); } } } public class RegisterRequest { public string Name { get; set; } public string Password { get; set; } public DbTypeEnum DbType { get; set; } public OrderShardingTypeEnum OrderShardingType { get; set; } public DateTime? BeginTimeForSharding { get; set; } } public class LoginRequest { public string Name { get; set; } public string Password { get; set; } }折疊
啟動配置
var builder = WebApplication.CreateBuilder(args);// Add services to the container.builder.Services.AddControllers();builder.Services.AddAuthentication();#region 用戶系統(tǒng)配置builder.Services.AddDbContext(o => o.UseSqlServer(“Data Source=localhost;Initial Catalog=IdDb;Integrated Security=True;”));//生成密鑰var keyByteArray = Encoding.ASCII.GetBytes(“123123!@#!@#123123”);var signingKey = new SymmetricSecurityKey(keyByteArray);//認(rèn)證參數(shù)builder.Services.AddAuthentication(“Bearer”) .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey, ValidateIssuer = true, ValidIssuer = “https://localhost:5000”, ValidateAudience = true, ValidAudience = “api”, ValidateLifetime = true, ClockSkew = TimeSpan.Zero, RequireExpirationTime = true, }; });#endregionbuilder.Services.AddSingleton();builder.Services.AddSingleton();builder.Services.AddSingleton();#region 配置ShardingCorevar provider = builder.Configuration.GetValue(“Provider”, “UnKnown”);//Add-Migration InitialCreate -Context TenantDbContext -OutputDir MigrationsSqlServer -Args “–provider SqlServer”//Add-Migration InitialCreate -Context TenantDbContext -OutputDir MigrationsMySql -Args “–provider MySql”builder.Services.AddDbContext((sp, b) =>{ var tenantManager = sp.GetRequiredService(); var currentTenantContext = tenantManager.GetCurrentTenantContext(); //如果有上下文那么創(chuàng)建租戶dbcontext否則就是啟動命令A(yù)dd-Migration if (currentTenantContext != null) { var shardingRuntimeContext = currentTenantContext.GetShardingRuntimeContext(); b.UseDefaultSharding(shardingRuntimeContext); } if (args.IsNotEmpty()) { //命令啟動時為了保證Add-Migration正常運行 if (provider == “MySql”) { b.UseMySql(“server=127.0.0.1;port=3306;database=TenantDb;userid=root;password=L6yBtV6qNENrwBy7;”, new MySqlServerVersion(new Version())) .UseMigrationNamespace(new MySqlMigrationNamespace()) .ReplaceService(); return; } if (provider == “SqlServer”) { b.UseSqlServer(“Data Source=localhost;Initial Catalog=TenantDb;Integrated Security=True;”) .UseMigrationNamespace(new SqlServerMigrationNamespace()) .ReplaceService(); return; } }});#endregionvar app = builder.Build();//初始化啟動配置租戶信息app.Services.InitTenant();app.UseAuthorization();//在認(rèn)證后啟用租戶選擇中間件app.UseMiddleware();app.MapControllers();app.Run();折疊
添加遷移腳本
持久化identity遷移
多租戶SqlServer版本
多租戶MySql版本
啟動程序
啟動程序我們發(fā)現(xiàn)IdentityDbContext已經(jīng)創(chuàng)建好了,并且支持了自動遷移
創(chuàng)建A租戶
{ “Name”:”A”, “Password”:”A”, “DbType”:1, “OrderShardingType”:2, “BeginTimeForSharding”:”2022-01-01″, “MigrationNamespace”:”ShardingCoreMultiTenantSys.Migrations.SqlServer”}
注意:MigrationNamespace應(yīng)該自動生成,這邊只是為了演示方便沒寫
完成
創(chuàng)建B租戶
{ “Name”:”B”, “Password”:”B”, “DbType”:2, “OrderShardingType”:1, “BeginTimeForSharding”:”2022-01-01″, “MigrationNamespace”:”ShardingCoreMultiTenantSys.Migrations.Myql”}
完美創(chuàng)建
創(chuàng)建C租戶
{ “Name”:”C”, “Password”:”C”, “DbType”:1, “OrderShardingType”:2, “BeginTimeForSharding”:”2022-06-01″, “MigrationNamespace”:”ShardingCoreMultiTenantSys.Migrations.SqlServer”}
C租戶完美創(chuàng)建并且和A租戶采用一樣的分片規(guī)則不一樣的分片起始時間
分別對abc進(jìn)行crud
首先獲取token,然后插入A租戶
B租戶
C租戶
最后完成
文章來自https://www.cnblogs.com/xuejiaming/p/16508446.html