前言

    我们都知道当单库系统遇到性能瓶颈时,读写分离是首要优化手段之一。因为绝大多数系统读的比例远高于写的比例,并且大量耗时的读操作容易引起锁表导致无发写入数据,这时读写分离就更加重要了。

    EFCore如何通过代码实现读写分离,我们可以搜索到很多案例。总结起来一种方法是注册一个DbContextFactory,读操作注入ReadDcontext,写操作注入WriteDbcontext;另外一种是动态修改数据库连接串。

    以上无论哪种方法,实现简单粗暴的读写分离功能也不复杂。但是如果需要实现从库状态监测(从库宕机)、主备自动切换(主库宕机)、从库灵活的负载均衡配置、耗时查询的SQL路由到指定的从库、指定一些表不需要读写分离(如:基础数据表)等等,随着系统的数据量增加,以后还会涉及到分片(分库,分表),分片后又会涉及到分布式事务,以上这些如果通过业务代码去实现那需要费太多脑子且稳定性是个大问题。

    有没有更优雅的法案?中间件或许是个不错的选择,EFCore也一样可以很好的基于中间实现读写分离。

为什么要使用中间件

  • 读写分离采用客户端直连方案(c#代码实现),因为少了一层中间件转发,查询性能可能会稍微好一点。但是这种方案,由于要了解后端部署细节,所以在出现主备切换、库迁移等操作的时候,客户端都需要感知到,并且需要调整数据库连接信息。如果通过中间件转发,客户端不需要关注后端细节、连接维护、主从切换等工作,都由中间件完成。这样可以让业务端只专注于业务逻辑开发。
  • 绝大部分生产项目,性能的瓶颈都在数据库。实现读写分离是解决性能瓶颈的首要手段之一。然而当读写分离还不能解决时,接下来手段就是分片(分库、分表)。数据被分到多个分片数据库后,应用如果需要读取数据,就要需要处理多个数据源的数据。如果没有数据库中间件,那么应用将直接面对分片集群,数据源切换、事务处理、数据聚合都需要应用直接处理,原本该是专注于业务的应用,将会花大量的工作来处理分片后的问题,最重要的是每个应用处理将是完全的重复造轮子。所以有了数据库中间件,应用只需要集中与业务处理,大量的通用的数据聚合,事务,数据源切换都由中间件来处理。
  • 国内各大厂、各个云平台都有自己的数据库中间件。

几款免费开源中间件介绍

目前社区成熟的、免费开源并且还在维护的中间件有mycatshardingsphere-proxyproxysqlmaxscale

mycat

  • 官网:http://www.mycat.org.cn/
  • 开发语言:Java
  • 是否支持分片:支持
  • 支持的数据库:MySQL/Mariadb、Oracle、DB2、SQL Server、PostgreSQL
  • 路由规则:事务包裹的SQL会全部走写库、没有事务包裹SQL读写库通过设置Hint实现。其它功能通过配置文件实现。
  • 简介:mycat 2013年从阿里cobar分离出来重构而成,至今还一直在更新。据官方文档介绍2015年就已经有电信、银行级别的客户在用。mycat也是四个中间件中支持数据库类型最多、功能最全的。不管你是否使用mycat, mycat权威指南 这个PDF文件建议大家都看一看,里面详细介绍了各种主从复制方法、分库/分表的规则、如何实现以及它们优缺点等等,作者2016年写这本书应该花费了很多时间与精力。2016年后mycat官方文档、wiki以及配套的mycat-web几乎停滞了,这也是mycat需要吐槽的地方。如果mycat能一直坚持更新完善文档以及配套的mycat-web,更合理有序的规划产品版本,那么mycat还真是第一选择。

shardingsphere-proxy

  • 官网:http://shardingsphere.apache.org/index_zh.html
  • 开发语言:Java
  • 是否支持分片:支持
  • 支持的数据库:MySQL/Mariadb、PostgreSQL
  • 路由规则:同一个线程且同一个数据库连接遇到有写操作那么之后的读操作都会读写库,同时也可以通过设置Hint强制读写库。其它功能通过配置文件实现。
  • 简介:shardingsphere有三个产品,对于dotneter来说shardingsphere-proxy是唯一的选择。shardingsphere是当当网开源贡献给社区,京东在基础上发扬光大。已于2020年4月成为Apache基金会顶级项目,shardingsphere-proxy后期可以重点关注。网上搜索shardingsphere-proxy相关文档绝大部分都是copy了官方的介绍文档,相关案例文档也很少,可能还需要再养一养。

proxysql

  • 官网:https://proxysql.com/
  • 开发语言:C++
  • 是否支持分片:支持
  • 支持的数据库:MySQL/Mariadb
  • 简介:proxysql也是一款成熟的MySQL/Mariadb数据库中间件。官网文档完整,使用案例应该是4款中间件中最丰富和最多的。ProxySQL 的路由规则非常灵活,可以基于用户,基于schema,以及单个sql语句实现路由规则定制。同样也可以通过Hint与路由规则配合指定路由。proxysql也是一个非常不错的选择。

maxscale

  • 官网:https://mariadb.com/kb/en/maxscale/
  • 开发语言:C
  • 是否支持分片:不支持
  • 支持的数据库:MySQL/Mariadb
  • 路由规则:事务包裹的SQL会全部走写库、没有事务包裹SQL读写库通过设置Hint实现。其它功能通过配置文件实现。
  • 简介:maridb开发的一个MySQL/Mariadb数据中间,已经非常成熟。官网文档非常完整,使用案例丰富。同时它提供了很多过滤器,如HintFilter;NamedServerFilter该过滤器可以设置指定表不需要读写分离,全部路由到写库;TopFilter该过滤器可以设置查询最慢的N条sql路由到指定读库;其他过滤器请查看官方文档。maxscale对于数据库集群高可用性提供的配置应该是4款中最丰富的。

    通过对4款中间件的简单介绍,我们发现他们都有自己路由规则,最配合丰富配置实现读写分离,而不是简单粗暴的分离。也都都提供了Hint的支持以及后端数据库监控。对于我们c#代码端要做的事情只需设置Sql的Hint,其它的交给中间件处理。

Hint作为一种 SQL 补充语法,在关系型数据库中扮演着非常重要的角色。它允许用户通过相关的语法影响 SQL 的执行方式,对 SQL 进行特殊的优化。

简单来说就是SQL语句前加注解,如maxscale指定读写库的Hint:SELECT * from table1; -- maxscale route to master

EFCore生成maxscale的Hint

读写分离必须要部署集群,基于maxscale中间件实现,还需要安装maxsale。

EFCore的TagWith是什么请参考官方文档

点击查看完整源码


public class EfCoreConsts
{
public const string MyCAT_ROUTE_TO_MASTER = "#mycat:db_type=master";
public const string MAXSCALE_ROUTE_TO_MASTER = "maxscale route to master";
} public abstract class BaseRepository<TDbContext, TEntity> : IEfRepository<TEntity>
where TDbContext : DbContext
where TEntity : EfEntity
{
public virtual IQueryable<TrdEntity> GetAll<TrdEntity>(bool writeDb = false) where TrdEntity : EfEntity
{
var dbSet = DbContext.Set<TrdEntity>().AsNoTracking();
if (writeDb)
//读操作路由到写库
return dbSet.TagWith(EfCoreConsts.MAXSCALE_ROUTE_TO_MASTER);
return dbSet;
} public virtual async Task<IEnumerable<TResult>> QueryAsync<TResult>(string sql, object param = null, int? commandTimeout = null, CommandType? commandType = null, bool writeDb = false)
{
if (writeDb)
//这个方法集成了dapper实现复杂查询,读操作路由到写库
sql = string.Concat("/* ", EfCoreConsts.MAXSCALE_ROUTE_TO_MASTER, " */", sql);
return await DbContext.Database.GetDbConnection().QueryAsync<TResult>(sql, param, null, commandTimeout, commandType);
}
}

基于maxscale要写的代码就是上面这些,数据库连接字符串与直连数据库一样,端口改成maxscale的端口。

EFCore生成mycat的Hint

再介绍一下mycat如何生成Hint

同样也必须要先部署好集群,基于mycat中间件实现,还需要安装mycat。

EFCore生成mycat的Hint稍微复杂一些,EFCoreTagWith方法生成的Hint是这这样的

-- #mycat:db_type=master
SELECT * FROM TABLE1

mycat要求是这样

/*#mycat:db_type=master*/
SELECT * FROM TABLE1

    我以Pomelo.EntityFrameworkCore.MySql为例,简单点说就是EFCore有一个IQuerySqlGeneratorFactory接口,PomeloMySqlQuerySqlGeneratorFactory类实现了这个接口,Create()方法负责创建具体的QuerySqlGenerator,这个类负责查询SQL的生成。点击查看完整源码

    我们需要做三件事情,

  • 新建工厂类AdncMySqlQuerySqlGeneratorFactory继承MySqlQuerySqlGeneratorFactory并覆写Create()方法。代码如下
namespace Pomelo.EntityFrameworkCore.MySql.Query.ExpressionVisitors.Internal
{
/// <summary>
/// adnc sql生成工厂类
/// </summary>
public class AdncMySqlQuerySqlGeneratorFactory : MySqlQuerySqlGeneratorFactory
{
private readonly QuerySqlGeneratorDependencies _dependencies;
private readonly MySqlSqlExpressionFactory _sqlExpressionFactory;
private readonly IMySqlOptions _options; public AdncMySqlQuerySqlGeneratorFactory(
[NotNull] QuerySqlGeneratorDependencies dependencies,
ISqlExpressionFactory sqlExpressionFactory,
IMySqlOptions options) : base(dependencies, sqlExpressionFactory, options)
{
_dependencies = dependencies;
_sqlExpressionFactory = (MySqlSqlExpressionFactory)sqlExpressionFactory;
_options = options;
} /// <summary>
/// 重写QuerySqlGenerator
/// </summary>
/// <returns></returns>
public override QuerySqlGenerator Create()
{
var result = new AdncQuerySqlGenerator(_dependencies, _sqlExpressionFactory, _options);
return result;
}
}
}
  • 新建Sql生成类AdncQuerySqlGenerator继承QuerySqlGenerator,覆写两个方法。
namespace Pomelo.EntityFrameworkCore.MySql.Query.ExpressionVisitors.Internal
{
/// <summary>
/// adnc sql 生成类
/// </summary>
public class AdncQuerySqlGenerator : MySqlQuerySqlGenerator
{
protected readonly Guid ContextId;
private bool _isQueryMaseter = false; public AdncQuerySqlGenerator(
[NotNull] QuerySqlGeneratorDependencies dependencies,
[NotNull] MySqlSqlExpressionFactory sqlExpressionFactory,
[CanBeNull] IMySqlOptions options)
: base(dependencies, sqlExpressionFactory, options)
{
ContextId = Guid.NewGuid();
} /// <summary>
/// 获取IQueryable的tags
/// </summary>
/// <param name="selectExpression"></param>
protected override void GenerateTagsHeaderComment(SelectExpression selectExpression)
{
if (selectExpression.Tags.Contains(EfCoreConsts.MyCAT_ROUTE_TO_MASTER))
{
_isQueryMaseter = true;
selectExpression.Tags.Remove(EfCoreConsts.MyCAT_ROUTE_TO_MASTER);
}
base.GenerateTagsHeaderComment(selectExpression);
} /// <summary>
/// pomelo最终生成的sql
/// 该方法主要是调试用
/// </summary>
/// <param name="selectExpression"></param>
/// <returns></returns>
public override IRelationalCommand GetCommand(SelectExpression selectExpression)
{
var command = base.GetCommand(selectExpression);
return command;
} /// <summary>
/// 在pomelo生成查询sql前,插入mycat注解
/// 该注解的意思是从写库读取数据
/// </summary>
/// <param name="selectExpression"></param>
/// <returns></returns>
protected override Expression VisitSelect(SelectExpression selectExpression)
{
if (_isQueryMaseter)
Sql.Append(string.Concat("/*", EfCoreConsts.MyCAT_ROUTE_TO_MASTER, "*/ ")); return base.VisitSelect(selectExpression);
}
}
}
  • 注册DbContext时替换Pomelo的SQL生成工厂
/// <summary>
/// 注册EfcoreContext
/// </summary>
public virtual void AddEfCoreContext()
{
_services.AddDbContext<AdncDbContext>(options =>
{
options.UseMySql(_mysqlConfig.ConnectionString, mySqlOptions =>
{
mySqlOptions.ServerVersion(new ServerVersion(new Version(10, 5, 4), ServerType.MariaDb));
mySqlOptions.CharSet(CharSet.Utf8Mb4);
});
//替换默认查询sql生成器,如果通过mycat中间件实现读写分离需要替换默认SQL工厂。
options.ReplaceService<IQuerySqlGeneratorFactory, AdncMySqlQuerySqlGeneratorFactory>();
});
}
  • 使用方法

public class EfCoreConsts
{
public const string MyCAT_ROUTE_TO_MASTER = "#mycat:db_type=master";
public const string MAXSCALE_ROUTE_TO_MASTER = "maxscale route to master";
} public abstract class BaseRepository<TDbContext, TEntity> : IEfRepository<TEntity>
where TDbContext : DbContext
where TEntity : EfEntity
{
public virtual IQueryable<TrdEntity> GetAll<TrdEntity>(bool writeDb = false) where TrdEntity : EfEntity
{
var dbSet = DbContext.Set<TrdEntity>().AsNoTracking();
if (writeDb)
//读操作路由到写库
return dbSet.TagWith(EfCoreConsts.MyCAT_ROUTE_TO_MASTER);
return dbSet;
} public virtual async Task<IEnumerable<TResult>> QueryAsync<TResult>(string sql, object param = null, int? commandTimeout = null, CommandType? commandType = null, bool writeDb = false)
{
if (writeDb)
//这个方法集成了dapper实现复杂查询,读操作路由到写库
sql = string.Concat("/* ", EfCoreConsts.MyCAT_ROUTE_TO_MASTER, " */", sql);
return await DbContext.Database.GetDbConnection().QueryAsync<TResult>(sql, param, null, commandTimeout, commandType);
}
}

基于mycat要写的代码就是上面这些,数据库连接字符串与直连数据库一样,端口改成mycat的端口。


参考资料

https://blog.csdn.net/qq_40378034/article/details/91125768

https://www.cnblogs.com/huhongy/p/11206724.html

http://www.mycat.org.cn/document/mycat-definitive-guide.pdf

探讨EFCore如何优雅的实现读写分离的更多相关文章

  1. efcore在Saas系统下多租户零脚本分表分库读写分离解决方案

    efcore在Saas系统下多租户零脚本分表分库读写分离解决方案 ## 介绍 本文ShardinfCore版本x.6.0.20+ 本期主角: - [`ShardingCore`](https://gi ...

  2. EntityFramework Core进行读写分离最佳实践方式,了解一下(一)?

    前言 本来打算写ASP.NET Core MVC基础系列内容,看到有园友提出如何实现读写分离,这个问题提的好,大多数情况下,对于园友在评论中提出的问题,如果是值得深究或者大多数同行比较关注的问题我都会 ...

  3. EF多租户实例:演变为读写分离

    前言 我又来写关于多租户的内容了,这个系列真够漫长的. 如无意外这篇随笔是最后一篇了.内容是讲关于如何利用我们的多租户库简单实现读写分离. 分析 对于读写分离,其实有很多种实现方式,但是总体可以分以下 ...

  4. Spring aop应用之实现数据库读写分离

    Spring加Mybatis实现MySQL数据库主从读写分离 ,实现的原理是配置了多套数据源,相应的sqlsessionfactory,transactionmanager和事务代理各配置了一套,如果 ...

  5. 主要从架构上来做优化,负载均衡、CDN、静态化、数据库的水平切割和纵向切割、读写分离、分布式缓存着手

    语言知识一种工具,甚至技术本身也只是一种工具,本身并不值钱,关键在于用于何种行业,产生了什么价值. 但从语言来看,我个人更喜欢php,然后是C#,然后是java从框架而言,先是java,然后C#,再次 ...

  6. spring读写分离(配置多数据源)[marked]

    我们今天的主角是AbstractRoutingDataSource,在Spring2.0.1发布之后,引入了AbstractRoutingDataSource,使用该类可以实现普遍意义上的多数据源管理 ...

  7. Mysql + keepalived 实现双主热备读写分离【转】

    Mysql + keepalived 实现双主热备读写分离 2013年6月16日frankwong发表评论阅读评论   架构图 系统:CentOS6.4_X86_64软件版本:Mysql-5.6.12 ...

  8. MySQL多数据源笔记2-Spring多数据源一主多从读写分离(手写)

    一.为什么要进行读写分离呢? 因为数据库的"写操作"操作是比较耗时的(写上万条条数据到Mysql可能要1分钟分钟).但是数据库的"读操作"却比"写操作 ...

  9. DBA 小记 — 分库分表、主从、读写分离

    前言 我在上篇博客 "Spring Boot 的实践与思考" 中比对不同规范的 ORM 框架应用场景的时候提到过主从与读写分离,本篇随笔将针对此和分库分表进行更深入地探讨. 1. ...

随机推荐

  1. 第8.8节 Python使用__new__方法和构造方法__init__完成类实例化的过程详解

    第8.8节 Python使用__new__方法和构造方法__init__完成类实例化的过程详解 前面章节介绍了Python类中的__new__方法和构造方法__init__,并通过实例分析了二者之间关 ...

  2. PyQt(Python+Qt)学习随笔:工具箱(QToolBox)的用途及标签部件项(tabbed widget item)作用介绍

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 1.概述 toolBox工具箱是一个容器部件,对应类为QToolBox,在其内有一列从上到下顺序排列 ...

  3. Python正则表达式re.findall一个有趣的现象

    下面通过几个案例来分析一下, 注意:本节的parsematch函数请参考<妙用re.sub分析正则表达式解析匹配过程> 案例一: >>> re.findall(r&quo ...

  4. B站自动爬取器并制作词云

    效果 词云展示 弹幕展示 爬取弹幕过程 基本步骤 1.寻找视频url 2.构造请求头 3.寻找弹幕地址 4.根据弹幕地址运用正则或xpath爬取 寻找B站视频的url 制作请求头 headers = ...

  5. 【软件测试部署基础】gradle的认识

    1. gradle简介 Java世界中主要有三大构建工具:Ant.Maven和Gradle.经过几年的发展,Ant几乎销声匿迹.Maven也日薄西山,而Gradle的发展则如日中天. 1.1. ANT ...

  6. 性能测试学习之路 (三)jmeter常见性能指标(相关术语、聚合报告 && 服务器性能监控配置 && 图形结果 && 概要报告)

    1 性能测试目的 性能测试的目的:验证软件系统是否能够达到用户提出的性能指标,同时发现软件系统中存在的性能瓶颈,以优化软件. 最后起到优化系统的目的性能测试包括如下几个方面: 1.评估系统的能力:测试 ...

  7. IAR FOR STM8 同一个工程芯片选择003F3可以编译003K3提示空间不足

    同一个工程文件,选择103F3可以编译通过,但是选择103K3便提示空间不足 百思不得其解,查阅大量资料无果.最后在IAR工程里面找到了配置文件 打开003f3的配置文件和003K3配置文件进行对比, ...

  8. Scrum 冲刺 第五篇

    Scrum 冲刺 第五篇 每日会议照片 昨天已完成工作 队员 昨日完成任务 黄梓浩 初步完成app项目架构搭建 黄清山 完成部分个人界面模块数据库的接口 邓富荣 完成后台首页模块数据库的接口 钟俊豪 ...

  9. 【SDOI2017】天才黑客(前后缀优化建图 & 最短路)

    Description 给定一张有向图,\(n\) 个点,\(m\) 条边.第 \(i\) 条边上有一个边权 \(c_i\),以及一个字符串 \(s_i\). 其中字符串 \(s_1, s_2, \c ...

  10. 四、LoadRunner11安装和破解

    之前安装了LoadRunner12 社区版的,应为满足不了工作需求, 上网仔细查了教程下来LoadRunner11破解版 链接:https://pan.baidu.com/s/1dM8Lwf4p160 ...