Colder框架硬核更新(Sharding+IOC)

目錄

引言

前方硬核警告:全文乾貨11000+字,請耐心閱讀
遙想去年這個時候,差很少剛剛畢業,現在正式工做差很少一年了。Colder開源快速開發框架從上次版本發佈至今差很少有三個月了,Github的星星5個版本框架總共也有近800顆,QQ羣從最初的一我的發展到如今的500人(吐槽下,人數上限了,太窮開不起SVIP,因此另開了一個,羣號在文章末),這都是你們共同發展的結果,本框架可以幫助到你們鄙人就十分開心。可是,技術是不斷髮展的,本框架也必須適應潮流,不斷升級纔可以與時俱進,在實際意義上提升生產力。本系列框架從原始雛形(鄙人畢業設計)=>.NET45+Easyui=>.NET Core2.1+Easyui=>.NET45+AdminLTE=>.NET Core2.1+AdminLTE,這其中都是根據實際狀況不斷升級。例如鄙人最初的畢業設計搭建了框架的雛形(倉儲層不夠完善、界面較簡陋),並不適合實際的生產開發,所以使用Easyui做爲前端UI框架(控件豐富,使用簡單),後又因爲.NET Core的發展迅速,已經發展到2.0,其基礎類庫組件也相對比較成熟了,所以從.NET45遷移到.NET Core。後來發現Easyui的樣式比較落後,給人一種過期古老的感受,故而又將前端UI改成基於Bootstrap的AdminLTE,比較成熟主流而且開源。
可是,新的要求又出現了:前端

  • 因爲沒有使用IOC致使各個類經過New致使的強耦合問題
  • 數據庫大數據量如何處理的問題
    所以,本次版本更新主要就是爲了解決上述的問題,即全面使用Autofac做爲IOC容器實現解耦以及數據庫讀寫分離分庫分表(Sharding)支持。下面將分別介紹。
    此次更新.NET45版本與.NET Core版本同步更新:
.NET版本 前端UI 地址
Core2.2 AdminLTE https://github.com/Coldairarrow/Colder.Fx.Core.AdminLTE
.NET4.52 AdminLTE https://github.com/Coldairarrow/Colder.Fx.Net.AdminLTE

控制反轉

IOC(DI),即控制反轉(依賴注入),相關概念你們應該都知道,而且大多數人應該都已經運用於實際。我就簡單描述下,簡單講就是面向接口編程,經過接口來解除類之間的強耦合,方便開發維護測試。這個概念在JAVA開發中應該比較廣泛,由於有Spring框架的正確引導,可是在.NET中可能開發人員的相關意識就沒那麼強,JAVA與.NET我這裏不作評價,可是做爲技術人員,天生就是不斷學習的,好的東西固然要學習,畢竟技多不壓身。vue

在.NET 領域中IOC框架主流有兩個,即Autofac與Unity,這兩個都是優秀的開源框架,通過一番考量後我最終選擇了更加主流的(星星更多)Autofac。git

關於Autofac的詳細使用教程請看官方文檔https://autofac.org/,我這裏主要介紹下集成到本框架的思路以及用法。
傳統使用方法經過手動註冊具體的類實現某接口,這種作法顯然不符合實際生產需求,須要一種自動註冊的方式。本框架經過定義兩個接口類:IDependency與ICircleDependency來做爲依賴注入標記,全部須要使用IOC的類只須要繼承其中一個接口就行了,其中IDependency是普通注入標記,支持屬性注入但不支持循環依賴,ICircleDependency是循環依賴注入標記,支持循環依賴,實際使用中按需選擇便可。下面代碼就是自動註冊的實現:github

var builder = new ContainerBuilder();

var baseType = typeof(IDependency);
var baseTypeCircle = typeof(ICircleDependency);

//Coldairarrow相關程序集
var assemblys = BuildManager.GetReferencedAssemblies().Cast<Assembly>()
    .Where(x => x.FullName.Contains("Coldairarrow")).ToList();

//自動注入IDependency接口,支持AOP
builder.RegisterAssemblyTypes(assemblys.ToArray())
    .Where(x => baseType.IsAssignableFrom(x) && x != baseType)
    .AsImplementedInterfaces()
    .PropertiesAutowired()
    .InstancePerLifetimeScope()
    .EnableInterfaceInterceptors()
    .InterceptedBy(typeof(Interceptor));

//自動注入ICircleDependency接口,循環依賴注入,不支持AOP
builder.RegisterAssemblyTypes(assemblys.ToArray())
    .Where(x => baseTypeCircle.IsAssignableFrom(x) && x != baseTypeCircle)
    .AsImplementedInterfaces()
    .PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies)
    .InstancePerLifetimeScope();

//註冊Controller
builder.RegisterControllers(assemblys.ToArray())
    .PropertiesAutowired();

//註冊Filter
builder.RegisterFilterProvider();

//註冊View
builder.RegisterSource(new ViewRegistrationSource());

//AOP
builder.RegisterType<Interceptor>();

var container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

AutofacHelper.Container = container;

代碼中有相關注釋,使用方法推薦使用構造函數注入:

框架已在Business層與Web層全面使用DI,Util層、DataRepository層與Entity層不涉及業務邏輯,所以不使用DI。算法

讀寫分離分庫分表

前面的IOC或許沒啥可驚喜的,可是數據庫讀寫分離分庫分表應該不會讓你們失望。接下來將闡述下框架支持Sharding的設計思路以及具體使用方法。數據庫

理論基礎

數據庫讀寫分離分庫分表(如下簡稱Sharding),這並非什麼新概念,網上也有許多的相關資料。其根本就是爲了解決一個問題,即數據庫大數據量如何處理的問題。編程

當業務規模較小時,使用一個數據庫便可知足,可是當業務規模不斷擴大(數據量增大、用戶數增多),數據庫最終將會成爲瓶頸(響應慢)。數據庫瓶頸主要有三種狀況:數據量不大可是讀寫頻繁數據量大可是讀寫不頻繁以及數據量大而且讀寫頻繁c#

首先,爲了解決數據量不大可是讀寫頻繁致使的瓶頸,須要使用讀寫分離,所謂讀寫分離就是將單一的數據庫分爲多個數據庫,一些數據庫做爲寫庫(主庫),一些數據庫做爲讀庫(從庫),而且開啓主從複製(實時將寫入的數據同步到從庫中),這樣將數據的讀寫分離後,將原來單一數據庫用戶的讀寫操做分散到多個數據庫中,極大的下降了數據庫壓力,而且打多數狀況下讀操做要遠多於寫操做,所以實際運用中大多使用一主多從的模式。後端

其次,爲了解決數據量大可是讀寫不頻繁致使的瓶頸,須要使用分庫分表。其實思想也是同樣的,即分而治之,一切複雜系統都是經過合理的拆分從而有效的解決問題。分庫分表就是將原來的單一數據庫拆分爲多個數據庫,將原來的一張表拆分爲多張表,這樣表的數據量就將下來了,從而解決問題。可是,拆表並非胡亂拆的,隨便拆到時候數據都找不到,那還怎麼玩,所以拆表須要按照必定的規則來進行。最簡單的拆表規則,就是根據Id字段Hash後求餘,這種方式使用簡單可是擴容很麻煩(絕大多數都須要遷移,工做量巨大,十分麻煩),所以大多用於基本無需擴容的業務場景。後來通過一番研究後,發現可使用雪花Id(分佈式自增Id)來解決問題,雪花Id中自帶了時間軸,所以在擴容時能夠根據時間段來判斷具體的分片規則,從而擴容時無需數據遷移,可是存在必定程度上的數據熱點問題。最後,找到了葵花寶典-一致性哈希,關於一致性哈希的理論我這裏就不獻醜了,相關資料網上一大把。一致性哈希從必定程度上解決了普通哈希的擴容問題與數據熱點問題,框架也支持使用一致性哈希分片規則。緩存

最後,就是大BOSS,大數據量與大訪問量,很簡單隻須要結合讀寫分離與分庫分表便可,下表是具體業務場景與採用方案的關係
| 數據量\訪問量 | | |
|-|-|-|
|| 無| 讀寫分離 |
| | 分庫分表 |讀寫分離分庫分表|

設計目標

首先定一個小目標(先賺他一個億):支持多種數據庫,使用簡單,業務升級改動小。
有了目標就須要調查業界狀況,實現Sharding,市面上主要分爲兩種,即便用中間件與客戶端實現。

現狀調研

中間件的優勢是對客戶端透明,即對於客戶端來說中間件就是數據庫,所以對於業務改動影響幾乎沒有,可是對中間件的要求就很高,目前市面上比較主流成熟的就是mycat,其對MySQL支持比較好,可是對於其餘數據庫支持就比較無力(我的測試,沒有深刻研究,如有不妥請不要糾結),而且不支持EF,此方案行不通。其它類型數據庫也有對應的中間件,可是都並不如意,本身開發更不現實,所以使用中間件方案行不通。

既然中間件行不通,那就只能選擇客戶端方案了。目前在JAVA中有大名鼎鼎的Sharding-JDBC,瞭解了下貌似很牛逼,惋惜.NET中並無Sharding-NET,可是有FreeSql,粗略瞭解了下是一個比較強大ORM框架,但個人框架原來底層是使用EF的,而且EF是.NET中主流的ORM框架,總體遷移到FreeSql不現實,所以最終沒找到成熟的解決方案。

設計思路

最後終於到了最壞的狀況,既沒有完美的中間件方案,又沒有現成的客戶端方案,怎麼辦呢?放棄是不可能的,這輩子都不可能放棄的,終於,心裏受到了黨的啓發,決定另起爐竈(既然沒有現成的那就本身早造)、打掃乾淨屋子再請客(重構數據倉儲層,實現Sharding)、一邊倒(堅決目標不改變,不妥協),因爲EF支持多種數據庫,已經對底層SQL進行了抽象封裝,所以決定基於EF打造一套讀寫分離分庫分表方案。

數據庫讀寫分離實現:讀寫分離比較簡單,在倉儲接口中已經明肯定義了CRUD操做接口,其中增、刪、改就是指寫操做,寫的時候按照具體的讀寫規則找到具體的寫庫進行寫操做便可,讀操做(查數據)按照具體的讀規則找到具體的讀庫進行讀便可。

數據庫分庫分表:分庫還好說,使用不一樣的數據庫便可,分表就比較麻煩了。首先實現分表的寫操做,能夠根據分片規則可以找到具體的物理表而後進行操做便可,實現比較容易。而後實現分表的讀操做,這個就比較麻煩了,就比如前面的都是鬥皇如下的在小打小鬧,而這個倒是鬥帝(騎馬),可是,做爲一名合格的攻城獅是不怕鬥帝的,遇到了困難不要慌,須要冷靜思考處理。前面提到過,解決複雜問題就是一個字「」,首先聯表查詢就直接不考慮支持了(大數據量進行笛卡爾積就是一種愚蠢的作法,怎麼優化都沒用,物理數據庫隔絕聯表不現實,實現難度太大放棄)。接下來考慮最經常使用的方法:分頁查詢、數據篩選、最大值、最小值、平均值、數據量統計,EF中查詢都是經過IQueryable接口實現的,IQueryable中主要包括了數據源(特定表)與關聯的表達式樹Expression,經過考慮將數據源與關聯的表達式樹移植到分表的IQueryable便可實現與抽象表相同的查詢語句,最後將併發多線程查詢分表的數據經過合併算法便可獲得最終的實際數據。想法很美好,現實很殘酷,下面爲你們簡單闡述下實現過程,能夠說是過五關斬六將

實現之過五關斬六將

動態對象

首先考慮分表的寫操做,傳統用法都有具體的實體類型進行操做,可是分表時,例如Base_UnitTest_0、Base_UnitTest_一、Base_UnitTest_2,這些表所有保存爲實體類不現實,所以須要一種非泛型方法,後來在EF的關鍵類DbContext中找到DbEntityEntry Entry(object entity)方法,經過DbEntityEntry能夠實現數據的增刪改操做,又注意到傳入參數是object,由此猜想EF支持非泛型操做,即只須要傳入特定類型的object對象也行。例如抽象表是Base_UnitTest,實際須要映射到表Base_UnitTest_0,那麼怎樣將Base_UnitTest類型的對象轉換成Base_UnitTest_0類型的對象?通過查閱資料,能夠經過System.Reflection.Emit命名空間下的TypeBuilder在運行時建立動態類型,便可以在運行時建立Base_UnitTest_0類型,該類型擁有與Base_UnitTest徹底同樣的屬性(由於表結構徹底同樣),建立了須要的類型,接下來只須要經過Json.NET將Base_UnitTest對象轉爲Base_UnitTest_0便可。實現到這裏,原覺得會順利成功,可是並無那麼簡單,EF直接報錯「上下文不包含模型Base_UnitTest_0」,這明顯就是模型的問題了,接下來進入下一關:EF動態模型緩存

動態模型緩存

一般都是經過繼承DbContext重寫OnModelCreating方法來註冊實體模型,這裏有個坑就是OnModelCreating只會執行一次,並最終生成DbCompiledModel而後將其緩存,後續建立的DbContext就會直接使用緩存的DbCompiledModel,因爲最初註冊實體模型的時候只有抽象類型Base_UnitTest,全部後續在使用Base_UnitTest_0對象的時候會報錯。爲了解決這個問題,須要本身管理DbCompiledModel緩存,實現過程比較麻煩,這裏就不詳細分析了,有興趣的直接看源碼便可。將緩存問題解決後,終於成功的實現了Base_UnitTest_0的增刪改,這時,內心一喜(有戲)。實現了寫操做(增、刪、改)以後,接下來就是實現查詢了,那麼如何實現查詢呢?EF中查詢操做都是經過IQueryable接口實現的,IQueryable中包括了具體數據表的數據源和關聯的查詢表達式樹,那麼如何將IQueryable < Base_UnitTest >轉換爲IQueryable < Base_UnitTest_0 > 而且保留原始查詢語句就成了關鍵問題。

數據源移植

根據經驗,想一舉同時移植數據源與表達式樹應該不現實,實際狀況也是如此,移植數據源,經過使用ExpressionVisitor能夠找到根數據源,實際上是一個ObjectQuery類型,而且在表達式樹中是以ConstantExpression存在,一樣經過ExpressionVisitor則可將原ObjectQuery替換爲新的,實現過程省略10000字。

查詢表達式樹深度移植

數據源移植後,別覺得就大功告成了,接下來進入一個深坑(最難點),表達式樹移植,通過一番踩坑後發現,表達式樹中的全部節點都是樹狀結構,任何一個查詢(Where、OrderBy、Skip、Take等)在表達式樹中都是以一個節點存在,而且一級扣一級,也就是說你改了數據源沒用,由於數據源只是表達式樹的根節點,下面的全部子節點還都是原來的根節點發的牙,並不能使用,那怎樣才能用新數據源構建與原數據源同樣的表達式樹呢?通過以下分析:IQuryable中的全部操做都是MethodCallExpression一層一層包裹,那麼我從外到內剝開方法,而後再從內到外包裹新的數據源,那不就模擬得如出一轍了嗎?(貌似有戲),想到先進後出腦子裏直接就蹦出了數據結構中的,強大的.NET固然支持棧了,通過一番操做(奮鬥幾個晚上),此處省略10000字,最終完成IQueryable的移植,即從IQueryable < Base_UnitTest >轉換爲IQueryable < Base_UnitTest_0 > 而且保留原始查詢語句。有了分表的IQueryable就可以獲取分表的數據了,最後須要將獲取的分表數據進行合併。

數據合併算法

分表後的數據合併算法主要參考了網上的一些資料,雖然分庫分表的實現方式各不相同,可是思想都是差很少的,例如須要獲取Count,只須要將各個分表的Count求和便可,最大值只須要全部分表的最大值的最大值便可,最小值只須要全部分表最小值的最小值便可,平均值須要全部分表的和而後除以全部分表的數據條數便可。最後比較麻煩的就是分頁查詢,分頁查詢須要分表排序後獲取前N頁的全部數據(不能直接獲取某一頁的數據,由於不必定就是那一頁),最後將全部表的數據再進行分頁便可。實現到這裏,已經實現了增、刪、改、查了,看似革命已經成功,其實還有最後的大BOSS:事務支持

事務支持

由於分表極可能不在同一個數據庫中,由於普通的單庫事務顯然不能知足需求,本來框架中已經有分佈式事務支持(多庫事務),這裏須要集成到Sharding中,實現過程省略10000字,最終黃天不負有心人終於實現了。

到這裏,確定有暴躁老哥坐不住了:你前面BBB那麼多,說得那麼牛逼,到底怎麼用啊???,若文章到此爲止,估計就是下圖:

鄙人則回覆以下:

深夜12點了,放鬆一下,最後介紹如何使用

實際使用

本框架支持數據庫讀寫分離分庫分表(即Sharding),而且支持主流關係型數據庫(SQLServer、Oracle、MySQL、PostgreSQL),理論上只要EF支持那麼本框架支持。
因爲技術緣由以及結合實際狀況,目前本框架僅支持單表的Sharding,即支持單表的CRUD、分頁、統計(數量、最大值、最小值、平均值),支持跨庫(表分散在不一樣的數據庫中,不一樣類型數據庫也支持)。具體如何使用以下:

  • Sharding配置
    首先、要進行分庫分表操做,那麼必要的配置必不可少。配置代碼以下:
ShardingConfigBootstrapper.Bootstrap()
    //添加數據源
    .AddDataSource("BaseDb", DatabaseType.SqlServer, dbBuilder =>
    {
        //添加物理數據庫
        dbBuilder.AddPhsicDb("BaseDb", ReadWriteType.ReadAndWrite);
    })
    //添加抽象數據庫
    .AddAbsDb("BaseDb", absTableBuilder =>
    {
        //添加抽象數據表
        absTableBuilder.AddAbsTable("Base_UnitTest", tableBuilder =>
        {
            //添加物理數據表
            tableBuilder.AddPhsicTable("Base_UnitTest_0", "BaseDb");
            tableBuilder.AddPhsicTable("Base_UnitTest_1", "BaseDb");
            tableBuilder.AddPhsicTable("Base_UnitTest_2", "BaseDb");
        }, new ModShardingRule("Base_UnitTest", "Id", 3));
    });

上述代碼中完成了Sharding的配置:
ShardingConfigBootstrapper.Bootstrap()在一個項目中只能執行一次,因此建議放到Application_Start中(ASP.NET Core中的Startup)
AddDataSource是指添加數據源,數據源能夠看作抽象數據庫,一個數據源包含了一組同類型的物理數據庫,即實際的數據庫。一個數據源至少包含一個物理數據庫,多個物理數據庫須要開啓主從複製或主主複製,經過ReadWriteType(寫、讀、寫和讀)參數來指定數據庫的操做類型,一般將寫庫做爲主庫,讀庫做爲從庫。同一個數據源中的物理數據庫類型相同,表結構也相同。
配置好數據源後就能夠經過AddAbsDb來添加抽象數據庫,抽象數據庫中須要添加抽象數據表。如上抽象表Base_UnitTest對應的物理表就是Base_UnitTest_0、Base_UnitTest_1與Base_UnitTest_2,而且這三張表都屬於數據源BaseDb。分表配置固然須要分表規則(即經過一種規則找到具體數據在哪張表中)。
上述代碼中使用了最簡單的取模分片規則
源碼以下:

能夠看到其使用方式及優缺點。
另外還有一致性HASH分片規則

雪花Id的mod分片規則

上述的分片規則各有優劣,都實現IShardingRule接口,實際上只須要實現FindTable方法便可實現自定義分片規則。
實際使用中我的推薦使用雪花Id的mod分片規,這也是爲何前面數據庫設計規範中默認使用雪花Id做爲數據庫主鍵的緣由(PS,以前版本使用GUID做爲主鍵被各類嫌棄,此次看大家怎麼說)

  • 使用方式
    配置完成,下面開始使用,使用方式很是簡單,與日常使用基本一致
    首先獲取分片倉儲接口IShardingRepository
IShardingRepository _db = DbFactory.GetRepository().ToSharding();

而後便可進行數據操做:

Base_UnitTest _newData  = new Base_UnitTest
{
    Id = Guid.NewGuid().ToString(),
    UserId = "Admin",
    UserName = "超級管理員",
    Age = 22
};
List<Base_UnitTest> _insertList = new List<Base_UnitTest>
{
    new Base_UnitTest
    {
        Id = Guid.NewGuid().ToString(),
        UserId = "Admin1",
        UserName = "超級管理員1",
        Age = 22
    },
    new Base_UnitTest
    {
        Id = Guid.NewGuid().ToString(),
        UserId = "Admin2",
        UserName = "超級管理員2",
        Age = 22
    }
};
//添加單條數據
_db.Insert(_newData);
//添加多條數據
_db.Insert(_insertList);
//清空表
_db.DeleteAll<Base_UnitTest>();
//刪除單條數據
_db.Delete(_newData);
//刪除多條數據
_db.Delete(_insertList);
//刪除指定數據
_db.Delete<Base_UnitTest>(x => x.UserId == "Admin2");
//更新單條數據
_db.Update(_newData);
//更新多條數據
_db.Update(_insertList);
//更新單條數據指定屬性
_db.UpdateAny(_newData, new List<string> { "UserName", "Age" });
//更新多條數據指定屬性
_db.UpdateAny(_insertList, new List<string> { "UserName", "Age" });
//更新指定條件數據
_db.UpdateWhere<Base_UnitTest>(x => x.UserId == "Admin", x =>
{
    x.UserId = "Admin2";
});
//GetList獲取表的全部數據
var list=_db.GetList<Base_UnitTest>();
//GetIQPagination獲取分頁後的數據
var list=_db.GetIShardingQueryable<Base_UnitTest>().GetPagination(pagination);
//Max
var max=_db.GetIShardingQueryable<Base_UnitTest>().Max(x => x.Age);
//Min
var min=_db.GetIShardingQueryable<Base_UnitTest>().Min(x => x.Age);
//Average
var min=_db.GetIShardingQueryable<Base_UnitTest>().Average(x => x.Age);
//Count
var min=_db.GetIShardingQueryable<Base_UnitTest>().Count();
//事務,使用方式與普通事務一致
using (var transaction = _db.BeginTransaction())
{
    _db.Insert(_newData);
    var newData2 = _newData.DeepClone();
    _db.Insert(newData2);
    bool succcess = _db.EndTransaction().Success;
}

上述操做中表面上是操做Base_UnitTest表,實際上卻在按照必定規則使用Base_UnitTest_0~2三張表,使分片對業務操做透明,極大提升開發效率,基本達成了最初定製的小目標。
具體使用方式請參考單元測試源碼:
"\src\Coldairarrow.UnitTests\DataRepository\ShardingTest.cs"

最後放上簡單的測試圖:300W的表分紅三張100W的表後效果


看來功夫沒白費,效果明顯(還不快點贊

展望將來

結束也是是新的開始,版本後續計劃採用先後端徹底分離方案,前端使用vue-element-admin,後端以.NET Core爲主,傳統的.NET將逐步中止更新,敬請期待!
文章雖然結束了,可是技術永無止境,但願個人文檔可以幫助到你們。
深夜碼字,實屬不易,文章中不免會出現一些紕漏,一些觀點也不必定徹底正確,還望各位大哥不吝賜教。
最後以爲文檔不錯,請點贊,Github請星星,如有各類疑問歡迎進羣交流:
QQ羣1:373144077(已滿)
QQ羣2:579202910

See You
相關文章
相關標籤/搜索