在很多一主多从数据库的场景下,很多开发同学为了复用DbContext往往采用创建一个包含所有DbSet<Model>父类通过继承派生出Write和ReadOnly类型来实现,其实可以通过命名注入来实现一个类型注册多个实例来实现。下面来用代码演示一下。

一、环境准备

数据库选择比较流行的postgresql,我们这里选择使用helm来快速的从开源包管理社区bitnami拉取一个postgresql的chart来搭建一个简易的主从数据库作为环境,,执行命令如下:

注意这里我们需要申明architecture为replication来创建主从架构,否则默认的standalone只会创建一个实例副本。同时我们需要暴露一下svc的端口用于验证以及预设一下root的密码,避免从secret重新查询。

helm repo add bitnami https://charts.bitnami.com/bitnami
helm install mypg --set global.postgresql.auth.postgresPassword=Mytestpwd#123 --set architecture=replication --set primary.service.type=NodePort --set primary.service.nodePorts.postgresql=32508 --set readReplicas.service.type=NodePort --set readReplicas.service.nodePorts.postgresql=31877 bitnami/postgresql

关于helm安装集群其他方面的细节可以查看文档,这里不再展开。安装完成后我们可以get po 以及get svc看到主从实例已经部署好了,并且服务也按照预期暴露好端口了(注意hl开头的是无头服务,一般情况下不需要管他默认我们采用k8s自带的svc转发。如果有特殊的负载均衡需求时可以使用他们作为dns服务提供真实后端IP来实现定制化的连接)

接着我们启动PgAdmin连接一下这两个库,看看主从库是否顺利工作

可以看到能够正确连接,接着我们创建一个数据库,看看从库是否可以正确异步订阅并同步过去

可以看到数据库这部分应该是可以正确同步了,当然为了测试多个从库,你现在可以通过以下命令来实现只读副本的扩容,接下来我们开始第二阶段。

kubectl scale --replicas=n statefulset/mypg-postgresql-read

二、实现单一上下文的多实例注入

首先我们创建一个常规的webapi项目,并且引入ef和pgqsql相关的nuget。同时由于需要做数据库自动化迁移我们引入efcore.tool包,并且引入autofac作为默认的DI容器(由于默认的DI不支持在长周期实例(HostedService-singleton)注入短周期实例(DbContext-scoped))

  <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.3" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="7.2.0" />
</ItemGroup>

接着我们创建efcontext以及一个model

    public class EfContext : DbContext
{
public DbSet<User> User { get; set; }
public EfContext(DbContextOptions<EfContext> options) : base(options) { }
}
public class User
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}

然后我们创建对应的读写上下文的工厂用于自动化切换,并创建一个扩展函数用于注册上下文到多个实例,同时要记得创建对应的接口用于DI容器注册时的key

首先是我们核心的扩展库,这是实现多个实例注册的关键:

    public static class MultipleEfContextExtension
{
private static AsyncLocal<ReadWriteType> type = new AsyncLocal<ReadWriteType>();
public static IServiceCollection AddReadWriteDbContext<Context>(this IServiceCollection services, Action<DbContextOptionsBuilder> writeBuilder, Action<DbContextOptionsBuilder> readBuilder) where Context : DbContext, IContextWrite, IContextRead
{
services.AddDbContext<Context>((serviceProvider, builder) =>
{
if (type.Value == ReadWriteType.Read)
readBuilder(builder);
else
writeBuilder(builder);
}, contextLifetime: ServiceLifetime.Transient, optionsLifetime: ServiceLifetime.Transient);
services.AddScoped<IContextWrite, Context>(services => {
type.Value = ReadWriteType.Write;
return services.GetService<Context>();
});
services.AddScoped<IContextRead, Context>(services => {
type.Value = ReadWriteType.Read;
return services.GetService<Context>();
});
return services;
}
}

接着是我们需要申明的读写接口以及注册上下文工厂:

    public interface IContextRead
{ }
public interface IContextWrite
{ }
public class ContextFactory<TContext> where TContext : DbContext
{
private ReadWriteType asyncReadWriteType = ReadWriteType.Read;
private readonly TContext contextWrite;
private readonly TContext contextRead;
public ContextFactory(IContextWrite contextWrite, IContextRead contextRead)
{
this.contextWrite = contextWrite as TContext;
this.contextRead = contextRead as TContext;
}
public TContext Current { get { return asyncReadWriteType == ReadWriteType.Read ? contextRead : contextWrite; } }
public void SetReadWrite(ReadWriteType readWriteType)
{
//只有类型为非强制写时才变化值
if (asyncReadWriteType != ReadWriteType.ForceWrite)
{
asyncReadWriteType = readWriteType;
}
}
public ReadWriteType GetReadWrite()
{
return asyncReadWriteType;
}
}

同时修改一下EF上下文的继承,让上下文继承这两个接口:

public class EfContext : DbContext, IContextWrite, IContextRead

然后我们需要在program里使用这个扩展并注入主从库对应的连接配置

builder.Services.AddReadWriteDbContext<EfContext>(optionsBuilderWrite =>
{
optionsBuilderWrite.UseNpgsql("User ID=postgres;Password=Mytestpwd#123;Host=192.168.1.x;Port=32508;Database=UserDb;Pooling=true;");
}, optionsBuilderRead =>
{
optionsBuilderRead.UseNpgsql("User ID=postgres;Password=Mytestpwd#123;Host=192.168.1.x;Port=31877;Database=UserDb;Pooling=true;");
});

同时这里需要注册一个启动服务用于数据库自动化迁移(注意这里需要注入写库实例,连接只读库实例则无法创建数据库迁移)

builder.Services.AddHostedService<MyHostedService>();
    public class MyHostedService : IHostedService
{
private readonly EfContext context;
public MyHostedService(IContextWrite contextWrite)
{
this.context = contextWrite as EfContext;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
context.Database.EnsureCreated();
await Task.CompletedTask;
} public async Task StopAsync(CancellationToken cancellationToken)
{
await Task.CompletedTask;
}
}

再然后我们创建一些传统的工作单元和仓储用于简化orm的操作,并且在准备在控制器开始进行演示

首先定义一个简单的IRepository并实现几个常规的方法,接着我们在Repository里实现它,这里会有几个关键代码我已经标红

    public interface IRepository<T>
{
bool Add(T t);
bool Update(T t);
bool Remove(T t);
T Find(object key);
IQueryable<T> GetByCond(Expression<Func<T, bool>> cond);
}
public class Repository<T> : IRepository<T> where T:class
{
private readonly ContextFactory<EfContext> contextFactory;
private EfContext context { get { return contextFactory.Current; } }
public Repository(ContextFactory<EfContext> contextFactory)
{
this.contextFactory = contextFactory;
}
public bool Add(T t)
{
contextFactory.SetReadWrite(ReadWriteType.Write);
context.Add(t);
return true;
} public bool Remove(T t)
{
contextFactory.SetReadWrite(ReadWriteType.Write);
context.Remove(t);
return true;
} public T Find(object key)
{
contextFactory.SetReadWrite(ReadWriteType.Read);
var entity = context.Find(typeof(T), key);
return entity as T;
} public IQueryable<T> GetByCond(Expression<Func<T, bool>> cond)
{
contextFactory.SetReadWrite(ReadWriteType.Read);
return context.Set<T>().Where(cond);
} public bool Update(T t)
{
contextFactory.SetReadWrite(ReadWriteType.Write);
context.Update(t);
return true;
}
}

可以看到这些方法就是自动化切库的关键所在,接着我们再实现对应的工作单元用于统一提交和事务,并注入到容器中,这里需要注意到工作单元开启事务后,传递的枚举是强制写,也就是会忽略仓储默认的读写策略,强制工厂返回写库实例,从而实现事务一致。

    public interface IUnitofWork
{
bool Commit(IDbContextTransaction tran = null);
Task<bool> CommitAsync(IDbContextTransaction tran = null);
IDbContextTransaction BeginTransaction();
Task<IDbContextTransaction> BeginTransactionAsync();
} public class UnitOnWorkImpl<TContext> : IUnitofWork where TContext : DbContext
{
private TContext context { get { return contextFactory.Current; } }
private readonly ContextFactory<TContext> contextFactory;
public UnitOnWorkImpl(ContextFactory<TContext> contextFactory)
{
this.contextFactory = contextFactory;
}
public bool Commit(IDbContextTransaction tran = null)
{
var result = context.SaveChanges() > -1;
if (result && tran != null)
tran.Commit();
return result;
}
public async Task<bool> CommitAsync(IDbContextTransaction tran = null)
{
var result = (await context.SaveChangesAsync()) > -1;
if (result && tran != null)
await tran.CommitAsync();
return result;
}
public IDbContextTransaction BeginTransaction()
{
contextFactory.SetReadWrite(ReadWriteType.ForceWrite);
return context.Database.BeginTransaction();
}
public async Task<IDbContextTransaction> BeginTransactionAsync()
{
contextFactory.SetReadWrite(ReadWriteType.ForceWrite);
return await context.Database.BeginTransactionAsync();
}
}

最后我们将工作单元和仓储注册到容器里:

            serviceCollection.AddScoped<IUnitofWork, UnitOnWorkImpl<Context>>();
serviceCollection.AddScoped<ContextFactory<Context>>();
typeof(Context).GetProperties().Where(x => x.PropertyType.IsGenericType && typeof(DbSet<>).IsAssignableFrom(x.PropertyType.GetGenericTypeDefinition())).Select(x => x.PropertyType.GetGenericArguments()[0]).ToList().ForEach(x => serviceCollection.AddScoped(typeof(IRepository<>).MakeGenericType(x), typeof(Repository<>).MakeGenericType(x)));

这里的关键点在于开启事务后所有的数据库请求必须强制提交到主库,而非事务情况下那种根据仓储操作类型去访问各自的读写库,所以这里传递一个ForceWrite作为区分。基本的工作就差不多做完了,现在我们设计一个控制器来演示,代码如下:

    [Route("{Controller}/{Action}")]
public class HomeController : Controller
{
private readonly IUnitofWork unitofWork;
private readonly IRepository<User> repository;
public HomeController(IUnitofWork unitofWork, IRepository<User> repository)
{
this.unitofWork = unitofWork;
this.repository = repository;
}
[HttpGet]
[Route("{id}")]
public string Get(int id)
{
return JsonSerializer.Serialize(repository.Find(id), new JsonSerializerOptions() { Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) });
}
[HttpGet]
[Route("{id}/{name}")]
public async Task<bool> Get(int id, string name)
{
using var tran = await unitofWork.BeginTransactionAsync();
var user = repository.Find(id);
if (user == null)
{
user = new User() { Id = id, Name = name };
repository.Add(user);
}
else
{
user.Name = name;
repository.Update(user);
}
unitofWork.Commit(tran);
return true;
}
[HttpGet]
[Route("/all")]
public async Task<string> GetAll()
{
return JsonSerializer.Serialize(await repository.GetByCond(x => true).ToListAsync(), new JsonSerializerOptions() { Encoder = JavaScriptEncoder.Create(UnicodeRanges.All) });
}

控制器就是比较简单的三个action,根据id和查所有以及开启一个事务做事务查询+编辑 or 新增。现在我们启动项目,来测试一下接口是否正常工作

我们分别访问/all /home/get/1 和/home/get/1/小王 ,然后再次访问/all和get/1。可以看到成功的写入了。

再看看数据库的情况,可以看到主从库都已经成功同步了。

现在我们尝试用事务连接到从库试试能否写入,我们修改以下代码:让上下文工厂获取到枚举值是ForceWrite时返回错误的只读实例试试:

    public class ContextFactory<TContext> where TContext : DbContext
{
......
public TContext Current { get { return readWriteType.Value == ReadWriteType.ForceWrite ? contextRead : contextWrite; } }
......
}

接着重启项目,访问/home/get/1/小王,可以看到连接从库的情况下无法正常写入,同时也验证了确实可以通过这样的方式让单个上下文类型按需连接数据库了。

浅谈.net core如何使用EFCore为一个上下文注类型注入多个实例用于连接主从数据库的更多相关文章

  1. 浅谈.Net Core中使用Autofac替换自带的DI容器

    为什么叫 浅谈 呢?就是字面上的意思,讲得比较浅,又不是不能用(这样是不对的)!!! Aufofac大家都不陌生了,说是.Net生态下最优秀的IOC框架那是一点都过分.用的人多了,使用教程也十分丰富, ...

  2. 浅谈.Net Core DependencyInjection源码探究

    前言     相信使用过Asp.Net Core开发框架的人对自带的DI框架已经相当熟悉了,很多刚开始接触.Net Core的时候觉得不适应,主要就是因为Core默认集成它的原因.它是Asp.Net ...

  3. 浅谈程序员创业(要有一个自己的网站,最好的方式还是自己定位一个产品,用心把这个产品做好。或者满足不同需求的用户,要有特色)good

    浅谈程序员创业 ——作者:邓学彬.Jiesoft 1.什么是创业? 关于“创业”二字有必要重新学习一下,找了两个相对权威定义: 创业就是创业者对自己拥有的资源或通过努力能够拥有的资源进行优化整合,从而 ...

  4. 浅谈 EF CORE 迁移和实例化的几种方式

    出于学习和测试的简单需要,使用 Console 来作为 EF CORE 的承载程序是最合适不过的.今天笔者就将平时的几种使用方式总结成文,以供参考,同时也是给本人一个温故知新的机会.因为没有一个完整的 ...

  5. 好代码是管出来的——浅谈.Net Core的代码管理方法与落地(更新中...)

    软件开发的目的是在规定成本和时间前提下,开发出具有适用性.有效性.可修改性.可靠性.可理解性.可维护性.可重用性.可移植性.可追踪性.可互操作性和满足用户需求的软件产品. 而对于整个开发过程来说,开发 ...

  6. 浅谈.Net Core后端单元测试

    目录 1. 前言 2. 为什么需要单元测试 2.1 防止回归 2.2 减少代码耦合 3. 基本原则和规范 3.1 3A原则 3.2 尽量避免直接测试私有方法 3.3 重构原则 3.4 避免多个断言 3 ...

  7. 小E浅谈丨区块链治理真的是一个设计问题吗?

    在2018年6月28日Zcon0论坛上,“区块链治理”这个话题掀起了大神们对未来区块链治理和区块链发展的一系列的畅想. (从左至右,分别为:Valkenburgh,Zooko,Jill, Vitali ...

  8. 浅谈JavaScript浮点数及其运算

    原文:浅谈JavaScript浮点数及其运算     JavaScript 只有一种数字类型 Number,而且在Javascript中所有的数字都是以IEEE-754标准格式表示的.浮点数的精度问题 ...

  9. 浅谈 js 字符串 search 方法

    原文:浅谈 js 字符串 search 方法 这是一个很久以前的事情了,好像是安心兄弟在学习js的时候做的练习.具体记不清了,今天就来简单分析下 search 究竟是什么用的. 从字面意思理解,一个是 ...

随机推荐

  1. 树形DP总结基础

    概念 应用 例题 最大独立子集 没有上司的晚会 题目描述 分析 树的重心 题目描述 分析 树的直径 概念 题目描述 分析 概念 给定一棵有N个节点的树(通常是无根树,也就是有N-1条无向边),我们可以 ...

  2. Python_魔法属性和方法

    魔法属性 __doc__:表示类或方法的描述信息 __moudle__:表示当前操作对象的模块,当前模块时,显示__main__ __class__:表示当前操作对象的类型 __name__:表示类或 ...

  3. Centos7上传文件和下载文件命令

    https://www.cnblogs.com/patrick-yeh/p/12922829.html 使用工具:SecureCRT 或 Xshell 步骤一:安装lrzsz软件,root权限下.安装 ...

  4. 实验 6 :OpenDaylight 实验——OpenDaylight 及 Postman实现流表下发

    实验 6 :OpenDaylight 实验--OpenDaylight 及 Postman实现流表下发 一.实验目的 熟悉 Postman 的使用:熟悉如何使用 OpenDaylight 通过 Pos ...

  5. Linux上天之路(八)之用户和组

    主要内容. 用户创建,删除,修改 密码及密码文件 组创建,删除,修改 组密码及组配置文件 相关文件 Linux用户分类 超级管理员: UID为0 root用户拥有至高无上的命令,root用户不能改名 ...

  6. nuxt中iview按需加载配置

    在plugins/iview.js中修改 初始代码如下 import Vue from 'vue' import iView from 'iview' import locale from 'ivie ...

  7. console.log(a)和console.log(window.a)的区别?

    console.log(window.l); //undefined console.log(l); //Uncaught ReferenceError: l is not defined js对于未 ...

  8. git 那些事儿 —— 基于 Learn Git Branching

    前言 推荐一个 git 图形化教学网站:Learn Git Branching,这个网站有一个沙盒可以直接在上面模拟 git 的各种操作,操作效果使用图形的方式展示,非常直观.本文可以看作是它的文字版 ...

  9. Linux - 文件处理

    链接服务器 ssh 使用ssh:ssh -p22 username@host(服务器地址) 输入后会提示输入密码 -p22是ssh默认端口 可以不用 登录之后会默认处于 home 路径 xshell ...

  10. 今天太开心了,因为我知道了seastar框架

    今天听说了一个新的C++语言开发的网络框架,叫做seastar. seastar有何特别之处呢?先看看官网提供的性能数据: 性能 HTTPD benchmark: cpu # request/sec ...