自從CYQ.Data框架出了數據庫讀寫分離、分布式緩存MemCache、自動緩存等大功能之後,就進入了頻繁的細節打磨優化階段。
從以下的更新列表就可以看出來了,3個月更新了100條次功能:
305:處理視圖名重復時的問題,同時簡化MDataTable的代碼,取消MDataTable的ReadFromDbDataReader(統一用CreateFrom(sdr)方法讀取。(2016-07-16) 306:優化通過Reader獲取列結構(該方法不靠譜,需要重新修正元數據的DataType、Size、Scale、DalType等參數)(2016-07-16) 307:為MDataTable和MAction的Set方法增加重載Set(key,value,state),在循環賦值時,產生批量更新時,可以對state賦值2(2016-07-21) 308:增加貼心功能:自定義參數化語句@符號,在各數據庫自動被替換成相應的?或:符號兼容多數據庫。(2016-07-22) 309:增強MDataTable綁定到Winform和WFP的功能(2016-07-23) 310:修正MProc的ExeMDataTableList的自動緩存問題2016-07-23) 311:DBTool的GetMapTable增加對表名-中劃線符號和空格的兼容處理(2016-07-30) 312:CYQ.Data.ProjectTool 升級版本到V2.0(支持英文環境)(2016-07-30) 313:XHtmlAction大力調整升級(細節改動多)(2016-07-31) 314:MDataRow:SetToAll和MDataTable的Bind功能支持XHtmlAction對象(2016-07-31) 315:Dtd文件變成資源文件合在V5裡,用到時動態自動解壓提升使用體驗(為減少文件大小,刪注釋扣到我差點眼瞎)(2016-07-31) 316:處理MDataTable的GetChange方法引發的Bug和CreateFrom產生的數據初始狀態置為1(2016-08-02) 317:XHtmlAction處理對radio標簽的處理。(2016-08-02) 318:XHtmlAction增加html的clearflag標簽【值為0(清除InnerXml)或1(節點移除)】(用於節點未處理時,處理掉標簽)(2016-08-02) 319:XHtmlAction處理html的img,select,input checkbox等節點的處理。(2016-08-02) 320:XHtmlActon重寫Load方法(優化加載,自動識別,並處理該<轉義的符號)(2016-08-03) 321:AppConfig減少一個Xml相關的配置項(UseFileLoadXml)(2016-08-03) 322:MDataTable修正Select方法(修正為引用)(2016-08-04) 323:DBTool的GetTables方法增加Lock(2016-08-04) 324:修正失敗時仍緩存的問題(2016-08-08) 325:增加AppConfig.RunPath屬性,獲取框架運行的所在文件夾(2016-08-09) 326:處理配置工具ProjectTool升級(2016-08-09) 327:修正MDataCell對二進制數據二次賦值(2016-08-10) 328:調整MDataRow:CreateFrom(外部數據)的行狀態初始始為1;LoadFrom(外部數據)的狀態和自身值有關(2016-08-10) 329:修正自動緩存(2016-08-10) 330:Oracle修正第1頁分頁問題【當排序條件為字符串時】(2016-08-11) 331:AppConfig新增加NoCacheTables屬性,允許指定某些表不允許緩存(自動緩存開啟時)(2016-08-11) 332:XHtmlAction 增加對Xml文檔中&符號的處理(2016-08-15) 333:XHtmlAction 對SetForeachEventHandler事件做優化調整(2016-08-15) 334:MProc的SetCustom方法增加對MSSQL用戶自定義表類型的支持(2016-08-15) 335:StaticTool:提升了ChangeType方法的轉換性能(2016-08-18) 336:MDataTable的ToList<T>方法增加一個判斷條件,預防繼承OrmBase的遠程實體使用Emit(2016-08-18) 337:JsonHelper:優化提升了大數量下的ToString()的性能(2016-08-19) 338:AutoCache:當數據>10萬條時不自動緩存(2016-08-19) 339:MDataRow:修正索引取值(在字段名為2個符號同時字段數>10時候產生的問題)(2016-08-23) 340:內部SQL語句優化(2016-08-23) 341:MAction:Select方法(優化查詢記錄總數的代碼,利用自動緩存功能,避免分頁時重新計算)(2016-08-24) 342:AppConfig.Cache.IgnoreCacheColumns,可以指定表的某些列的更新操作時不更新緩存(2016-08-24) 343:JsonHelper增加對數組的檢測支持(2016-08-25) 344:JsonHelper支持對二進制和Base64的轉換(2016-08-26) 345:DBTool.CreateTable或DBTool.DropTable後的緩存處理(2016-08-27)。 346:MAction、MProc取消SetAopOn和SetAopOff方法,統一為:SetAopState方法(簡化方法,同時能處理更多的狀態,包括關閉自動緩存)(2016-08-27) 347:MDataTable的Select方法增強(對浮點數的比較)(2016-08-27) 348:AutoCache的緩存時間,改成DefaultCacheTime配置的時間,(用戶可以自己配置自動緩存時間)(2016-08-27) 349:MProc的ExeMDataTableList方法增加對Oracle的批量語句的支持(2016-08-27) 350:優化Oracle拿表結構的語句(2016-08-27) 351:MDataTable的Merge方法修正(2016-08-27) 352:ThreadBreak的AddGlobalThread增加重載方法(2016-08-27) 353:CacheManage提供PreLoadDBSchemaToCache方法(2016-08-27) 354:JsonSplit處理IsJson判斷問題(2016-08-28) 355:MDataTable AcceptChanges(Update方法,處理當配置了AppConfig.DB.DeleteField時引發的問題)(2016-08-30) 356:DBTool.GetColumns方法處理"\r\nwhere"場景時產生的錯誤(2016-09-02) 357:文本數據庫(NoSqlCommand)增對select a as b 別名的支持(2016-09-02) 358:MAction處理多次Fill時未清理舊值的問題(2016-09-02) 359:ORM(OrmBase和SimpleOrmBase)增加SetAopState方法(2016-09-02) 360:AutoCache:處理MAction的Fill方法的時的緩存引用(改成克隆,避免多次Fill指向同一緩存)(2016-09-02) 361:MDataTable增加Description屬性。(2016-09-03) 362:DBTool的GetColumns增加對表映射的支持(2016-09-05) 363:修正文本數據庫的ResetTable方法(原表沒有清空)(2016-09-06) 364:改造並去掉內部的MD5(win2008下加密算法默認引發異常)(2016-09-08) 365:去掉映射表的條件限制(支持更多的外部映射)(2016-09-11) 366:修正讀寫分離時,insert into ...select語句處理到分庫的問題(2016-09-12) 367:SqlCreate處理Oracle日期條件的轉換問題。(2016-09-13) 368:SqlCreate增加對GUID類型的檢測(2016-09-20) 369:OrmBase、SimpleOrmBase延遲加載初始化(2016-09-20) 370:MAction在Insert時,對Oracle,Mysql等放置(獲取最大值)事務(2016-09-20) 371:MAction在Insert時的InsertOp默認選項變更為:ID,原來為(Fill)(2016-09-20) 372:JsonHelper.ToJson增加對List<MDataTable>和List<DataTable>的支持(2016-09-20) 373:DBBase處理Oracle下返回的DataBase名稱問題。(2016-09-21) 374:Oracle的加載方式進行小細節優化(2016-09-22) 375:StaticTool處理ChangeType中對於Guid的轉換(2016-09-22) 376:SqlCompatible增加對(+ ||)、Left和Right函數的處理(2016-09-24) 377:Oracle的ODP.NET參數添BindByName置為true(2016-09-24) 378:MDataRowCollection AddNew方法,處理Winform(DataGrid綁定時)在空白行和數據行來回點擊時不斷添加空白數據的問題。(2016-09-29) 379:MAction SetPara增加重載方法(2016-09-29) 380:MAction Update的where條件Error時,RecordsAffected值從原來的0變更為-2;(2016-09-30) 381:MDataTable 修正批量更新的返回值問題(2016-09-30) 382:MAction 內部增加IsIgnoreDeleteField 內部屬性(2016-09-30) 383:XHtmlBase 修正對Xml的加載(2016-10-08) 384:SqlValue 調整兩個名稱(GUID和ISNULL)的命名(2016-10-08) 385:MDataTable 修正Select條件為<=的數字判斷問題(2016-10-08) 386:AutoCache(JsonHelper增加Escape屬性、MDataTable增加ToJson重載)不處理\n的轉義替換(2016-10-09) 387:MDataTable ToJson 對於null的數據,默認輸出 xx:null 值(2016-10-09) 388:Oracle(DBTool.GetTables) 增加對視圖的過濾(2016-10-10) 389:JsonHelper 修正實體嵌套的問題、同時增加對數組的支持(2016-10-14) 390:MDataTable AcceptChange 修正無主鍵時的的批量更新(2016-10-14) 391:MDataTable 增加 GetIndex 方法(統計滿足條件的行所在的索引)(2016-10-16) 392:NoSqlAction 文本數據庫修正無法刪除最後1條數據的問題(2016-10-16) 393:MDictionary增加索引取值或賦值。(2016-10-17) 394:XHtmlAction、RSS的OnForeach的參數由Dictionary變為MDictionary(2016-10-17) 395:JsonHelper 修正對數組的輸出和還原(2016-10-17) 396:JsonHelper 修正Json嵌套問題。(2016-10-18) 397:MDataTable 優化批量更新問題。(2016-10-18) 398:MDataRow和MDataColumn 的ToTable() 調整適應(新增智能提示)(2016-10-19) 399:MySql 處理存儲過程Out值。(2016-10-19) 400:MySql 批量方法解決了Bit類型和空表時自增ID被置為1的問題(2016-10-20) 401:JsonHelper、NoSqlAction小優化調整(2016-10-20) 402:MDataTable的AcceptChanges新增加Truncate屬性(2016-10-20) 403:JsonHelper的GetJosnValue(json寫錯順理)變更名稱為GetValue(2016-10-21) 404:NoSqlAction 文本數據庫加強刪除最後一條數據時的並發處理問題(2016-10-23) 405:DBTool.GetColumns修正對於沒有where的group by語句拿表結構的問題(2016-10-24) 406:AppConfig增加SetConn方法(同時增加鏈接緩存)(2016-10-24) 407:SqlCreateForPager 處理分頁的order by aa,bb 沒帶asc的問題(2016-10-25) 408:NoSqlAction(修正第404修改產生刪除後無法批量插入的問題)(2016-10-26) 409:MDataTable的AcceptChanges處理重復批量(同時外部沒有產生事務對象的條件下)的問題(2016-10-26) 410:SqlCompatible 對多語句兼容的關鍵詞(不區分大小寫)(2016-10-26) 411:MDataTable的Description增加表字段說明輸出(2016-10-27) 412:StaticTool優化處理GetDbName的細節(2016-10-28) 413:增加Redis分布式緩存支持(配置AppConfig.Cache.RedisServers)(2016-10-30) 414:為Redis和MemCache增加備份節點支持(配置AppConfig.Cache.RedisServersBak、AppConfig.Cache.MemCacheServersBak)(2016-10-30)
其實更多的時間,是放在ASP.NET Aries 業務開發框架上,上裡下外全部重構了一遍。
前幾天,決定把Redis集成進來,一鼓作氣,解決了。
下面分享一下經歷:
一開始我是拒絕的,不願動態調用第三方的客戶端(關聯依賴的dll太多)。
最近打算支持Redis,有點妥協了,動態加載就動態加載了吧:
考慮著引入:StackExchange.Redis或ServiceStack.Redis?
看著這些DLL,太重量級,方法反射起來也費勁!
中間思維停頓了一會。。。
在尋找Redis的API資料時,無意發現了這個開源的輕量級Bettle.Redis。
看到源碼編繹後才46K,感覺就是它了。
不過才幾刻間,發現了以下幾個問題了:
1:自身雖然46K,但代碼引用了另外兩個3個dll(依賴太多):
2:使用的方法不符合使用習慣,一個命令類型就對應一個類。
3:不支持集群的水平擴展(沒實現支持一致性Hash)。
4:代碼是用.NET 4.0 以下版本寫的,(CYQ.Data 框架是支持2.0起的,改代碼改到我手痛)
所以,以上原因估計是它沒被普及的原因,也是最終沒有被我選擇集成的原因。
但是它開放了源碼、對我還是有點啟發和參考意義。
在決定支持Redis的過程中,花了不少時間掃了Redis的文檔:
更多命令詳情可以看:http://doc.redisfans.com
從這麼一堆的命令中,找到基本命令:Get、Set、Exists、Expire、Info,可憐沒有Add。
其它的命令,多數都是可以用基本命令實現的,就被無視了。
經過短時間內大量的集中思考,決定自己實現了:
框架之前已經集成了MemCache,而Redis和MemCache又大同小異。
一些共性的東西,可以復用:
1:hash算法。
2:一致性Hash(水平擴展)。
3:SocketPool。
4:ServerPool。
5:序列化(壓縮)
剩下的,就是完成Socket和Redis的交互及使用方式。
以下是Redis的協議規范,不過是我實現Redis相關功能後才發現的:
協議規范 redis允許客戶端以TCP方式連接,默認6379端口。傳輸數據都以\r\n結尾。 請求格式 *<number of arguments>\r\n$<number of bytes of argument 1>\r\n<argument data>\r\n 例:*1\r\n$4\r\nINFO\r\n 響應格式 1:簡單字符串,非二進制安全字符串,一般是狀態回復。 +開頭,例:+OK\r\n 2: 錯誤信息。 -開頭, 例:-ERR unknown command 'mush'\r\n 3: 整型數字。 :開頭, 例::1\r\n 4:大塊回復值,最大512M。 $開頭+數據長度。 例:$4\r\mush\r\n 5:多條回復。 *開頭, 例:*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
Bettle.Redis裡有源碼,看看實現就可以了,所以沒找協議規范:
通過幾個小時的引進和代碼調整,測試。
以為大功告成之際,測試到當Set的數據太大時,NetworkStream報異常:此流不支持Seek操作。
懷疑是Redis的Set有大小限制?:用Bettle.Redis自身試了下,發現正常,夢B了。
經代碼調試,發現Bettle的Socket實現(Socket.Send)和Socket池的實現(NetworkStream.Write)不一樣。
Bettle.Redis是把所有的協議構造好一次性Socket.Send(byte[])。
懷疑NetworkStream的默認緩存池太小引發的?:用memCache,Set了大量的數據,發現NetworkStream並沒有拋異常,又夢B了。
懷疑是Redis協議的問題了?:改造代碼,把協議分拆,先發送:$長度 ,再發送數據,發現竟然正常了,無語問蒼天了!
經過一夜一天的折騰,Cache目錄下補了4個類,同時進行了算法優化,清掉一些沒用的代碼。
支持Redis後,發現cyq.data.dll的大小竟然沒變化,結果超出了預期,很好!
最後改造成的源碼結構是:
完整的源碼已經提交在:https://github.com/cyq1162/cyqdata
AppConfig.Cache.RedisServers = "127.0.0.1:6379,127.0.0.1:1121";//配置啟用, AppConfig.Cache.RedisServersBak = "127.0.0.1:6379";//備用配置。 CacheManage cache = CacheManage.RedisInstance;//操作對象 cache.Add("obj", cache.CacheTable);//添加DataTable MDataTable obj = cache.Get<MDataTable>("obj"); Console.WriteLine(obj.Rows.Count); Dictionary<string, string> dic = new Dictionary<string, string>(); dic.Add("路過秋天", "http://www.cnblogs.com/cyq1162"); cache.Add("dic", dic);//添加字段 Dictionary<string, string> dicObj = cache.Get<Dictionary<string, string>>("dic"); Console.WriteLine(dicObj["路過秋天"]); cache.Remove("dic");//移除Dic bool hasKey = cache.Contains("dic");//檢測是否存在 Console.WriteLine(hasKey); Console.Read();
結果:
由於Redis的Get只支持字符串,為了達到支持任意類型,我必須改進算法:
1:存檔:目標是對象時=》進行序列化(對於>128K的會進行壓縮)
2:數據的第1個字節:存檔數據類型。
3:獲取數據時:根據第1個字節,進行准確的數據類型還原。
(aaa是通過命令行Set的,而a0是通過代碼設置的,所以多了\x02的類型標識)
因此:框架靠Set與Get能支持任意類型的存取檔!
內部已經實現了一致性Hash算法,因此省了不少工作:
簡單的描述為:把ip1產生N個hash ,ip2產生N個hash,... 然後排序(最後就看key的hash值離誰最新就粘誰了)
借用一張圖表示為:
在測試的過程中,我填寫了一台異常的主機,發現被分配到異常的主機的key的讀寫都沒反應了:
(我潛意識默認以為會自動轉移到相鄰的主機中)
1:沒有自動切換相鄰的主機【用思考代碼疑問:主動切換可能導致雪崩效應,(累積的壓力可能把所有的服務器都搞掛)】。
2:有重試連接機制(2分鐘試1次)。
1:根據Hash,每一台主機都會指向一台備份機。 2:主機異常時,由備份機代理服務器15分鐘(即每15分檢測主機是否正常一次,如果正常,則恢復主機服務)。
3:當主機恢復時,從備份機裡恢復數據,並清空備份機的數據(未實現)
由於可能同時掛掉N台,所以備份機可能存檔多台主機的信息。
於是算法的思路有3個:
1:數據不要了(主機重新緩存即可) 2:主機被請求時(檢測是否掛過,如果是,讀自身(若沒有)=》讀備份機(同時發表移除指令)(若有數據)=》返回(同時寫入主機) 3:主機被請求時(檢測是否掛過,如果是開啟線程(讀備份機所有Key,檢測Hash是否符合自身,如果是,則從備份讀取並寫入,同時清除備份機的數據)
至此,CYQ.Data已經支持上Redis了,而且在分布式算法上,借了memCache的風,以及改進的算法,顯的更為實用!
當然,細節仍需打磨,代碼還可以改的更簡潔優美。
在分布式已經泛濫的今天,能正確的判斷並用好分布式框架是一種能力的體現。
剛剛群裡有人發了這條消息:
其實前面的問題都可以無視,因為最後解決方案他只是把Redis部署從Windows轉移到Linux就好了。
QPS最大時聽說7萬多(兩台Web分來就是3萬多,大部分是刷票造成的請求)
Redis在Windows上的表現並不如Linux的好,這個可以理解。
但是如果在架構設計方案上稍為調整,其實也毫無壓力了。
最後我發現問題的根源不在於技術,在於人:.NET缺少有足夠知識和思維的架構師。
不要遇到點問題就力不從心,在.NET的陣營上堅持吧,少年!