在很多一主多从数据库的场景下,很多开发同学为了复用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. win 10 遇到某文件一直在占用导致无法关闭,或者去任务管理器找不到服务怎么办?具体解决

    1. 打开 cmd 指令框 ,输入 perfmon 回车 就会出来这个 点击  打开资源监视器, 在句柄搜索框搜索 那个占用资源的文件或软件关键词 ,如下 搜索酷狗 将有关的选项,右键选中后 打开菜单 ...

  2. ubuntu18.04 安装谷歌chrome浏览器

    将下载源添加到系统源列表 # sudo wget http://www.linuxidc.com/files/repo/google-chrome.list -P /etc/apt/source.li ...

  3. http://dl-ssl.google.com/android上不去解决方案

    转:https://blog.csdn.net/j04110414/article/details/44149653/ 一. 更新sdk,遇到了更新下载失败问题: Fetching https://d ...

  4. VictoriaMerics学习笔记(2):核心组件

    核心组件 1. 单机版 victoria-metrics-prod 单一二进制文件 读写都在一个节点上 作者推荐单机版 特性 merge方式配置 通过HTTP协议提供服务 内存限制(防止OOM) 使用 ...

  5. 【记录一个问题】cv::cuda::dft()比cv::dft()慢很多

    具体的profile调用图如下: 可以看见compute很快,但是构造函数很慢. nvidia官网看到几篇类似的帖子,但是没有讲明白怎么解决的: opencv上的参考文档:https://docs.o ...

  6. manjora20不小心卸载,重新安装terminal,软件商店/软件中心linux类似

    问题 重新安装老版本gnome-shell 如果突然死机可能卸载完了terminal和软件商店,但是没有安装新的. 此时没有terminal也没有软件商店 无法安装软件 解决方案 terminal c ...

  7. 面试突击17:HashMap除了死循环还有什么问题?

    面试合集:https://gitee.com/mydb/interview 本篇的这个问题是一个开放性问题,HashMap 除了死循环之外,还有其他什么问题?总体来说 HashMap 的所有" ...

  8. 集合框架-HashSet存储自定义对象

    1 package cn.itcast.p4.hashset.test; 2 3 import java.util.HashSet; 4 import java.util.Iterator; 5 6 ...

  9. 前端HTML基础之form表单

    目录 一:form表单 1.form表单功能 2.表单元素 二:form表单搭建(注册页面) 1.编写input会出现黄色阴影问题 三:完整版,前端代码(注册页面) 四:type属性介绍 1.inpu ...

  10. Nginx配置文件nginx.conf有哪些属性模块?

    worker_processes 1: # worker进程的数量 events { # 事件区块开始 worker_connections 1024: # 每个worker进程支持的最大连接数 } ...