EF多租户实例:演变为读写分离
前言
我又来写关于多租户的内容了,这个系列真够漫长的。
如无意外这篇随笔是最后一篇了。内容是讲关于如何利用我们的多租户库简单实现读写分离。
分析
对于读写分离,其实有很多种实现方式,但是总体可以分以下两类:
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 项目中应用起来。
那优雅的方式,确实是每个程序员都愿意使用的。
那么我们可以期待我们自行实现的 Cacheable,CachePut,CacheEvict。
这里的难点是什么,C# 对比 Java 语法特色上最大区别是 asynchorize 的支持,所以 C# 对这种拦截器最大复杂度,就是在分别处理同步和异步。
有一些已经存在的类似的缓存库,往往需要使用反射进行对异步封装或异步解释,我将用更加优异的方式实现。
关于代码
请查看github : https://github.com/woailibain/kiwiho.EFcore.MultiTenant
EF多租户实例:演变为读写分离的更多相关文章
- EF多租户实例:如何快速实现和同时支持多个DbContext
前言 上一篇随笔我们谈到了多租户模式,通过多租户模式的演化的例子.大致归纳和总结了几种模式的表现形式. 并且顺带提到了读写分离. 通过好几次的代码调整,使得这个库更加通用.今天我们聊聊怎么通过该类库快 ...
- Mysql多实例安装+主从复制+读写分离 -学习笔记
Mysql多实例安装+主从复制+读写分离 -学习笔记 .embody{ padding:10px 10px 10px; margin:0 -20px; border-bottom:solid 1px ...
- EF多租户实例:快速实现分库分表
前言 来到这篇随笔,我们继续演示如何实现EF多租户. 今天主要是演示多租户下的变形,为下图所示 实施 项目结构 这次我们的示例项目进行了精简,仅有一个API项目,直接包含所有代码. 其中Control ...
- SQL Server读写分离之发布订阅
一.发布 上面有多种发布方式,这里我选择事物发布,具体区别请自行百度. 点击下一步.然后继续选择需要发布的对象. 如果需要筛选发布的数据点击添加. 根据自己的计划选择发布的时间. 点击安全设置,设置 ...
- EF Core 实现读写分离的最佳方案
前言 公司之前使用Ado.net和Dapper进行数据访问层的操作, 进行读写分离也比较简单, 只要使用对应的数据库连接字符串即可. 而最近要迁移到新系统中,新系统使用.net core和EF Cor ...
- EF core 实现读写分离解决方案
我们公司2019年web开发已迁移至.NET core,目前有部分平台随着用户量增加,单一数据库部署已经无法满足我们的业务需求,一直在寻找EF CORE读写分离解决方案,目前在各大技术论坛上还没找到很 ...
- EF架构~通过EF6的DbCommand拦截器来实现数据库读写分离~续~添加事务机制
回到目录 上一讲中简单介绍了一个EF环境下通过DbCommand拦截器来实现SQLSERVER的读写分离,只是一个最简单的实现,而如果出现事务情况,还是会有一些问题的,因为在拦截器中我们手动开启了Co ...
- EF架构~通过EF6的DbCommand拦截器来实现数据库读写分离~再续~添加对各只读服务器的心跳检测
回到目录 上一讲中基本实现了对数据库的读写分离,而在选择只读数据库上只是随机选择,并没有去检测数据库服务器是否有效,如服务器挂了,SQL服务停了,端口被封了等等,而本讲主要对以上功能进行一个实现,并对 ...
- EF架构~通过EF6的DbCommand拦截器来实现数据库读写分离~终结~配置的优化和事务里读写的统一
回到目录 本讲是通过DbCommand拦截器来实现读写分离的最后一讲,对之前几篇文章做了一个优化,无论是程序可读性还是实用性上都有一个提升,在配置信息这块,去除了字符串方式的拼接,取而代之的是sect ...
随机推荐
- Apple的Core ML3简介——为iPhone构建深度学习模型(附代码)
概述 Apple的Core ML 3是一个为开发人员和程序员设计的工具,帮助程序员进入人工智能生态 你可以使用Core ML 3为iPhone构建机器学习和深度学习模型 在本文中,我们将为iPhone ...
- 用序列到序列和注意模型实现的:Translation with a Sequence to Sequence Network and Attention
In this project we will be teaching a neural network to translate from French to English. 最后效果: [KEY ...
- XXE白盒审计 PHP
XXE与XML注入的区别 https://www.cnblogs.com/websecurity-study/p/11348913.html XXE又分为内部实体和外部实体.我简单区分为内部实体就是自 ...
- 人生苦短,学用python
1. 我为什么开始学着用 python 啦? 扯扯网上疯传的一组图片.网上流传<人工智能实验教材>的图片,为幼儿园的小朋友们量身打造的实验教材,可谓是火了.甚至有网友调侃道:pytho ...
- iOS开发|从小公司到进大厂,我的进阶学习之旅!
iOS高级进发 OC源码下载地址 苹果开发文档 如何阅读苹果开发文档 GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍 源码地址:http://www.gnustep.o ...
- 【电商】性能测试网站搭建:XAMPP1.8+DBShop1.3
1.安装准备 1.1软件版本 XAMPP的版本:XAMPP 1.8.2 DBShop的版本:DBShop 1.3 1.2.安装环境 我的环境:win10 win7,win10都可以运行的,安装步骤 ...
- MyBatis(十):Mybatis缓存的重要内容
本文是按照狂神说的教学视频学习的笔记,强力推荐,教学深入浅出一遍就懂!b站搜索狂神说或点击下面链接 https://space.bilibili.com/95256449?spm_id_from=33 ...
- Java第十七天,Set接口
Set接口 1.特点 (1)不包含重复元素. (2)没有索引. (3)继承自Collection接口,所以Collection接口中的所有方法都适用于Set接口. 2.解析 (1)为什么不能包含重复元 ...
- 汇编刷题 已知整数变量A和B,试编写完成下列操作的程序
1.若两个数中有一个是奇数,一个是偶数,则将它们互换储存地址 2.若两个数都是奇数,则分别加一 3.若两个数都是偶数,则不变 DATA SEGMENT A DB 12H B DB 25H DATA E ...
- 28 api的使用2
本文将讲解如下api的使用: Object.System.Date.DateFormat.Calendar.Integer-- int的包装类等 1. 类 Object 是类层次结构的根类.每个类都使 ...