前言

我又来写关于多租户的内容了,这个系列真够漫长的。

如无意外这篇随笔是最后一篇了。内容是讲关于如何利用我们的多租户库简单实现读写分离。

分析

对于读写分离,其实有很多种实现方式,但是总体可以分以下两类:

1. 通过不同的连接字符串分离读库和写库

2. 通过有多个连接实例,分别连接到读或写库

他们2种类型都有各自明显的优缺点。我下面会列举部分优缺点

第1种,如果一个请求 scope 内只有一个连接实例,那么就造成同一 scope 内就只能连接读或写库。

由于一个 scope 里只有一个连接实例,造成读写都只能在一个库,好处是在需要写的情况,数据一致性很高,但也造成对于一些需要长时间运行的请求,会降低整个读写框架的效率。

另一个好处是可以节省连接,一个 scope 只有一个连接,对连接的开销更加少。

第2种,同一个请求 scope 内有多个连接实例,可以同时对读和写库进行操作。

在同时对读库和写库操作时,必须要对数据的一致性问题小心处理,由于读库写库的同步是需要很长时间的(对比一个请求的花费时间)。

在这种情况下,一般我们要对绝大部分的写操作进行觅等处理,部分只增不改的数据简单处理就行(例如新增操作记录)

由于同一个 scope 下同时拥有读和写库的实例,可以非常优雅的自动对 insert,update 等指向写库, select 指向读库。而不需要在写代码阶段显式标注

上面的2种类型我都有在实际项目中使用过,我个人是更加偏向于第1种,因为在第2种类型的项目应用中,数据的一致性问题常常造成各种各样的问题,越来越多的接口后来都将2个连接实例转变成读或写实例操作。

但不得不说,第2种类型确实比第一种效率上更加高。因为即使在一个需要写的接口下,可能需要读4~5次库,才会进行1次写操作,所以这不是一个影响效率的小因素。

由于这篇随笔我只想讨论读写分离,数据一致性问题不想过多涉及,所以本文会使用第1种类型进行讲解。

实施

在具体的实施步骤前,我们先看看项目的结构。其中 Entity,DbContext,Controller 都是前文多次提及的,就不再强调他的代码实现了,有需要等朋友去github或者前面几篇文章参考。

读写是靠什么分离的

在我们的实例中,最大的难题是: 如何区分读和写?

对的,这就是我们全文的核心。从代码层面可以区分为 人为显式标明代码自动识别数据库操作

人为显式标明很简单理解,就是我们在实现一个接口的时候,实际上已经知道它是否有需要写库。本文的实施方式

代码自动识别数据库,简单来说通过区分数据库的操作类型,从而自动指向不同的库。但由于我们本文的示例不具备很好的结构优势(上文提到的第1种类型),所以可操作性较低。

既然我们选择认为显示标明,那么大家很容易想到的是使用 C# 中备受推崇的注解方式 Attribute 。那么,我们很简单按照要求就创建了下面的这个类

这个 Attribute 看起来非常地简单,甚至连构造函数、属性和字段都没有。

有的只有第1行的 AttributeUsage 注解。这里的作用是规定他只能在方法上使用,并且不能同时存在多个和在继承时无效。

可能有朋友会提问为什么不用 ActionFilterAttribute 作为父类,其实这只是一个标识,没有任何逻辑在里面,自然也不需要用到强大的 ActionFilterAttribute 了

 [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class IsWriteAttribute : Attribute
{
}

连接实例初始化

较为熟悉 asp.net core 的朋友或者有留意系列文章的朋友,应该不难发现 EF core 的连接实例 DbContext 是通过控制反转自动初始化的,在 Controller 产生之前,DbContext 已经初始化完成了。

那么我们是如何在 Controller 构造之前就标明这个DbContext 使用的是写库的连接还是读库的连接呢?

在这种情况下,我们就需要利用 asp.net core 的路由了,因为没有 asp.net core 的 Endpoint,我们是无法知道这个请求是到达哪一个 Controller 和方法的,这样就造成我们前文提到使用 Middleware 已经不再适用了。

通过苦苦地阅读了部分关于 Endpoint 的源码之后,我分析有2个较为合适的对象,分别是:IActionInvokerProvider 和 IControllerActivator。

最终我选定使用 IActionInvokerProvider ,理由暂不叙述,如果有机会我们展开源码讨论的时候再谈。

下面贴出 ReadWriteActionInvokerProvider 的代码。 OnProviderExecuted 就是执行后,OnProviderExecuting 就是执行前,这个很好理解。

第14行就是读出当前即将执行的接口方法有没有上文提到的使用 IsWriteAttribute 进行标注

剩下的代码的作用,主要就是对当前请求 scope 的 tenantInfo 进行赋值,用于区分当前请求是读还是写。

 public class ReadWriteActionInvokerProvider : IActionInvokerProvider
{
public int Order => ; public void OnProvidersExecuted(ActionInvokerProviderContext context)
{
} public void OnProvidersExecuting(ActionInvokerProviderContext context)
{
if (context.ActionContext.ActionDescriptor is ControllerActionDescriptor descriptor)
{
var serviceProvider = context.ActionContext.HttpContext.RequestServices;
var isWrite = descriptor.MethodInfo.GetCustomAttributes(typeof(IsWriteAttribute), false)?.Length > ; var tenantInfo = serviceProvider.GetService(typeof(TenantInfo)) as TenantInfo;
tenantInfo.Name = isWrite ? "WRITE" : "READ";
(tenantInfo as dynamic).IsWrite = isWrite;
}
}
}

获取连接字符串

连接字符串这部分,由于我们已经跳出了多租户库规定的范畴了,所以我们需要自己实现一个可用于读写分离的 ConnectionGenerator

其中 TenantKey 属性和 MatchTenantKey 方法是 IConnectionGenerator 中必须的,主要是用来这个 Generator 是否匹配当前 DbContext

GetConection 中的逻辑,主要是通过 IsWrite 来判断是否是写库,从而获得唯一的写库连接字符串。其他的任何情况都通过随机数的取模,从2个读库的连接字符串中取一个。

 public class ReadWriteConnectionGenerator : IConnectionGenerator
{ static Lazy<Random> random = new Lazy<Random>();
private readonly IConfiguration configuration;
public string TenantKey => ""; public ReadWriteConnectionGenerator(IConfiguration configuration)
{
this.configuration = configuration;
} public string GetConnection(TenantOption option, TenantInfo tenantInfo)
{
dynamic info = tenantInfo;
if (info?.IsWrite == true)
{
return configuration.GetConnectionString($"{option.ConnectionPrefix}write");
}
else
{
var mod = random.Value.Next() % ;
return configuration.GetConnectionString($"{option.ConnectionPrefix}read{(mod + 1)}");
}
} public bool MatchTenantKey(string tenantKey)
{
return true;
}
}

注入配置

来到 asp.net core 的世界,怎么能缺少注入配置和管道配置呢。

首先是配置我们自定义的 IActionInvokerProvider 和 IConnectionGernerator .

然后是配置多租户。 这里利用 AddTenantedDatabase 这个基础方法,主要是为了表名它并不需要前文提到的mysql,sqlserver等的众多实现库。

 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)
{
services.AddSingleton<IActionInvokerProvider, ReadWriteActionInvokerProvider>();
services.AddScoped<IConnectionGenerator, ReadWriteConnectionGenerator>();
services.AddTenantedDatabase<StoreDbContext>(null, setupDb); services.AddControllers();
} void setupDb(TenantSettings<StoreDbContext> settings)
{
settings.ConnectionPrefix = "mysql_";
settings.DbContextSetup = (serviceProvider, connectionString, optionsBuilder) =>
{
var tenant = serviceProvider.GetService<TenantInfo>();
optionsBuilder.UseMySql(connectionString, builder =>
{
// not necessary, if you are not using the table or schema
builder.TenantBuilderSetup(serviceProvider, settings, tenant);
});
};
} // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} // app.UseHttpsRedirection(); app.UseRouting(); // app.UseAuthorization(); app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}

其他

通过了上面的好几个关键步骤,我们已经将最关键的几个部分说明了。

剩下的是还有 StoreDbContext, Controller, Product, appsettings 等,请参考源码或者。

ProductionController 中有一个方法可以贴出来做为一个示例,标明我们怎么使用 IsWriteAttribute

 [HttpPost("")]
[IsWriteAttribute]
public async Task<ActionResult<Product>> Create(Product product)
{
var rct = await this.storeDbContext.Products.AddAsync(product); await this.storeDbContext.SaveChangesAsync(); return rct?.Entity; }

检验结果

其实这里我提供的例子,并不能从接口的响应如何区分是自动指向了读库或写库,所以效果就不截图了。

最后

这个系列终于要完成了。整整持续了2个月,主要是最近太忙了,即使在家办公,工作还是多得做不完。所以文章的产出非常的慢。

接下来做什么

这个系列的文章虽然完成了,但是开源的代码还是在继续的,我会开始完成github的Readme,以求让大家通过阅读github的介绍就能快速上手。

可能有朋友会有EF migration有需求,那请参阅我之前写的文章,其实套路都一样,没什么难度的。

之后会介绍什么知识点

其实我在写这个系列文章之前,就打算写缓存。可能有朋友会觉得缓存有什么可说的,不就是读一下,有就拿出来,没有就先写进去。

确实这是缓存的最基础操作,但是有没有一种优雅的方式,另我们不用不停重复写if else去读写缓存呢?

是有的,自从我读了Spring boot的部分源码,里面的缓存使用方式实在令我眼前一亮,后来我也在 asp.net core 项目中应用起来。

那优雅的方式,确实是每个程序员都愿意使用的。

那么我们可以期待我们自行实现的 CacheableCachePutCacheEvict

这里的难点是什么,C# 对比 Java 语法特色上最大区别是 asynchorize 的支持,所以 C# 对这种拦截器最大复杂度,就是在分别处理同步和异步。

有一些已经存在的类似的缓存库,往往需要使用反射进行对异步封装或异步解释,我将用更加优异的方式实现。

关于代码

请查看github  : https://github.com/woailibain/kiwiho.EFcore.MultiTenant

EF多租户实例:演变为读写分离的更多相关文章

  1. EF多租户实例:如何快速实现和同时支持多个DbContext

    前言 上一篇随笔我们谈到了多租户模式,通过多租户模式的演化的例子.大致归纳和总结了几种模式的表现形式. 并且顺带提到了读写分离. 通过好几次的代码调整,使得这个库更加通用.今天我们聊聊怎么通过该类库快 ...

  2. Mysql多实例安装+主从复制+读写分离 -学习笔记

    Mysql多实例安装+主从复制+读写分离 -学习笔记 .embody{ padding:10px 10px 10px; margin:0 -20px; border-bottom:solid 1px ...

  3. EF多租户实例:快速实现分库分表

    前言 来到这篇随笔,我们继续演示如何实现EF多租户. 今天主要是演示多租户下的变形,为下图所示 实施 项目结构 这次我们的示例项目进行了精简,仅有一个API项目,直接包含所有代码. 其中Control ...

  4. SQL Server读写分离之发布订阅

    一.发布 上面有多种发布方式,这里我选择事物发布,具体区别请自行百度. 点击下一步.然后继续选择需要发布的对象.  如果需要筛选发布的数据点击添加. 根据自己的计划选择发布的时间. 点击安全设置,设置 ...

  5. EF Core 实现读写分离的最佳方案

    前言 公司之前使用Ado.net和Dapper进行数据访问层的操作, 进行读写分离也比较简单, 只要使用对应的数据库连接字符串即可. 而最近要迁移到新系统中,新系统使用.net core和EF Cor ...

  6. EF core 实现读写分离解决方案

    我们公司2019年web开发已迁移至.NET core,目前有部分平台随着用户量增加,单一数据库部署已经无法满足我们的业务需求,一直在寻找EF CORE读写分离解决方案,目前在各大技术论坛上还没找到很 ...

  7. EF架构~通过EF6的DbCommand拦截器来实现数据库读写分离~续~添加事务机制

    回到目录 上一讲中简单介绍了一个EF环境下通过DbCommand拦截器来实现SQLSERVER的读写分离,只是一个最简单的实现,而如果出现事务情况,还是会有一些问题的,因为在拦截器中我们手动开启了Co ...

  8. EF架构~通过EF6的DbCommand拦截器来实现数据库读写分离~再续~添加对各只读服务器的心跳检测

    回到目录 上一讲中基本实现了对数据库的读写分离,而在选择只读数据库上只是随机选择,并没有去检测数据库服务器是否有效,如服务器挂了,SQL服务停了,端口被封了等等,而本讲主要对以上功能进行一个实现,并对 ...

  9. EF架构~通过EF6的DbCommand拦截器来实现数据库读写分离~终结~配置的优化和事务里读写的统一

    回到目录 本讲是通过DbCommand拦截器来实现读写分离的最后一讲,对之前几篇文章做了一个优化,无论是程序可读性还是实用性上都有一个提升,在配置信息这块,去除了字符串方式的拼接,取而代之的是sect ...

随机推荐

  1. 使用FME对CAD数据进行过滤、中心点替换转为shapefile

    1.首先加载CAD数据,并暴露出需要使用到的相关字段.比如:block_number.fme_geometry.fme_type等字段. (本次的管网设备由于是一个圆圈里面有三个文字因此将fme_ty ...

  2. macro

    Hello, 宏定义魔法世界 宏只是在预处理器里进行文本替换,没有类型,不做任何类型检查,编译器可以对相同的字符串进行优化.只保存一份到 .rodata 段.甚至有相同后缀的字符串也可以优化,你可以用 ...

  3. Conda安装包错误-CondaHTTPError: HTTP 000 CONNECTION FAILED for url <https://conda.anaconda.org/r/win-64/repodata.json> Elapsed:

    可能是防火墙问题:conda config --set ssl_verify false 安装 openssl . 换源: cmd输入conda config --add channels r 进入C ...

  4. MATLAB GUI设计(3)

    一.gca.gcf.gco 1.三者的功能定义: gcf 返回当前Figure 对象的句柄值 gca 返回当前axes 对象的句柄值 gco 返回当前鼠标单击的句柄值,该对象可以是除root 对象外的 ...

  5. MATLAB 概率论题

    1. 用模拟仿真的方法求解 clc clear tic n=0; N=100000; for ii=1:N b='MAXAM'; %字符串格式 a=randperm(5); % b=[b(a(1)), ...

  6. JS 剑指Offer(二)二维数组中的查找

    04.在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序. 请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数. var ...

  7. 数据科学 R语言速成

    文章更新于:2020-03-07 按照惯例,需要的文件附上链接放在文首: 文件名:R-3.6.2-win.exe 文件大小:82.4M 下载链接:https://www.lanzous.com/i9c ...

  8. 关于wget下载jdk问题解决

    问题: 直接从jdk官网下载会出现: 正在解析主机 login.oracle.com (login.oracle.com)... 156.151.58.18正在连接 login.oracle.com ...

  9. python3.6 ubuntu部署nginx、 uwsgi、 django

    ubuntu部署nginx. uwsgi. django 将项目上传到服务器 python manager.py runserver 0:80 在浏览器输入服务器的域名或者ip地址,访问成功. 安装u ...

  10. python 函数--生成器

    一.生成器函数: 常规定义函数,使用yield语句而不是return语句返回结果.yield语句一次返回一个结果. 好处在于,不会一下占用很多内存生成数据. 本质:就是一个迭代器. python中提供 ...