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

  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

原文地址:https://www.cnblogs.com/MeteorSeed/p/8446154.html  


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/322106.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ASP.NET CORE 微服务(简化版)实战系列-没有比这性价比再高的实战课程了

ASP.NET CORE 微服务(简化版&#xff09;实战系列&#xff0c;最后1天298&#xff0c;现在注册购买再减50。作者jesse 腾飞在2.14 早上我买了他的课程后&#xff0c;他才做了下面这个活动&#xff1a;作者jesse 腾飞花了大量的时间做了一个非常好的视频教程&#xff0c;我个人也…

祝大家狗年家庭事业旺旺旺

冒泡排序&#xff0c;选择排序&#xff0c;插入排序&#xff0c;快速排序&#xff0c;堆排序&#xff0c;归并排序&#xff0c;希尔排序&#xff0c;桶排序&#xff0c;基数排序新年帮您排忧解难。有向图&#xff0c;无向图&#xff0c;有环图&#xff0c;无环图&#xff0c;完…

携程Apollo(阿波罗)配置中心在.NET Core项目快速集成

.NET Core的支持文档大体上可以参考文档.Net客户端使用指南&#xff1a;https://github.com/ctripcorp/apollo/wiki/.Net%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97登录Apollo上新建App和相关的配置项&#xff0c;可以参考如下配置&#xff1a;在Nuget上…

欢乐纪中A组赛【2019.8.9】

前言 在短暂的比赛时间中&#xff0c;我发现本菜鸡越是功于心计想ACACAC&#xff0c;越是拿不到分&#xff0c;所以。。。 我不写比赛了JOJO!JOJO!JOJO! 成绩 JJJ表示初中&#xff0c;HHH表示高中后面加的是几年级 RankRankRankPersonPersonPersonScoreScoreScoreAAABBBCCC17…

SpringCloud Zuul(四)之工作原理

一、筛选器概述 Zuul的中心是一系列过滤器&#xff0c;这些过滤器能够在HTTP请求和响应的路由期间执行一系列操作。 以下是Zuul过滤器的主要特征&#xff1a; 类型&#xff1a;通常定义路由流程中应用过滤器的阶段&#xff08;尽管它可以是任何自定义字符串&#xff09;执行…

使用Nito.AsyncEx实现异步锁

Lock是常用的同步锁&#xff0c;但是我们无法在Lock的内部实现异步调用&#xff0c;比如我们无法使用await.以下面的代码为例&#xff0c;当你在lock内部使用await时&#xff0c;VS会报错提醒。最简单的解决办法就是使用第三方的库Nito.AsyncEx。可以通过Nuget安装。通过AsyncL…

汽车之家店铺数据抓取 DotnetSpider实战[一]

一、背景春节也不能闲着&#xff0c;一直想学一下爬虫怎么玩&#xff0c;网上搜了一大堆&#xff0c;大多都是Python的&#xff0c;大家也比较活跃&#xff0c;文章也比较多&#xff0c;找了一圈&#xff0c;发现园子里面有个大神开发了一个DotNetSpider的开源库&#xff0c;很…

Comet OJ(Contest #8)-D菜菜种菜【树状数组,指针】

前言 话说昨晚写题的时候贼NMNMNM惊险&#xff0c;最后22秒把程序交了上去竟然过了 正题 题目链接:https://cometoj.com/contest/58/problem/D?problem_id2758 题目大意 nnn个点mmm条单向边&#xff0c;然后每次询问一个区间[L,R][L,R][L,R]求若只选择这个区间的点&#xf…

微软正式开源Blazor ,将.NET带回到浏览器

微软 ASP.NET 团队近日正式开源了 Blazor &#xff0c;这是一个 Web UI 框架&#xff0c;可通过 WebAssembly 在任意浏览器中运行 .Net 。Blazor 旨在简化快速的单页面 .Net 浏览器应用的构建过程&#xff0c;它虽然使用了诸如 CSS 和 HTML 之类的 Web 技术&#xff0c;但它使…

在.NetCore中使用Myrmec检测文件真实格式

Myrmec 是什么&#xff1f;Myrmec 是一个用于检测文件格式的库&#xff0c;Myrmec不同于其它库或者手写检测代码&#xff0c;Myrmec不依赖文件扩展名&#xff08;在实际使用中&#xff0c;你的用户很可能使用虚假的扩展名欺骗你的应用程序&#xff09;&#xff0c;Myrmec会检测…

jzoj3736-[NOI2014模拟7.11]数学题(math)【计算几何】

正题 题目大意 给定两个向量a(x1,y1),b(x2,y2)a(x_1,y_1),b(x_2,y_2)a(x1​,y1​),b(x2​,y2​)&#xff0c;然后求∣λ1aλ2b∣|\lambda _1a\lambda _2b|∣λ1​aλ2​b∣的最小值&#xff0c;要求λ1,λ2\lambda_1,\lambda _2λ1​,λ2​不同时为0。 解题思路 我们先考虑若…

Orleans之EventSourcing

引入:如果没有意外,我再这篇文章中用ES代替EventSourcing,如果碰到"事件回溯","事件溯源","事溯"等词语,都一般代表Eventsourcing.如果引入Orleans而不用es的话,那就只用了Orleans一半的优点,多线程编程的逻辑\排错的简化以及可分布式.下面我聊聊…

Alex: 2018年对混合现实MR的展望

原文作者&#xff1a;Alex Kipman&#xff0c; 微软操作系统工程院技术院士 Hello 大家好&#xff01;难以置信我们已经走过了2018年的头两个月了。每年一月份我都会去巴西省亲&#xff0c;和我的家人欢聚一堂&#xff0c;度过一个美好的假日。在我省亲的同时&#xff0c;我想了…

jzoj3738-[NOI2014模拟7.11]理想城市(city)【树,模型转换】

正题 题目大意 一个理想城市有nnn个块构成&#xff0c;有以下性质 任意两个块之间可以通过其他块到达任意两个块之间可以不通过其他块(通过空位)到达 然后求每个块之间的距离之和。 解题思路 我们将横竖的距离分开计算。 假设现在我们考虑计算竖向的边的距离&#xff0c;我…

SpringCloud Ribbon(二)之自定义负载均衡策略IRule

一、Ribbon负载均衡策略 一个服务对应一个LoadBalancer&#xff0c;一个LoadBalancer只有一个Rule&#xff0c;LoadBalancer记录服务的注册地址&#xff0c;Rule提供从服务的注册地址中找出一个地址的规则。 Ribbon提供七种负载均衡策略&#xff0c;默认的负载均衡策略是轮训策…

欢乐纪中A组赛【2019.8.10】

前言 昨天&#xff1a; MdMdMd今天还真爆零了 顺便%%%ZZYRank1\%\%\%ZZY\ Rank1%%%ZZY Rank1 成绩 JJJ表示初中&#xff0c;HHH表示高中后面加的是几年级 RankRankRankPersonPersonPersonScoreScoreScoreAAABBBCCC111(H−1)ZZY(H-1)ZZY(H−1)ZZY1501501502020203030301001001…

Blazor正式成为Microsoft官方.NET 和WebAssembly项目

Microsoft从Blazor的开发者Steve Sanderson手中接手了这款应用程序&#xff0c;自此&#xff0c;将.NET在浏览器运行的计划又更进了一步。由此&#xff0c;Microsoft又进一步扩充了自己的WebAssembly/.NET栈&#xff0c;更进一步帮助.NET开发人员搭建基于浏览器的应用程序。在一…

SpringCloud Ribbon(一)之自定义负载均衡器ILoadBalancer

一、Ribbon负载均衡 一个服务对应一个LoadBalancer&#xff0c;一个LoadBalancer只有一个Rule&#xff0c;LoadBalancer记录服务的注册地址&#xff0c;提供更新服务的注册地址&#xff0c;Rule提供从服务的注册地址中找出一个地址的规则。 二、 自定义负载均衡 本文自定义负…

.NET Core 2.1路线图

Microsoft的Scott Hunter发布了Microsoft .NET Core 2.1版本的路线图。Hunter宣布Microsoft .NET Core每天约有五十万开发人员的使用量。根据Microsoft所收集的数据&#xff0c;在2017年9月.NET Core 2的使用量已经超过了.NET Core 1.X。有了之前成功的发布经验&#xff0c;Mic…

SpringCloud Ribbon(三)之IPing机制

一、IPing机制 IPing是一个主动探测服务节点存活的机制&#xff0c;通过判断服务节点的当前状态&#xff0c;设置节点的可用状态。只有当节点为可用时候才会作为负载均衡器的选取节点。 IPing有以下几种模式: DummyPing&#xff1a;默认返回true&#xff0c;即认为所有节点都…