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

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

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

curl -sSL https://get.docker.com/ | sh
sudo usermod -aG docker $USER && exec sg docker newgrp `id -gn`
sudo systemctl start docker sudo curl -sSL https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo 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.yml
docker-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?',
,
now(),
now()); INSERT INTO questions VALUES (
'a47ffcd2-635a-496e-8c65-c1cab53702a7',
'b8a83a82-bb41-4bb3-bfaa-e923faab2ca4',
'Using postgresql for multitenant data?',
,
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()); 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()
.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.local
127.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

  

  

基于Citus和ASP.NET Core开发多租户应用的更多相关文章

  1. 基于ASP.Net Core开发的一套通用后台框架

    基于ASP.Net Core开发一套通用后台框架 写在前面 这是本人在学习的过程中搭建学习的框架,如果对你有所帮助那再好不过.如果您有发现错误,请告知我,我会第一时间修改. 知其然,知其所以然,并非重 ...

  2. dot watch+vs code提成asp.net core开发效率

    在园子中,已经又前辈介绍过dotnet watch的用法,但是是基于asp.net core 1.0的较老版本来讲解的,在asp.net core 2.0的今天,部分用法已经不太一样,所以就再写一篇文 ...

  3. ASP.Net Core开发(踩坑)指南

    ASP.NET与ASP.NET Core很类似,但它们之间存在一些细微区别以及ASP.NET Core中新增特性的使用方法,在此之前也写过一篇简单的对比文章ASP.NET MVC应用迁移到ASP.NE ...

  4. C# -- HttpWebRequest 和 HttpWebResponse 的使用 C#编写扫雷游戏 使用IIS调试ASP.NET网站程序 WCF入门教程 ASP.Net Core开发(踩坑)指南 ASP.Net Core Razor+AdminLTE 小试牛刀 webservice创建、部署和调用 .net接收post请求并把数据转为字典格式

    C# -- HttpWebRequest 和 HttpWebResponse 的使用 C# -- HttpWebRequest 和 HttpWebResponse 的使用 结合使用HttpWebReq ...

  5. dotnet watch+vs code提升asp.net core开发效率

    在园子中,已经又前辈介绍过dotnet watch的用法,但是是基于asp.net core 1.0的较老版本来讲解的,在asp.net core 2.0的今天,部分用法已经不太一样,所以就再写一篇文 ...

  6. windows/Linux下设置ASP.Net Core开发环境并部署应用

    10分钟学会在windows/Linux下设置ASP.Net Core开发环境并部署应用 创建和开发ASP.NET Core应用可以有二种方式:最简单的方式是通过Visual Studio 2017 ...

  7. 2月送书福利:ASP.NET Core开发实战

    大家都知道我有一个公众号“恰童鞋骚年”,在公众号2020年第一天发布的推文<2020年,请让我重新介绍我自己>中,我曾说到我会在2020年中每个月为所有关注“恰童鞋骚年”公众号的童鞋们送一 ...

  8. 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(五)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  9. 《ASP.NET Core项目开发实战入门》带你走进ASP.NET Core开发

    <ASP.NET Core项目开发实战入门>从基础到实际项目开发部署带你走进ASP.NET Core开发. ASP.NET Core项目开发实战入门是基于ASP.NET Core 3.1 ...

随机推荐

  1. 听说 Android 9.0 要禁用 @Hide Api 的调用,你怎么看?

    Android 9.0? Hi,大家好,我是承香墨影! 距离 Android 8.0 发布,已经过了五个月,虽然现在占有率并不高,不过呢,Google 已经着手准备下一版本的 Android 系统. ...

  2. 用 Deployment 运行应用 - 每天5分钟玩转 Docker 容器技术(123)

    从本章开始,我们将通过实践深入学习 Kubernetes 的各种特性.作为容器编排引擎,最重要也是最基本的功能当然是运行容器化应用,这就是本章的内容. Deployment 前面我们已经了解到,Kub ...

  3. 算法-java代码实现快速排序

    快速排序 对于一个int数组,请编写一个快速排序算法,对数组元素排序. 给定一个int数组A及数组的大小n,请返回排序后的数组. 测试样例: [1,2,3,5,2,3],6 [1,2,2,3,3,5] ...

  4. java常量池詳解

    一.相关概念 什么是常量用final修饰的成员变量表示常量,值一旦给定就无法改变!final修饰的变量有三种:静态变量.实例变量和局部变量,分别表示三种类型的常量. Class文件中的常量池在Clas ...

  5. DESTOON B2B标签(tag)调用手册

    路径:include/tag.func.php 1.标签格式的大致说明 {tag("moduleid=9&table=article_9&length=40&cond ...

  6. MyCat 启蒙:分布式系统的数据库架构演变

    文章首发于[博客园-陈树义],点击跳转到原文<MyCat 启蒙:分布式系统的数据库架构演变> 单数据库架构 一个项目在初期的时候,为了尽可能快地验证市场,其对业务系统的最大要求是快速实现. ...

  7. C# winform引用com组件,创建AXHOST组件失败解决方案

    解决方法非常简单,请首先关闭你的开发工具然后删除所有*.vshost.exe 的文件. 重新打开visual studio开发工具,重新编译你的程序.

  8. 2017-07-05 (whereis which find)

    whereis whereis 命令名 作用 搜索命令所在的路径以及帮助文档所在的位置 选项 -b 搜索命令所在的位置 -m 搜索帮助文档所在的位置 例子 whereis ls  查看ls命令所在的位 ...

  9. Oracle实战笔记(第七天)之PL/SQL进阶

    一.控制结构 控制结构包括:判断语句(条件分支语句).循环语句.顺序控制语句三种. 1.条件分支语句 if--then:简单条件判断 --编写一个过程,可以输入一个雇员名,如果该雇员名的工资低于200 ...

  10. centos 如何关闭防火墙?

    1 查看防火墙状态: 命令: /etc/init.d/iptables status 如果是开着显示内容类是截图 2 临时关闭防火墙: 命令:/etc/init.d/iptables stop     ...