在一個分佈式緩存遍地都是的環境下,還講本地緩存,感受有點out了啊!可能你們看到標題,就沒有想繼續看下去的慾望了吧。可是,本地緩存的重要性也是有的!html
本地緩存相比分佈式緩存確實是比較out和比較low,這個我也是贊成的。可是嘛,總有它存在的意義,存在即合理。git
先來看看下面的圖,它基本解釋了緩存最基本的使用。github
關於緩存的考慮是多方面,可是大部分狀況下的設計至少應該要有兩級纔算是比較合適的,一級是關於應用服務器的(本地緩存),一級是關於緩存服務器的。sql
因此上面的圖在應用服務器內還能夠進一步細化,從而獲得下面的一張圖:數據庫
這裏也就是本文要講述的重點了。緩存
注:本文涉及到的緩存沒有特別說明都是指的數據緩存!服務器
在介紹本身瞎折騰的方案以前,先來看一下目前用的比較多,也是比較常見的本地緩存有那些。網絡
在.NET Framework 時代,咱們最爲熟悉的本地緩存應該就是HttpRuntime.Cache和MemoryCache這兩個了吧。併發
一個依賴於System.Web,一個須要手動添加System.Runtime.Caching的引用。app
第一個很明顯不能在.NET Core 2.0的環境下使用,第二個貌似要在2.1纔會有,具體的不是很清楚。
在.NET Core時代,目前可能就是Microsoft.Extensions.Caching.Memory。
固然這裏是沒有說明涉及到其餘第三方的組件!如今應該也會有很多。
本文主要是基於SQLite作了一個本地緩存的實現,也就是我瞎折騰搞的。
爲何會考慮SQLite呢?主要是基於下面緣由:
爲何說是簡單的設計呢,由於本文的實現是比較簡單的,還有許多緩存應有的細節並無考慮進去,但應該也能夠知足大多數中小型應用的需求了。
先來創建存儲緩存數據的表。
CREATE TABLE "main"."caching" ( "cachekey" text NOT NULL, "cachevalue" text NOT NULL, "expiration" integer NOT NULL, PRIMARY KEY("cachekey") );
這裏只須要簡單的三個字段便可。
字段名 | 描述 |
---|---|
cachekey | 緩存的鍵 |
cachevalue | 緩存的值,序列化以後的字符串 |
expiration | 緩存的絕對過時時間 |
因爲SQLite的列並不能直接存儲完整的一個對象,須要將這個對象進行序列化以後 再進行存儲,因爲多了一些額外的操做,相比MemoryCache就消耗了多一點的時間,
好比如今有一個Product類(有id,name兩個字段)的實例obj,要存儲這個實例,須要先對其進行序列化,轉成一個JSON字符串後再進行存儲。固然在讀取的時候也就須要進行反序列化的操做才能夠。
爲了方便緩存的接入,統一了一下緩存的入口,便於後面的使用。
/// <summary> /// Cache entry. /// </summary> public class CacheEntry { /// <summary> /// Initializes a new instance of the <see cref="T:SQLiteCachingDemo.Caching.CacheEntry"/> class. /// </summary> /// <param name="cacheKey">Cache key.</param> /// <param name="cacheValue">Cache value.</param> /// <param name="absoluteExpirationRelativeToNow">Absolute expiration relative to now.</param> /// <param name="isRemoveExpiratedAfterSetNewCachingItem">If set to <c>true</c> is remove expirated after set new caching item.</param> public CacheEntry(string cacheKey, object cacheValue, TimeSpan absoluteExpirationRelativeToNow, bool isRemoveExpiratedAfterSetNewCachingItem = true) { if (string.IsNullOrWhiteSpace(cacheKey)) { throw new ArgumentNullException(nameof(cacheKey)); } if (cacheValue == null) { throw new ArgumentNullException(nameof(cacheValue)); } if (absoluteExpirationRelativeToNow <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException( nameof(AbsoluteExpirationRelativeToNow), absoluteExpirationRelativeToNow, "The relative expiration value must be positive."); } this.CacheKey = cacheKey; this.CacheValue = cacheValue; this.AbsoluteExpirationRelativeToNow = absoluteExpirationRelativeToNow; this.IsRemoveExpiratedAfterSetNewCachingItem = isRemoveExpiratedAfterSetNewCachingItem; } /// <summary> /// Gets the cache key. /// </summary> /// <value>The cache key.</value> public string CacheKey { get; private set; } /// <summary> /// Gets the cache value. /// </summary> /// <value>The cache value.</value> public object CacheValue { get; private set; } /// <summary> /// Gets the absolute expiration relative to now. /// </summary> /// <value>The absolute expiration relative to now.</value> public TimeSpan AbsoluteExpirationRelativeToNow { get; private set; } /// <summary> /// Gets a value indicating whether this <see cref="T:SQLiteCachingDemo.Caching.CacheEntry"/> is remove /// expirated after set new caching item. /// </summary> /// <value><c>true</c> if is remove expirated after set new caching item; otherwise, <c>false</c>.</value> public bool IsRemoveExpiratedAfterSetNewCachingItem { get; private set; } /// <summary> /// Gets the serialize cache value. /// </summary> /// <value>The serialize cache value.</value> public string SerializeCacheValue { get { if (this.CacheValue == null) { throw new ArgumentNullException(nameof(this.CacheValue)); } else { return JsonConvert.SerializeObject(this.CacheValue); } } } }
在緩存入口中,須要注意的是:
而後是緩存接口的設計,這個都是比較常見的一些作法。
/// <summary> /// Caching Interface. /// </summary> public interface ICaching { /// <summary> /// Sets the async. /// </summary> /// <returns>The async.</returns> /// <param name="cacheEntry">Cache entry.</param> Task SetAsync(CacheEntry cacheEntry); /// <summary> /// Gets the async. /// </summary> /// <returns>The async.</returns> /// <param name="cacheKey">Cache key.</param> Task<object> GetAsync(string cacheKey); /// <summary> /// Removes the async. /// </summary> /// <returns>The async.</returns> /// <param name="cacheKey">Cache key.</param> Task RemoveAsync(string cacheKey); /// <summary> /// Flushs all expiration async. /// </summary> /// <returns>The all expiration async.</returns> Task FlushAllExpirationAsync(); }
因爲都是數據庫的操做,避免沒必要要的資源浪費,就把接口都設計成異步的了。這裏只有增刪查的操做,沒有更新的操做。
最後就是如何實現的問題了。實現上藉助了Dapper來完成相應的數據庫操做,平時是Dapper混搭其餘ORM來用的。
想一想不弄那麼複雜,就只用Dapper來處理就OK了。
/// <summary> /// SQLite caching. /// </summary> public class SQLiteCaching : ICaching { /// <summary> /// The connection string of SQLite database. /// </summary> private readonly string connStr = $"Data Source ={Path.Combine(Directory.GetCurrentDirectory(), "localcaching.sqlite")}"; /// <summary> /// The tick to time stamp. /// </summary> private readonly int TickToTimeStamp = 10000000; /// <summary> /// Flush all expirated caching items. /// </summary> /// <returns></returns> public async Task FlushAllExpirationAsync() { using (var conn = new SqliteConnection(connStr)) { var sql = "DELETE FROM [caching] WHERE [expiration] < STRFTIME('%s','now')"; await conn.ExecuteAsync(sql); } } /// <summary> /// Get caching item by cache key. /// </summary> /// <returns></returns> /// <param name="cacheKey">Cache key.</param> public async Task<object> GetAsync(string cacheKey) { using (var conn = new SqliteConnection(connStr)) { var sql = @"SELECT [cachevalue] FROM [caching] WHERE [cachekey] = @cachekey AND [expiration] > STRFTIME('%s','now')"; var res = await conn.ExecuteScalarAsync(sql, new { cachekey = cacheKey }); // deserialize object . return res == null ? null : JsonConvert.DeserializeObject(res.ToString()); } } /// <summary> /// Remove caching item by cache key. /// </summary> /// <returns></returns> /// <param name="cacheKey">Cache key.</param> public async Task RemoveAsync(string cacheKey) { using (var conn = new SqliteConnection(connStr)) { var sql = "DELETE FROM [caching] WHERE [cachekey] = @cachekey"; await conn.ExecuteAsync(sql , new { cachekey = cacheKey }); } } /// <summary> /// Set caching item. /// </summary> /// <returns></returns> /// <param name="cacheEntry">Cache entry.</param> public async Task SetAsync(CacheEntry cacheEntry) { using (var conn = new SqliteConnection(connStr)) { //1. Delete the old caching item at first . var deleteSql = "DELETE FROM [caching] WHERE [cachekey] = @cachekey"; await conn.ExecuteAsync(deleteSql, new { cachekey = cacheEntry.CacheKey }); //2. Insert a new caching item with specify cache key. var insertSql = @"INSERT INTO [caching](cachekey,cachevalue,expiration) VALUES(@cachekey,@cachevalue,@expiration)"; await conn.ExecuteAsync(insertSql, new { cachekey = cacheEntry.CacheKey, cachevalue = cacheEntry.SerializeCacheValue, expiration = await GetCurrentUnixTimestamp(cacheEntry.AbsoluteExpirationRelativeToNow) }); } if(cacheEntry.IsRemoveExpiratedAfterSetNewCachingItem) { // remove all expirated caching item when new caching item was set . await FlushAllExpirationAsync(); } } /// <summary> /// Get the current unix timestamp. /// </summary> /// <returns>The current unix timestamp.</returns> /// <param name="absoluteExpiration">Absolute expiration.</param> private async Task<long> GetCurrentUnixTimestamp(TimeSpan absoluteExpiration) { using (var conn = new SqliteConnection(connStr)) { var sql = "SELECT STRFTIME('%s','now')"; var res = await conn.ExecuteScalarAsync(sql); //get current utc timestamp and plus absolute expiration return long.Parse(res.ToString()) + (absoluteExpiration.Ticks / TickToTimeStamp); } } }
這裏須要注意下面幾個:
最後的話,天然就是如何使用的問題了。
首先是在IServiceCollection中註冊一下
service.AddSingleton<ICaching,SQLiteCaching>();
而後在控制器的構造函數中進行注入。
private readonly ICaching _caching; public HomeController(ICaching caching) { this._caching = caching; }
插入緩存時,須要先實例化一個CacheEntry對象,根據這個對象來進行相應的處理。
var obj = new Product() { Id = "123" , Name = "Product123" }; var cacheEntry = new CacheEntry("mykey", obj, TimeSpan.FromSeconds(3600)); await _caching.SetAsync(cacheEntry);
從緩存中讀取數據時,建議是用dynamic去接收,由於當時沒有考慮泛型的處理。
dynamic product = await _caching.GetAsync("mykey"); var id = product.Id; var name = product.Name;
從緩存中移除緩存項的兩個操做以下所示。
//移除指定鍵的緩存項 await _caching.RemoveAsync("mykey"); //移除全部過時的緩存項 await _caching.FlushAllExpirationAsync();
通過在Mac book Pro上簡單的測試,從幾十萬數據中並行讀取1000條到10000條記錄也均可以在零點幾ms中完成。
這個在高讀寫比的系統中應該是比較有優點的。
可是並行的插入就相對要慢很多了,並行的插入一萬條記錄,直接就數據庫死鎖了。1000條還勉強能在20000ms搞定!
這個是由SQLite自己所支持的併發性致使的,另外插入緩存數據時都會開一個數據庫的鏈接,這也是比較耗時的,因此這裏能夠考慮作一下後續的優化。
移除全部過時的緩存項能夠在一兩百ms內搞定。
固然,還應該在不一樣的機器上進行更多的模擬測試,這樣獲得的效果比較真實可信。
SQLite作本地緩存有它本身的優點,也有它的劣勢。
優點:
劣勢:
雖然說併發高的時候能夠會有問題,可是在進入應用服務器的前已是通過一層負載均衡的分流了,因此這裏理論上對中小型應用影響不會太大。
另外對於緩存的滑動過時時間,文中並無實現,能夠在這個基礎上進行補充修改,從而使其能支持滑動過時。
本文示例Demo