其實完成這個功能之前,我就在思考:是先把想法寫了來,和大伙討論討論後再實現,還是實現後再寫文論述自己的思維。忽然腦後傳來一個聲音說:你發文後會進入發呆階段。所以還是靜下心,讓我輕輕地把代碼擼完再說。最近這幾天,自己在大腦里演練過各種技術難點,解決方案,推敲了各種該解決的問題,覺的差不多了,才決定擼碼... ...
前方:
其實完成這個功能之前,我就在思考:是先把想法寫了來,和大伙討論討論後再實現,還是實現後再寫文論述自己的思維。
忽然腦後傳來一個聲音說:你發文後會進入發呆階段。
所以還是靜下心,讓我輕輕地把代碼擼完再說。
最近這幾天,自己在大腦里演練過各種技術難點,解決方案,推敲了各種該解決的問題,覺的差不多了,才決定擼碼。
忽然發覺,原來代碼是可以寫在大腦里的。
要是你看到一個員工坐著2天沒寫一行代碼,說明人家是高手,正在大腦編程。
好,不扯,回正文!
傳統ORM的二級緩存為何失效?
有些ORM會提供:如Hibernate。
有些不提供:如EF,不提供是因為知道提供了也沒啥鳥用,因為:
1:你不能強迫一個項目全部用單實體編程,多表時,用戶更偏向於執行SQL語句。
2:沒有分散式緩存做為基礎,解決不了多應用程式部署的緩存策略問題。
因此:
1:若控制不了整個項目用戶的SQL語句,單機的搞不了。
2:沒有分散式緩存做基礎,分散式的都搞不了。
這也是為啥EF一直不提供,是因為看到Hibernate雖然提供但並沒多大卵用的原因吧!
疑惑資料庫已有緩存,為何框架還要造孽?
主要原因:
1:資料庫從請求到建立緩存,需要時間(框架緩存可以減緩資料庫緩存失效時壓力)
2:資料庫是有鏈接數限制的,不可能允許大量併發的直連,需要外界分壓。
3:資料庫的緩存是單機性。
4:資料庫發數據往伺服器的時間比本機緩存的長。
自動緩存設計前的一些思考:
1:一開始我思考的緩存策略,是細化到行或列,於是瞭解資料庫自身緩存後發現資料庫目前也只是做了以表為單位。
2:MSSQL是有提供SqlDependency的緩存依賴項的,它可以從資料庫層面通知你的數據何時失效。
3:但是SqlDependency和SqlCommand依賴太深,無法在所有資料庫層面通用。
4:SqlDependency的緩存依賴只能在本地緩存。
5:其它資料庫不支持依賴通知。
6:所以方案只能通過全局執行與分析,來處理緩存及失效策略。
7:單機時:全局攔截分析,如何分析出表?
8:應用分散式時:緩存及時失效?
9:用戶直接修改資料庫時:緩存如何失效?
還有好多好多問題,一直在思考......
緩存什麼?
1:緩存單個對象時,是直接存檔對象的,Cache返回時會根據本地或是遠程選擇是否Clone返回。。
2:緩存列表時:只存檔欄位類型僅包含:(數字、布爾、字元、時間、GUID)的欄位,並轉成Json字元串存檔。
技術細節:
A:一個對象存檔在本機時,存檔的是引用(可能出現誤寫操作);存檔在分散式時,存檔的不是引用,這會在使用時出現不確定性。
B:大對象的存檔,在緩存來去間需要序列化和反序列化,性能上降低很多。
因此:將列表轉成Json存檔,拿到時再還原,可以同時解決A和B的問題。
簡單的說,如果對象有長欄位,或者有二進位數據,是不會被緩存的,所以MSSQL的Timestamp欄位就不要用了;
如果要用:AppConfig.DB.HiddenFields="欄位名",把它隱藏了也行。
緩存時間?
考慮到通常訪問量低時都是在中午和晚上的時間,因此,將緩存的對象的時間隨機分佈(早上的分佈在中午失效,下午的分佈在晚上失效)
考慮到分頁時的查詢,通常都關註前面幾頁,因此前面幾頁的數據,時間如上的時間段分佈。
分頁後面的數據,只預設2分鐘的緩存時間。
其它規則有待討論......
緩存多大?
1:在單機狀態,檢測到記憶體的可用比例低於15%時,則不再接受緩存。
2:在分散式緩存狀態,暫時有多少扔多少。
緩存如何失效:
1:攔截請求:(包括(MAction)增刪改查+(MProc)執行自定義語句+(MDataTable)批量方法)
2:分析語句的關聯表(單表的可以拿表名,視圖的拿關聯結構涉及的表名,存儲過程(目前沒法),自定義SQL(語句分析出表名),批量(直接拿表名)
3:技術難點:如何從未知的SQL或視圖中準確的分析出所有關聯的表。
4:緩存失效:執行以下方法應該失效:增刪改,執行ExeNonQuery,批量語句。
5:技術難點:
1:對於視圖(關聯了多個表,如何根據一個表名,關聯到相應涉及的視圖語句失效?)
2:對於分散式的應用,A服務更新,如何B伺服器也失效。
如何處理修改頻繁的表:
1:一開始想增加配置,讓用戶設置不參與緩存的表,認真思考後,發現根據緩存失效的時間和次數,可自動分析判斷一個表是否修改頻繁。
2:表操作相關增刪查時,該表被置為失效(相關緩存會被移除),此時設置好時間間隔(6秒),在此時間段對該表相關的不緩存,同時提交的緩存刪命令也可以無視。
3:對被分析出為修改頻繁的表該如何處理?延長相應的不緩存時間,或是??還需要思考!!!
緩存失效的粒度能不能小?
1:目前的失效,和資料庫一樣,是以表為單位的。
2:對於插入操作,不會影響某一條數據的讀取(所以單條數據的查詢,是不應該受到插入操作的影響的)
3:還有其它情形是必然不會影響的?
框架有自動緩存,業務需不需要緩存?
1:資料庫有自動緩存,框架的也可以自動緩存。
2:框架有自動緩存,同理業務也可以有緩存。
框架能處理的粒度是有限的,不能細到具體的行或列的緩存級別,因此在業務複雜和併發到一定量後,業務緩存是必要的。
資料庫有緩存,業務也可做緩存,為何還思考往框架增加自動緩存?
1:資料庫的預設緩存是固定的,需要配置,各種資料庫環境不一致。
2:資料庫鏈接池預設是固定的。
3:業務加緩存的事,往往是後期的動作。
現另一個現狀是:
1:.NET 群體,存在很多初中級的開發人員,這部分人員的技術成長相對較慢,對緩存或性能調優並不熟。
2:國內有很多的中小網站,預設都抗不起併發,攻擊成本很小,幾百上千個併發就可以掛你站了。
因此,既然有現實的問題,就可以有對應的解決方案。
V5框架此功能的出現,就是為了從基礎層面統一解決這些問題。
只有當.NET行業不在有慢網站的存在,整體提升檔次了,有良好的口碑,才會引進更多的BOSS選用 .NET,大伙所期待的.NET春天也就近了。
V5的目前解決的問題:
總體而言,要實現這個功能,核心要解決以下問題:
下麵我來將技術一點一點出賣:
5:AOP攔截問題:
首先,要實現這功能,就得全局攔截,掃蕩過源碼或用過V5的同學,聽說過框架本身就有AOP的吧;
其次,得改造這個AOP:框架預設有一個空AOP,當外部有AOP裝載的時候,會替換掉這個空AOP。
要實現這個自動緩存:本想在空AOP里實現,放著浪費,但若用戶自定義的Aop被裝載,又會被替換掉,走不通...
方案想了三四個,思考了三四夜,最後還是在擼碼時才確定了現在的模式(這個告訴我們,想的差不多了就該擼碼了,要100%想通再擼不太靠譜):
於是,我這樣做了:
原有的Aop,改名成InterAop,不過是掛名的,因為它沒有繼承IAOP介面,而且從原本的單例變更成多例模式。
這裡可以貼兩行代碼,意思是:在Bengin和End方法調用了外部AOP的介面,並根據外部AOP的狀態決定後續的執行流程:
完整的源碼你們自己SVN了:https://github.com/cyq1162/cyqdata.git
public AopResult Begin(AopEnum action) { AopResult ar = AopResult.Continue; if (outerAop != null) { ar = outerAop.Begin(action, Para); if (ar == AopResult.Return) { return ar; } } if (AppConfig.Cache.IsAutoCache && !IsTxtDataBase) // 只要不是直接返回 { isHasCache = AutoCache.GetCache(action, Para); //找看有沒有Cache } if (isHasCache) //找到Cache { if (outerAop == null || ar == AopResult.Default)//不執行End { return AopResult.Return; } return AopResult.Break;//外部Aop說:還需要執行End } else // 沒有Cache,預設返回 { return ar; } } public void End(AopEnum action) { if (outerAop != null) { outerAop.End(action, Para); } if (!isHasCache && !IsTxtDataBase) { AutoCache.SetCache(action, Para); //找看有沒有Cache } }
代碼最後很少,但沒想出來之前,2天都搞不定。
1:基礎單表、視圖操作
A:單表,這個是最簡單的,傳遞進來的就是表名;
B:視圖,這個麻煩一點,傳遞的是視圖名;
於是,如何從視圖獲取相關參與的表名?你現在應該不知道,我來告訴你吧:
DBDataReader sdr=.... DataTable dt = sdr.GetSchemaTable();
這條語句,可以通殺所有的資料庫,不用去N種資料庫里搜各種元數據藏在哪了!!!
2:多表SQL語句操作:
對於SQL語句,可以用上面的方法,執行一個DataReader再拿,但我弄了一個簡單的方法來找關聯表:
internal static List<string> GetTableNamesFromSql(string sql) { List<string> nameList = new List<string>(); //獲取原始表名 string[] items = sql.Split(' '); if (items.Length == 1) { return nameList; }//單表名 if (items.Length > 3) // 總是包含空格的select * from xxx { bool isKeywork = false; foreach (string item in items) { if (!string.IsNullOrEmpty(item)) { string lowerItem = item.ToLower(); switch (lowerItem) { case "from": case "update": case "into": case "join": case "table": isKeywork = true; break; default: if (isKeywork) { if (item[0] == '(' || item.IndexOf('.') > -1) { isKeywork = false; } else { isKeywork = false; nameList.Add(NotKeyword(item)); } } break; } } } } return nameList; }
有可能會找多,找到後,再過濾一下名稱是不是資料庫里的表就可以了。
3:直接操作資料庫
一開始設置的思維,是動態創建一個表,欄位大概是這樣的:
表名 更新時間
然後如果手工操作資料庫,可以手工更改時間,也可以用觸發器引發這裡的更新。
然後後臺線程定時掃這個表,就知道有沒有表被更新了。
不過--------V5目前並木有實現它,只是開放了一個介面,可以讓你在代碼里調用移除緩存。
這個方法就是:
public abstract partial class CacheManage { /// <summary> /// 獲取系統內部緩存Key /// </summary> public static string GetKey(CacheKeyType ckt, string tableName) { return GetKey(ckt, tableName, AppConfig.DB.DefaultDataBase, AppConfig.DB.DefaultDalType); } /// <summary> /// 獲取系統內部緩存Key /// </summary> public static string GetKey(CacheKeyType ckt, string tableName, string dbName, DalType dalType) { switch (ckt) { case CacheKeyType.Schema: return TableSchema.GetSchemaKey(tableName, dbName, dalType); case CacheKeyType.AutoCache: return AutoCache.GetBaseKey(dalType, dbName, tableName); } return string.Empty; } }
4:跨伺服器操作
這個本來是簡單的,後來又想麻煩了,因為要兼顧性能問題,緩存移除可能會頻繁的問題。
後來,通過增加了緩存類型,來識別本地緩存或分散式緩存,來區別寫代碼:
private static void SetBaseKey(string baseKey, string key) { //baseKey是表的,不包括視圖和自定義語句 if (_MemCache.CacheType == CacheType.LocalCache) { if (cacheKeys.ContainsKey(baseKey)) { cacheKeys[baseKey] = cacheKeys[baseKey].Append("," + key); } else { cacheKeys.Add(baseKey, new StringBuilder(key)); } } else { StringBuilder sb = _MemCache.Get<StringBuilder>(baseKey); if (sb == null) { _MemCache.Set(baseKey, new StringBuilder(key)); } else { sb.Append("," + key); _MemCache.Set(baseKey, sb); } } }
6:緩存失效問題
這個問題,流程本來很簡單的:
但思考到Cache多,而且分散式時,返回會卡,所以刪除Cache操作就變成線程處理了。
後來為了避免線程多開,又把類改成了單例(一開始是多實例的)
現在,又把這線程的線程開啟,放到LocalCache里和另一個線程作伴了,然後這個單例類又變更成了靜態類。
V5框架怎麼使用這功能:
升級版本到最新版即可!
總結:
1:沒有這個功能之前:框架解決了三大問題:編程架構的統一(自動化)、資料庫壓力(讀寫分離)、伺服器壓力(分散式緩存)。
2:此功能的存在:是針對從基礎層面提升行業項目的整體水平。
3:最近大腦有點發春,一個個創新Idea不斷的從我大腦冒出來,折騰的我好累:
要思考架構、落實框架代碼、要寫文分享,寫框架Demo、群里解答。
4:開源不賺錢,又投入這麼多精力,只能把它當理想了,希望它有天成為.NET項目的標配數據層。
5:我博客是有打贊插件的,哈。