基于Citus和ASP.NET Core开发多租户应用

2018-02-13 19:55:42来源:cnblogs.com作者:MeteorSeed人点击

分享

  Citus是基于PsotgreSQL的扩展,用于切分PsotgreSQL的数据,非常简单地实现数据“切片(sharp)”。如果不使用Citus,则需要开发者自己实现分布式数据访问层(DDAL),实现路由和结果汇总等逻辑,借助Citus可简化开发,是开发者把精力集中在具体的业务逻辑上。

  对于多租户程序来说,Citus可以帮助企业对数据进行切片,相比于传统的数据管理方式,Citus更智能,操作更为简单,运维成本更低廉。下面演示Citus的简单使用。

Step 01 安装docker和docker-compose(以Docker方式部署Citus)

curl -sSL https://get.docker.com/ | shsudo usermod -aG docker $USER && exec sg docker newgrp `id -gn`sudo systemctl start dockersudo curl -sSL https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-composesudo chmod +x /usr/local/bin/docker-compose

Step 02 安装并启动Citus 

  Citus有3个版本Citus Community,Citus Cloud(云端版), Citus Enterprise(支持HA等高级特性),本文使用Citus Community。

curl -sSLO https://raw.githubusercontent.com/citusdata/docker/master/docker-compose.ymldocker-compose -p citus up -d

Step 03 连接postgres

docker exec -it citus_master psql -U postgres

Step 04 设置数据库用户密码

postgres=# /password postgres          #给postgres用户设置密码Enter new password: Enter it again: 

Step 05 创建表

CREATE TABLE tenants (    id uuid NOT NULL,    domain text NOT NULL,    name text NOT NULL,    description text NOT NULL,    created_at timestamptz NOT NULL,    updated_at timestamptz NOT NULL);CREATE TABLE questions (    id uuid NOT NULL,    tenant_id uuid NOT NULL,    title text NOT NULL,    votes int NOT NULL,    created_at timestamptz NOT NULL,    updated_at timestamptz NOT NULL);ALTER TABLE tenants ADD PRIMARY KEY (id);ALTER TABLE questions ADD PRIMARY KEY (id, tenant_id);

Step 06 告知Citus如何对数据进行切片

SELECT create_distributed_table('tenants', 'id');SELECT create_distributed_table('questions', 'tenant_id');

Step 07 初始化数据

INSERT INTO tenants VALUES (    'c620f7ec-6b49-41e0-9913-08cfe81199af',     'bufferoverflow.local',    'Buffer Overflow',    'Ask anything code-related!',    now(),    now());INSERT INTO tenants VALUES (    'b8a83a82-bb41-4bb3-bfaa-e923faab2ca4',     'dboverflow.local',    'Database Questions',    'Figure out why your connection string is broken.',    now(),    now());INSERT INTO questions VALUES (    '347b7041-b421-4dc9-9e10-c64b8847fedf',    'c620f7ec-6b49-41e0-9913-08cfe81199af',    'How do you build apps in ASP.NET Core?',    1,    now(),    now());INSERT INTO questions VALUES (    'a47ffcd2-635a-496e-8c65-c1cab53702a7',    'b8a83a82-bb41-4bb3-bfaa-e923faab2ca4',    'Using postgresql for multitenant data?',    2,    now(),    now());

Step 08 新建ASP.NET Core Web应用程序,并添加引用

  • 安装“Npgsql.EntityFrameworkCore.PostgreSQL”包

  Npgsql.EntityFrameworkCore.PostgreSQL:支持Entity Framework Core操作PostgreSQL。

  • 安装“SaasKit.Multitenancy”包

  SaasKit.Multitenancy:支持ASP.NET Core开发多租户应用。

Step 09 创建models

using System;namespace QuestionExchange.Models{    public class Question    {        public Guid Id { get; set; }        public Tenant Tenant { get; set; }        public string Title { get; set; }        public int Votes { get; set; }        public DateTimeOffset CreatedAt { get; set; }        public DateTimeOffset UpdatedAt { get; set; }    }}
using System;namespace QuestionExchange.Models{    public class Tenant    {        public Guid Id { get; set; }        public string Domain { get; set; }        public string Name { get; set; }        public string Description { get; set; }        public DateTimeOffset CreatedAt { get; set; }        public DateTimeOffset UpdatedAt { get; set; }    }}
using System.Collections.Generic;namespace QuestionExchange.Models{    public class QuestionListViewModel    {      public IEnumerable<Question> Questions { get; set; }    }}

Step 10 创建数据上下文

using System.Linq;using Microsoft.EntityFrameworkCore;using QuestionExchange.Models;namespace QuestionExchange{    public class AppDbContext : DbContext    {        public AppDbContext(DbContextOptions<AppDbContext> options)            : base(options)        {        }        public DbSet<Tenant> Tenants { get; set; }        public DbSet<Question> Questions { get; set; }        /// <summary>        /// C# classes and properties are PascalCase by convention, but your Postgres tables and columns are lowercase (and snake_case).         /// The OnModelCreating method lets you override the default name translation and let Entity Framework Core know how to find         /// the entities in your database.        /// </summary>        /// <param name="modelBuilder"></param>        protected override void OnModelCreating(ModelBuilder modelBuilder)        {            var mapper = new Npgsql.NpgsqlSnakeCaseNameTranslator();            var types = modelBuilder.Model.GetEntityTypes().ToList();            // Refer to tables in snake_case internally            types.ForEach(e => e.Relational().TableName = mapper.TranslateMemberName(e.Relational().TableName));            // Refer to columns in snake_case internally            types.SelectMany(e => e.GetProperties())                .ToList()                .ForEach(p => p.Relational().ColumnName = mapper.TranslateMemberName(p.Relational().ColumnName));        }    }}

Step 11 为SaaSKit实现解析器

using System;using System.Collections.Generic;using System.Threading.Tasks;using Microsoft.AspNetCore.Http;using Microsoft.EntityFrameworkCore;using Microsoft.Extensions.Caching.Memory;using Microsoft.Extensions.Logging;using SaasKit.Multitenancy;using QuestionExchange.Models;namespace QuestionExchange{    public class CachingTenantResolver : MemoryCacheTenantResolver<Tenant>    {        private readonly AppDbContext _context;        public CachingTenantResolver(            AppDbContext context, IMemoryCache cache, ILoggerFactory loggerFactory)             : base(cache, loggerFactory)        {            _context = context;        }        // Resolver runs on cache misses        protected override async Task<TenantContext<Tenant>> ResolveAsync(HttpContext context)        {            var subdomain = context.Request.Host.Host.ToLower();            var tenant = await _context.Tenants                .FirstOrDefaultAsync(t => t.Domain == subdomain);            if (tenant == null) return null;            return new TenantContext<Tenant>(tenant);        }        protected override MemoryCacheEntryOptions CreateCacheEntryOptions()            => new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromHours(2));        protected override string GetContextIdentifier(HttpContext context)            => context.Request.Host.Host.ToLower();        protected override IEnumerable<string> GetTenantIdentifiers(TenantContext<Tenant> context)            => new string[] { context.Tenant.Domain };    }}

Step 12 修改Startup.cs

using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Hosting;using Microsoft.EntityFrameworkCore;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;using QuestionExchange.Models;namespace QuestionExchange{    public class Startup    {        public Startup(IConfiguration configuration)        {            Configuration = configuration;        }        public IConfiguration Configuration { get; }        // This method gets called by the runtime. Use this method to add services to the container.        public void ConfigureServices(IServiceCollection services)        {            var connectionString = "Server=192.168.99.102;Port=5432;Database=postgres;Userid=postgres;Password=yourpassword;";            services.AddEntityFrameworkNpgsql()    .AddDbContext<AppDbContext>(options => options.UseNpgsql(connectionString));            services.AddMultitenancy<Tenant, CachingTenantResolver>();            services.AddMvc();        }        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.        public void Configure(IApplicationBuilder app, IHostingEnvironment env)        {            if (env.IsDevelopment())            {                app.UseDeveloperExceptionPage();                app.UseBrowserLink();            }            else            {                app.UseExceptionHandler("/Home/Error");            }            app.UseStaticFiles();            app.UseMultitenancy<Tenant>();            app.UseMvc(routes =>            {                routes.MapRoute(                    name: "default",                    template: "{controller=Home}/{action=Index}/{id?}");            });        }    }}

Step 13 创建View和Controller

@inject Tenant Tenant@model QuestionListViewModel@{    ViewData["Title"] = "Home Page";}<div class="row">    <div class="col-md-12">        <h1>Welcome to <strong>@Tenant.Name</strong></h1>        <h3>@Tenant.Description</h3>    </div></div><div class="row">    <div class="col-md-12">        <h4>Popular questions</h4>        <ul>            @foreach (var question in Model.Questions)            {                <li>@question.Title</li>            }        </ul>    </div></div>
using Microsoft.AspNetCore.Mvc;using Microsoft.EntityFrameworkCore;using QuestionExchange.Models;using System.Diagnostics;using System.Linq;using System.Threading.Tasks;namespace QuestionExchange.Controllers{    public class HomeController : Controller    {        private readonly AppDbContext _context;        private readonly Tenant _currentTenant;        public HomeController(AppDbContext context, Tenant tenant)        {            _context = context;            _currentTenant = tenant;        }        public async Task<IActionResult> Index()        {            var topQuestions = await _context                .Questions                .Where(q => q.Tenant.Id == _currentTenant.Id)                .OrderByDescending(q => q.UpdatedAt)                .Take(5)                .ToArrayAsync();            var viewModel = new QuestionListViewModel            {                Questions = topQuestions            };            return View(viewModel);        }        public IActionResult About()        {            ViewData["Message"] = "Your application description page.";            return View();        }        public IActionResult Contact()        {            ViewData["Message"] = "Your contact page.";            return View();        }        public IActionResult Error()        {            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });        }    }}

 Step 14 运行站点

  首先需要修改本地Hosts文件,添加:

127.0.0.1 bufferoverflow.local127.0.0.1 dboverflow.local

  运行cmd(命令行),输入以下命令,刷新DNS:

ipconfig /flushdns

  分别使用不同Url浏览站点,可以看到之前插入的测试数据在不同租户下显示不同:

  以上,简单演示了如何基于Citus开发多租户应用。此外,Citus还比较适合开发需要快速返回查询结果的应用(比如“仪表板”等)。

  本文演示的例子比较简单,仅仅是演示了使用Citus开发多租户应用的可能。具体实践中,还涉及到具体业务以及数据库切片技巧等。建议阅读微软的《Cloud Design Patterns Book》中的Sharding模式部分,以及Citus的官方技术文档。

参考资料:

  https://github.com/citusdata/citus

  https://www.citusdata.com/blog/2018/01/22/multi-tenant-web-apps-with-dot-net-core-and-postgres

  https://docs.citusdata.com/en/v7.1/aboutcitus/what_is_citus.html

  

  

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台