主頁 > .NET開發 > 在Saas系統下多租戶零腳本分表分庫讀寫分離解決方案

在Saas系統下多租戶零腳本分表分庫讀寫分離解決方案

2022-07-25 10:59:50 .NET開發

在Saas系統下多租戶零腳本分表分庫讀寫分離解決方案

## 介紹 本文ShardinfCore版本x.6.0.20+ 本期主角: - [`ShardingCore`](https://github.com/dotnetcore/sharding-core) 一款ef-core下高性能、輕量級針對分表分庫讀寫分離的解決方案,具有零依賴、零學習成本、零業務代碼入侵適配

單dbcontext多資料庫自動遷移

之前發過一篇文章 EFCore高級Saas系統下單DbContext如何支持不同資料庫的遷移 這篇文章讓efcore可以支持在單dbcontext下支持多個資料庫的遷移來實作多租戶下的不同資料庫實作

前言

您是否有以下場景:

  • 多租戶系統,資料庫級別隔離
  • 大資料量,需要分表分庫(動態添加),分庫分表全自動維護處理
  • 租戶之間可能需要使用不同的資料庫模式,譬如有些租戶要求用oracle,或者mssql,或者mysql或者pgsql
  • 多租戶系統在不同的資料庫環境下需要維護的表結構復雜繁瑣,需要維護許多腳本
  • 業務代碼需要進行大范圍的妥協來適應上述支持
  • 系統需要支持讀寫分離(動態添加)
  • 無需停機狀態實時添加租戶(租戶線上簽約)

當然我是一開始想先寫這篇文章,但是寫著寫著發現有些時候這個問題就來了,譬如多資料庫下efcore默認不支持遷移,經過不斷地努力,大腦的思維宮殿我下意識就發現了解決方案,最終用一天時間解決了就是前面的一篇文章 EFCore高級Saas系統下單DbContext如何支持不同資料庫的遷移 那么我們話不多說馬上開始

接下來我們將實作A,B,C三個租戶,其中A租戶我們使用MSSQL的訂單表使用按月分表,B租戶我們使用MYSQL的訂單表我們采用Id取模分表,C租戶我們使用MSSQL也是使用訂單按月分表但是起始時間和A不一樣

管理租戶資料

首先我們新建一個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; }
    }

租戶配置表

然后我們新建一張租戶的配置資訊表用來后續初始化配置


    public class SysUserTenantConfig
    {
        public string Id { get; set; }
        public string UserId { get; set; }
        /// <summary>
        /// 添加ShardingTenantOptions的Json包
        /// </summary>
        public string ConfigJson { get; set; }
        public DateTime CreationTime { get; set; }
        public bool IsDeleted { get; set; }
    }

定義租戶配置


//為了滿足上述需求我們需要對資料庫和訂單分片方式進行區分
    public class ShardingTenantOptions
    {
        /// <summary>
        /// 默認資料源名稱
        /// </summary>
        public  string DefaultDataSourceName { get; set;}
        /// <summary>
        /// 默認資料庫地址
        /// </summary>
        public  string DefaultConnectionString { get; set; }
        /// <summary>
        /// 資料庫型別
        /// </summary>
        public DbTypeEnum DbType { get; set; }
        /// <summary>
        /// 分片模式 取模還是按月
        /// </summary>
        public OrderShardingTypeEnum OrderShardingType { get; set; }
        /// <summary>
        /// 按月分片其實時間
        /// </summary>
        public DateTime BeginTimeForSharding { get; set; }
        /// <summary>
        /// 分片遷移的命名空間
        /// </summary>
        public string MigrationNamespace { get; set; }
    }

    public enum DbTypeEnum
    {
        MSSQL = 1,
        MYSQL = 2
    }

  public enum OrderShardingTypeEnum
  {
     Mod=1,
     ByMonth=2
    }

租戶持久化DbContext

新建一個dbcontext用來存盤我們的租戶資訊,當然你也可以使用檔案或者redis之類的都行


    public class IdentityDbContext:DbContext
    {
        public IdentityDbContext(DbContextOptions<IdentityDbContext> options):base(options)
        {
            
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ApplyConfiguration(new SysUserMap());
            modelBuilder.ApplyConfiguration(new SysUserTenantConfigMap());
        }
    }

這樣我們就完成了租戶資訊的存盤

租戶管理者

  我們擁有了租戶資訊持久化的資料后需要對租戶資訊的使用進行配置

首先我們新建一個介面可以用來管理租戶資訊


    public interface ITenantManager
    {
        /// <summary>
        /// 獲取所有的租戶
        /// </summary>
        /// <returns></returns>
        List<string> GetAll();

        /// <summary>
        /// 獲取當前租戶
        /// </summary>
        /// <returns></returns>
        TenantContext GetCurrentTenantContext();
        /// <summary>
        /// 添加租戶資訊
        /// </summary>
        /// <param name="tenantId"></param>
        /// <param name="shardingRuntimeContext"></param>
        /// <returns></returns>
        bool AddTenantSharding(string tenantId, IShardingRuntimeContext shardingRuntimeContext);

        /// <summary>
        /// 創建租戶環境
        /// </summary>
        /// <param name="tenantId"></param>
        /// <returns></returns>
        TenantScope CreateScope(string tenantId);
    }
    //租戶的默認管理實作
    public class DefaultTenantManager:ITenantManager
    {
        private readonly ITenantContextAccessor _tenantContextAccessor;
        private readonly ConcurrentDictionary<string, IShardingRuntimeContext> _cache = new();

        public DefaultTenantManager(ITenantContextAccessor tenantContextAccessor)
        {
            _tenantContextAccessor = tenantContextAccessor;
        }

        public List<string> 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("未找到對應租戶的配置");
            }

            _tenantContextAccessor.TenantContext = new TenantContext(shardingRuntimeContext);
            return new TenantScope(_tenantContextAccessor);
        }
    }
    //當前租戶背景關系訪問者
    public interface ITenantContextAccessor
    {
        TenantContext? TenantContext { get; set; }
    }
   //當前租戶背景關系訪問者實作
    public class TenantContextAccessor:ITenantContextAccessor
    {
        private static readonly AsyncLocal<TenantContext?> _tenantContext = new AsyncLocal<TenantContext?>();
        public TenantContext? TenantContext 
        {
            get => _tenantContext.Value;
            set => _tenantContext.Value = https://www.cnblogs.com/xuejiaming/p/value;
        }

    }
   //租戶背景關系
    public class TenantContext
    {
        private readonly IShardingRuntimeContext _shardingRuntimeContext;

        public TenantContext(IShardingRuntimeContext shardingRuntimeContext)
        {
            _shardingRuntimeContext = shardingRuntimeContext;
        }
        public IShardingRuntimeContext GetShardingRuntimeContext()
        {
            return _shardingRuntimeContext;
        }
    }
    //用來切換實作當前操作租戶環境
    public class TenantScope:IDisposable
    {
        public TenantScope(ITenantContextAccessor tenantContextAccessor)
        {
            TenantContextAccessor = tenantContextAccessor;
        }

        public ITenantContextAccessor TenantContextAccessor { get; }

        public void Dispose()
        {
            TenantContextAccessor.TenantContext = null;
        }
    }

構思ShardingCore如何不通過依賴注入使用

其實ShardingCore可以默認不在依賴注入中進行依賴注入,首先我們看下普通情況下ShardingCore如何實作非依賴注入獲取分片背景關系


var shardingRuntimeContext = new ShardingRuntimeBuilder<DefaultShardingDbContext>()
    .UseRouteConfig(o =>
    {
        o.AddShardingTableRoute<SysUserTableRoute>();
    }).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<IMigrationsSqlGenerator, ShardingMySqlMigrationsSqlGenerator>();
        });
    }).ReplaceService<ITableEnsureManager, MySqlTableEnsureManager>(ServiceLifetime.Singleton)
    .Build();

這樣我們就獲得了IShardingRuntimeContext,將不同的IShardingRuntimeContext放到不同的資料庫中我們就可以實作不同的租戶了

訂單表



    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<TenantDbContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ApplyConfiguration(new OrderMap());
        }

        public IRouteTail RouteTail { get; set; }
    }

創建訂單路由

訂單按月分片路由

注意這邊我們簡單的通過采用一個靜態欄位來實作


    public class OrderMonthTableRoute:AbstractSimpleShardingMonthKeyDateTimeVirtualTableRoute<Order>
    {
        private readonly ShardingTenantOptions _shardingTenantOptions;

        public OrderMonthTableRoute(ShardingTenantOptions shardingTenantOptions)
        {
            _shardingTenantOptions = shardingTenantOptions;
        }
        public override void Configure(EntityMetadataTableBuilder<Order> builder)
        {
            builder.ShardingProperty(o => o.CreationTime);
        }

        public override bool AutoCreateTableByTime()
        {
            return true;

        }

        public override DateTime GetBeginTime()
        {
            return _shardingTenantOptions.BeginTimeForSharding;
        }
    }

訂單取模分片路由

public class OrderModTableRoute:AbstractSimpleShardingModKeyStringVirtualTableRoute<Order>
{
    private readonly ShardingTenantOptions _shardingTenantOptions;

    public OrderModTableRoute(ShardingTenantOptions shardingTenantOptions) : base(2, 5)
    {
        _shardingTenantOptions = shardingTenantOptions;
    }

    public override void Configure(EntityMetadataTableBuilder<Order> builder)
    {
        builder.ShardingProperty(o => o.Id);
    }
}

實作多資料庫的code-first遷移

具體參考之前的博客EFCore高級Saas系統下單DbContext如何支持不同資料庫的遷移

https://www.cnblogs.com/xuejiaming/p/16510482.html

分片創建者


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<TenantDbContext>()
            .UseRouteConfig(o =>
            {
                if (tenantOptions.OrderShardingType == OrderShardingTypeEnum.Mod)
                {
                    o.AddShardingTableRoute<OrderModTableRoute>();
                }
                if (tenantOptions.OrderShardingType == OrderShardingTypeEnum.ByMonth)
                {
                    o.AddShardingTableRoute<OrderMonthTableRoute>();
                }
            }).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<IMigrationsAssembly,MultiDatabaseMigrationsAssembly>();
                });
                o.UseShardingTransaction((connection, builder) =>
                {
                    if (tenantOptions.DbType == DbTypeEnum.MYSQL)
                    {
                        builder
                            .UseMySql(connection, new MySqlServerVersion(new Version()));
                            //.UseMigrationNamespace(new MySqlMigrationNamespace());//遷移只會用connection string創建所以可以不加
                    }
                    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<IMigrationsSqlGenerator, ShardingMySqlMigrationsSqlGenerator>();
                    }
                    if (tenantOptions.DbType == DbTypeEnum.MSSQL)
                    {
                        b.ReplaceService<IMigrationsSqlGenerator, ShardingSqlServerMigrationsSqlGenerator>();
                    }
                });
            }).AddServiceConfigure(s =>
            {
                //IShardingRuntimeContext內部的依賴注入
                s.AddSingleton(tenantOptions);
            });
        
        if (tenantOptions.DbType == DbTypeEnum.MYSQL)
        {
            shardingRuntimeBuilder.ReplaceService<ITableEnsureManager, MySqlTableEnsureManager>(ServiceLifetime
                .Singleton);
        }
        if (tenantOptions.DbType == DbTypeEnum.MSSQL)
        {
            shardingRuntimeBuilder.ReplaceService<ITableEnsureManager, SqlServerTableEnsureManager>(ServiceLifetime
                .Singleton);
        }
        return shardingRuntimeBuilder.Build(_serviceProvider);
    }
}

到此為止基本上我們已經完成了多租戶的大部分配置了,jwt部分就不在這邊贅述了因為之前有實作過

Startup

主要關鍵的啟動點我們應該怎么配置呢

啟動初始化租戶

首先我們需要針對程式啟動后進行租戶的初始化操作


    public static class TenantExtension
    {
        public static void InitTenant(this IServiceProvider serviceProvider)
        {
            var tenantManager = serviceProvider.GetRequiredService<ITenantManager>();
            var shardingBuilder = serviceProvider.GetRequiredService<IShardingBuilder>();
            
            using (var scope = serviceProvider.CreateScope())
            {
                var identityDbContext = scope.ServiceProvider.GetRequiredService<IdentityDbContext>();
                identityDbContext.Database.Migrate();
                var sysUserTenantConfigs = identityDbContext.Set<SysUserTenantConfig>().ToList();
                if (sysUserTenantConfigs.Any())
                {
                    foreach (var sysUserTenantConfig in sysUserTenantConfigs)
                    {
                        var shardingTenantOptions = JsonConvert.DeserializeObject<ShardingTenantOptions>(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();
                    //開啟定時任務
                    shardingRuntimeContext.UseAutoShardingCreate();
                    var tenantDbContext = scope.ServiceProvider.GetService<TenantDbContext>();
                    //
                    tenantDbContext.Database.Migrate();
                    //補償表
                    shardingRuntimeContext.UseAutoTryCompensateTable();
                }
            }
        }
    }

請求租戶中間件

為了讓我們的所有請求都可以使用指定對應的租戶資料庫

    public class TenantSelectMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ITenantManager _tenantManager;

        public TenantSelectMiddleware(RequestDelegate next,ITenantManager tenantManager)
        {
            _next = next;
            _tenantManager = tenantManager;
        }

        /// <summary>
        /// 1.中間件的方法必須叫Invoke,且為public,非static,
        /// 2.Invoke方法第一個引數必須是HttpContext型別,
        /// 3.Invoke方法必須回傳Task,
        /// 4.Invoke方法可以有多個引數,除HttpContext外其它引數會嘗試從依賴注入容器中獲取,
        /// 5.Invoke方法不能有多載,
        /// </summary>
        /// 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<IActionResult> Register(RegisterRequest request)
        {
            if (await _identityDbContext.Set<SysUser>().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(); //啟動定時任務
                var tenantDbContext = scope.ServiceProvider.GetService<TenantDbContext>();
                tenantDbContext.Database.Migrate();
                runtimeContext.UseAutoTryCompensateTable();
            }

            return Ok();
        }

        [HttpPost]
        public async Task<IActionResult> Login(LoginRequest request)
        {
            var sysUser = await _identityDbContext.Set<SysUser>()
                .FirstOrDefaultAsync(o => o.Name == request.Name && o.Password == request.Password);
            if (sysUser == null)
                return BadRequest("name or password error");

            //秘鑰,就是標頭,這里用Hmacsha256演算法,需要256bit的密鑰
            var securityKey =
                new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes("123123!@#!@#123123")),
                    SecurityAlgorithms.HmacSha256);
            //Claim,JwtRegisteredClaimNames中預定義了好多種默認的引數名,也可以像下面的Guid一樣自己定義鍵名.
            //ClaimTypes也預定義了好多型別如role、email、name,Role用于賦予權限,不同的角色可以訪問不同的介面
            //相當于有效載荷
            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 $"Data Source=localhost;Initial Catalog=DB{userId};Integrated Security=True;";
                case DbTypeEnum.MYSQL:
                    return $"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 用戶系統配置

builder.Services.AddDbContext<IdentityDbContext>(o =>
    o.UseSqlServer("Data Source=localhost;Initial Catalog=IdDb;Integrated Security=True;"));
//生成密鑰
var keyByteArray = Encoding.ASCII.GetBytes("123123!@#!@#123123");
var signingKey = new SymmetricSecurityKey(keyByteArray);
//認證引數
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,
        };
    });

#endregion

builder.Services.AddSingleton<ITenantManager, DefaultTenantManager>();
builder.Services.AddSingleton<ITenantContextAccessor, TenantContextAccessor>();
builder.Services.AddSingleton<IShardingBuilder, DefaultShardingBuilder>();

#region 配置ShardingCore

var provider = builder.Configuration.GetValue("Provider", "UnKnown");
//Add-Migration InitialCreate -Context TenantDbContext -OutputDir Migrations\SqlServer -Args "--provider SqlServer"
//Add-Migration InitialCreate -Context TenantDbContext -OutputDir Migrations\MySql -Args "--provider MySql"
builder.Services.AddDbContext<TenantDbContext>((sp, b) =>
{
    var tenantManager = sp.GetRequiredService<ITenantManager>();
    var currentTenantContext = tenantManager.GetCurrentTenantContext();
    //如果有背景關系那么創建租戶dbcontext否則就是啟動命令Add-Migration
    if (currentTenantContext != null)
    {
        var shardingRuntimeContext = currentTenantContext.GetShardingRuntimeContext();
        b.UseDefaultSharding<TenantDbContext>(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<IMigrationsAssembly, MultiDatabaseMigrationsAssembly>();
            return;
        }

        if (provider == "SqlServer")
        {
            b.UseSqlServer("Data Source=localhost;Initial Catalog=TenantDb;Integrated Security=True;")
                .UseMigrationNamespace(new SqlServerMigrationNamespace())
                .ReplaceService<IMigrationsAssembly, MultiDatabaseMigrationsAssembly>();
            return;
        }
    }
});

#endregion


var app = builder.Build();

//初始化啟動配置租戶資訊
app.Services.InitTenant();
app.UseAuthorization();

//在認證后啟用租戶選擇中間件
app.UseMiddleware<TenantSelectMiddleware>();

app.MapControllers();

app.Run();

添加遷移腳本

持久化identity遷移

多租戶SqlServer版本

多租戶MySql版本

啟動程式

啟動程式我們發現IdentityDbContext已經創建好了,并且支持了自動遷移

創建A租戶

{
    "Name":"A",
    "Password":"A",
    "DbType":1,
    "OrderShardingType":2,
    "BeginTimeForSharding":"2022-01-01",
    "MigrationNamespace":"ShardingCoreMultiTenantSys.Migrations.SqlServer"
}

注意:MigrationNamespace應該自動生成,這邊只是為了演示方便沒寫

完成

創建B租戶

{
    "Name":"B",
    "Password":"B",
    "DbType":2,
    "OrderShardingType":1,
    "BeginTimeForSharding":"2022-01-01",
    "MigrationNamespace":"ShardingCoreMultiTenantSys.Migrations.Myql"
}

完美創建

創建C租戶

{
    "Name":"C",
    "Password":"C",
    "DbType":1,
    "OrderShardingType":2,
    "BeginTimeForSharding":"2022-06-01",
    "MigrationNamespace":"ShardingCoreMultiTenantSys.Migrations.SqlServer"
}

C租戶完美創建并且和A租戶采用一樣的分片規則不一樣的分片起始時間

分別對abc進行crud

首先獲取token,然后插入
A租戶

B租戶

C租戶

最后完成

最后的最后

附上demo:ShardingCoreMultiTenantSys https://github.com/xuejmnet/ShardingCoreMultiTenantSys

您都看到這邊了確定不點個star或者贊嗎,一款.Net不得不學的分庫分表解決方案,簡單理解為sharding-jdbc在.net中的實作并且支持更多特性和更優秀的資料聚合,擁有原生性能的97%,并且無業務侵入性,支持未分片的所有efcore原生查詢

  • github地址 https://github.com/xuejmnet/sharding-core
  • gitee地址 https://gitee.com/dotnetchina/sharding-core

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/500225.html

標籤:.NET Core

上一篇:WinUI3 FFmpeg.autogen 播放視頻,實作播放,暫停,停止,進度條設定播放時間。

下一篇:控制反轉IoC(Invers of Control)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more