探討EF Core如何優雅的實現讀寫分離


微服務開發框架 代碼改變世界 開源推動社區

前言

    我們都知道當單庫系統遇到性能瓶頸時,讀寫分離是首要優化手段之一。因為絕大多數系統讀的比例遠高於寫的比例,並且大量耗時的讀操作容易引起鎖表導致無發寫入數據,這時讀寫分離就更加重要了。

    EF Core如何通過代碼實現讀寫分離,我們可以搜索到很多案例。總結起來一種方法是注冊一個DbContextFactory,讀操作注入ReadDcontext,寫操作注入WriteDbcontext;另外一種是動態修改數據庫連接串。

    以上無論哪種方法,實現簡單粗暴的讀寫分離功能也不復雜。但是如果需要實現從庫狀態監測(從庫宕機)、主備自動切換(主庫宕機)、從庫靈活的負載均衡配置、耗時查詢的SQL路由到指定的從庫、指定一些表不需要讀寫分離(如:基礎數據表)等等,隨着系統的數據量增加,以后還會涉及到分片(分庫,分表),分片后又會涉及到分布式事務,以上這些如果通過業務代碼去實現那需要費太多腦子且穩定性是個大問題。

    有沒有更優雅的法案?中間件或許是個不錯的選擇,EF Core也一樣可以很好的基於中間實現讀寫分離。

為什么要使用中間件

  • 讀寫分離采用客戶端直連方案(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://aspdotnetcore.net/ef-core-readwrite/
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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM