1.緩存概念 1.什麼是緩存 這裡要講到的緩存是服務端緩存,簡單的說,緩存就是將一些實時性不高,但訪問又十分頻繁,或者說要很長時間才能取到的數據給存在記憶體當中,當有請求時直接返回,不用經過資料庫或介面獲取。這樣就可以減輕資料庫的負擔。 2.為什麼要用緩存 總的來說就是為了提高響應速度(用戶體驗度), ...
1.緩存概念
1.什麼是緩存
這裡要講到的緩存是服務端緩存,簡單的說,緩存就是將一些實時性不高,但訪問又十分頻繁,或者說要很長時間才能取到的數據給存在記憶體當中,當有請求時直接返回,不用經過資料庫或介面獲取。這樣就可以減輕資料庫的負擔。
2.為什麼要用緩存
總的來說就是為了提高響應速度(用戶體驗度),減少資料庫訪問頻率。
在一個用戶看來,軟體使用的體驗度才是關鍵,在對實時性要求不高的情況下,用戶肯定會覺得打開界面的響應速度快,能保證平常工作的應用才是好的。因此為了滿足這個需求,通過使用緩存,就可以保證滿足在正常工作的前提下響應時間儘可能短。
例如:當客戶端向伺服器請求某個數據時,伺服器先在緩存中找,如果在緩存中,就直接返回,無需查詢資料庫;如果請求的數據不在緩存中,這時再去資料庫中找,找到後返回給客戶端,並將這個資源加入緩存中。這樣下次請求相同資源時,就不需
要連接資料庫了。而且如果把緩存放在記憶體中,因為對記憶體的操作要比對資料庫操作快得多,這樣請求時間也會縮短。每當數據發生變化的時候(比如,數據有被修改,或被刪除的情況下),要同步的更新緩存信息,確保用戶不會在緩存取到舊的數據。
如果沒有使用緩存,用戶去請求某個數據,當用戶量和數據逐漸增加的時候,就會發現每次用戶請求的時間越來越長,且資料庫無時不刻都在工作。這樣用戶和資料庫都很痛苦,時間一長,就有可能發生下以下事情:
1.用戶常抱怨應用打開速度太慢,頁面經常無響應,偶爾還會出現崩潰的情況。
2.資料庫連接數滿或者說資料庫響應慢(處理不過來)。
3.當併發量上來的時候,可能會導致資料庫崩潰,使得應用無法正常使用。
2.選用Redis還是Memcached
簡單說下這兩者的區別,兩者都是通過key-value的方式進行存儲的,Memcached只有簡單的字元串格式,而Redis還支持更多的格式(list、 set、sorted set、hash table ),緩存時使用到的數據都是在記憶體當中,
不同的在於Redis支持持久化,集群、簡單事務、發佈/訂閱、主從同步等功能,當斷電或軟體重啟時,Memcached中的數據就已經不存在了,而Redis可以通過讀取磁碟中的數據再次使用。
這裡提高Windows版的安裝包:傳送門 可視化工具:因文件太大無法上傳到博客園。代碼倉庫中有,需要的私信哦~
3.兩者在NetCore 中的使用
Memcached的使用還是相當簡單的,首先在 Startup 類中做以下更改,添加緩存參數 賦值給外部類來方便使用
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IMemoryCache memoryCache) { //.... 省略部分代碼 DemoWeb.MemoryCache = memoryCache; //.... 省略部分代碼 }
DemoWeb中的代碼:
public class DemoWeb { //....省略部分代碼 /// <summary> /// MemoryCache /// </summary> public static IMemoryCache MemoryCache { get; set; } /// <summary> /// 獲取當前請求客戶端IP /// </summary> /// <returns></returns> public static string GetClientIp() { var ip = HttpContext.Request.Headers["X-Forwarded-For"].FirstOrDefault()?.Split(',')[0].Trim(); if (string.IsNullOrEmpty(ip)) { ip = HttpContext.Connection.RemoteIpAddress.ToString(); } return ip; } }
然後創建 MemoryCache 來封裝些緩存的簡單方法
/// <summary> /// MemoryCache緩存 /// </summary> public class MemoryCache { private static readonly HashSet<string> Keys = new HashSet<string>(); /// <summary> /// 緩存首碼 /// </summary> public string Prefix { get; } /// <summary> /// 構造函數 /// </summary> /// <param name="prefix"></param> public MemoryCache(string prefix) { Prefix = prefix + "_"; } /// <summary> /// 獲取 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public T Get<T>(string key) { return DemoWeb.MemoryCache.Get<T>(Prefix + key); } /// <summary> /// 設置 無過期時間 /// </summary> /// <param name="key"></param> /// <param name="data"></param> public void Set(string key, object data) { key = Prefix + key; DemoWeb.MemoryCache.Set(key, data); if (!Keys.Contains(key)) { Keys.Add(key); } } /// <summary> /// 設置 /// </summary> /// <param name="key"></param> /// <param name="data"></param> /// <param name="absoluteExpiration"></param> public void Set(string key, object data, DateTimeOffset absoluteExpiration) { key = Prefix + key; DemoWeb.MemoryCache.Set(key, data, absoluteExpiration); if (!Keys.Contains(key)) { Keys.Add(key); } } /// <summary> /// 設置 /// </summary> /// <param name="key"></param> /// <param name="data"></param> /// <param name="absoluteExpirationRelativeToNow"></param> public void Set(string key, object data, TimeSpan absoluteExpirationRelativeToNow) { key = Prefix + key; DemoWeb.MemoryCache.Set(key, data, absoluteExpirationRelativeToNow); if (!Keys.Contains(key)) { Keys.Add(key); } } /// <summary> /// 設置 /// </summary> /// <param name="key"></param> /// <param name="data"></param> /// <param name="expirationToken"></param> public void Set(string key, object data, IChangeToken expirationToken) { key = Prefix + key; DemoWeb.MemoryCache.Set(key, data, expirationToken); if (!Keys.Contains(key)) { Keys.Add(key); } } /// <summary> /// 設置 /// </summary> /// <param name="key"></param> /// <param name="data"></param> /// <param name="options"></param> public void Set(string key, object data, MemoryCacheEntryOptions options) { key = Prefix + key; DemoWeb.MemoryCache.Set(key, data, options); if (!Keys.Contains(key)) { Keys.Add(key); } } /// <summary> /// 移除某個 /// </summary> /// <param name="key"></param> public void Remove(string key) { key = Prefix + key; DemoWeb.MemoryCache.Remove(key); if (Keys.Contains(key)) { Keys.Remove(key); } } /// <summary> /// 清空所有 /// </summary> public void ClearAll() { foreach (var key in Keys) { DemoWeb.MemoryCache.Remove(key); } Keys.Clear(); } }View Code
其實接下來就可以直接使用緩存了,但為了方便使用,再建一個緩存類別的中間類來管理。
public class UserCache { private static readonly MemoryCache Cache = new MemoryCache("User"); private static TimeSpan _timeout = TimeSpan.Zero; private static TimeSpan Timeout { get { if (_timeout != TimeSpan.Zero) return _timeout; try { _timeout = TimeSpan.FromMinutes(20); return _timeout; } catch (Exception) { return TimeSpan.FromMinutes(10); } } } public static void Set(string key,string cache) { if (string.IsNullOrEmpty(cache)) return; Cache.Set(key, cache, Timeout); } public static string Get(string key) { if (string.IsNullOrEmpty(key)) return default(string); return Cache.Get<string>(key); } }UserCache
測試是否可以正常使用:代碼與截圖
[HttpGet] [Route("mecache")] public ActionResult ValidToken() { var key = "tkey"; UserCache.Set(key, "測試數據"); return Succeed(UserCache.Get(key)); }
可以清楚的看到 MemoryCache 可以正常使用。
那麼接下來將講到如何使用 Redis 緩存。先在需要封裝基礎類的項目 Nuget 包中添加 StackExchange.Redis 依賴。然後添加Redis 連接類
internal class RedisConnectionFactory { public string ConnectionString { get; set; } public string Password { get; set; } public ConnectionMultiplexer CurrentConnectionMultiplexer { get; set; } /// <summary> /// 設置連接字元串 /// </summary> /// <returns></returns> public void SetConnectionString(string connectionString) { ConnectionString = connectionString; } /// <summary> /// 設置連接字元串 /// </summary> /// <returns></returns> public void SetPassword(string password) { Password = password; } public ConnectionMultiplexer GetConnectionMultiplexer() { if (CurrentConnectionMultiplexer == null || !CurrentConnectionMultiplexer.IsConnected) { if (CurrentConnectionMultiplexer != null) { CurrentConnectionMultiplexer.Dispose(); } CurrentConnectionMultiplexer = GetConnectionMultiplexer(ConnectionString); } return CurrentConnectionMultiplexer; } private ConnectionMultiplexer GetConnectionMultiplexer(string connectionString) { ConnectionMultiplexer connectionMultiplexer; if (!string.IsNullOrWhiteSpace(Password) && !connectionString.ToLower().Contains("password")) { connectionString += $",password={Password}"; } var redisConfiguration = ConfigurationOptions.Parse(connectionString); redisConfiguration.AbortOnConnectFail = true; redisConfiguration.AllowAdmin = false; redisConfiguration.ConnectRetry = 5; redisConfiguration.ConnectTimeout = 3000; redisConfiguration.DefaultDatabase = 0; redisConfiguration.KeepAlive = 20; redisConfiguration.SyncTimeout = 30 * 1000; redisConfiguration.Ssl = false; connectionMultiplexer = ConnectionMultiplexer.Connect(redisConfiguration); return connectionMultiplexer; } }RedisConnectionFactory
再添加Redis客戶端類
/// <summary> /// Redis Client /// </summary> public class RedisClient : IDisposable { public int DefaultDatabase { get; set; } = 0; private readonly ConnectionMultiplexer _client; private IDatabase _db; public RedisClient(ConnectionMultiplexer client) { _client = client; UseDatabase(); } public void UseDatabase(int db = -1) { if (db == -1) db = DefaultDatabase; _db = _client.GetDatabase(db); } public string StringGet(string key) { return _db.StringGet(key).ToString(); } public void StringSet(string key, string data) { _db.StringSet(key, data); } public void StringSet(string key, string data, TimeSpan timeout) { _db.StringSet(key, data, timeout); } public T Get<T>(string key) { var json = StringGet(key); if (string.IsNullOrEmpty(json)) { return default(T); } return json.ToNetType<T>(); } public void Set(string key, object data) { var json = data.ToJson(); _db.StringSet(key, json); } public void Set(string key, object data, TimeSpan timeout) { var json = data.ToJson(); _db.StringSet(key, json, timeout); } /// <summary> /// Exist /// </summary> /// <param name="key"></param> /// <returns></returns> public bool Exist(string key) { return _db.KeyExists(key); } /// <summary> /// Delete /// </summary> /// <param name="key"></param> /// <returns></returns> public bool Delete(string key) { return _db.KeyDelete(key); } /// <summary> /// Set Expire to Key /// </summary> /// <param name="key"></param> /// <param name="expiry"></param> /// <returns></returns> public bool Expire(string key, TimeSpan? expiry) { return _db.KeyExpire(key, expiry); } /// <summary> /// 計數器 如果不存在則設置值,如果存在則添加值 如果key存在且類型不為long 則會異常 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="expiry">只有第一次設置有效期生效</param> /// <returns></returns> public long SetStringIncr(string key, long value = 1, TimeSpan? expiry = null) { var nubmer = _db.StringIncrement(key, value); if (nubmer == 1 && expiry != null)//只有第一次設置有效期(防止覆蓋) _db.KeyExpireAsync(key, expiry);//設置有效期 return nubmer; } /// <summary> /// 讀取計數器 /// </summary> /// <param name="key"></param> /// <returns></returns> public long GetStringIncr(string key) { var value = StringGet(key); return string.IsNullOrWhiteSpace(value) ? 0 : long.Parse(value); } /// <summary> /// 計數器-減少 如果不存在則設置值,如果存在則減少值 如果key存在且類型不為long 則會異常 /// </summary> /// <param name="key"></param> /// <returns></returns> public long StringDecrement(string key, long value = 1) { var nubmer = _db.StringDecrement(key, value); return nubmer; } public void Dispose() { _client?.Dispose(); } }RedisClient
然後再添加Redis連接生成工具類
public static class RedisFactory { private static readonly object Locker = new object(); private static RedisConnectionFactory factory; private static void InitRedisConnection() { try { factory = new RedisConnectionFactory(); var connectionString = DemoWeb.Configuration["Redis:ConnectionString"]; #if DEBUG connectionString = "127.0.0.1:6379"; #endif factory.ConnectionString = connectionString; factory.Password = DemoWeb.Configuration["Redis:Pwd"]; } catch (Exception e) { LogHelper.Logger.Fatal(e, "Redis連接創建失敗。"); } } public static RedisClient GetClient() { //先判斷一輪,減少鎖,提高效率 if (factory == null || string.IsNullOrEmpty(factory.ConnectionString)) { //防止併發創建 lock (Locker) { InitRedisConnection(); } } return new RedisClient(factory.GetConnectionMultiplexer()) { DefaultDatabase = DemoWeb.Configuration["Redis:DefaultDatabase"].ToInt() }; } }RedisFactory
這裡要使用到前面的靜態擴展方法。請自行添加 傳送門 ,還需要將 Startup 類中的 Configuration 給賦值到 DemoWeb中的 Configuration 欄位值來使用。
在配置文件 appsettings.json 中添加
"Redis": { "ConnectionString": "127.0.0.1:6379", "Pwd": "", "DefaultDatabase": 0 }
再添加Redis緩存使用類
/// <summary> /// Redis緩存 /// </summary> public class RedisCache { private static RedisClient _client; private static RedisClient Client => _client ?? (_client = RedisFactory.GetClient()); private static string ToKey(string key) { return $"Cache_redis_{key}"; } /// <summary> /// 獲取 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <returns></returns> public static T Get<T>(string key) { try { var redisKey = ToKey(key); return Client.Get<T>(redisKey); } catch (Exception e) { LogHelper.Logger.Fatal(e, "RedisCache.Get \n key:{0}", key); return default(T); } } /// <summary> /// 嘗試獲取 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="result"></param> /// <returns></returns> private static T TryGet<T>(string key, out bool result) { result = true; try { var redisKey = ToKey(key); return Client.Get<T>(redisKey); } catch (Exception e) { LogHelper.Logger.Fatal(e, "RedisCache.TryGet \n key:{0}", key); result = false; return default(T); } } /// <summary> /// 獲取 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="setFunc"></param> /// <param name="expiry"></param> /// <param name="resolver"></param> /// <returns></returns> public static T Get<T>(string key, Func<T> setFunc, TimeSpan? expiry = null) { var redisKey = ToKey(key); var result = TryGet<T>(redisKey, out var success); if (success && result == null) { result = setFunc(); try { Set(redisKey, result, expiry); } catch (Exception e) { LogHelper.Logger.Fatal(e, "RedisCache.Get<T> \n key:{0}", key); } } return result; } /// <summary> /// 設置 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="key"></param> /// <param name="value"></param> /// <param name="expiry"></param> /// <returns></returns> public static bool Set<T>(string key, T value, TimeSpan? expiry = null) { var allRedisKey = ToKey("||Keys||"); var redisKey = ToKey(key); var allkeyRedisValue = Client.StringGet(allRedisKey); var keys = allkeyRedisValue.ToNetType<List<string>>() ?? new List<string>(); if (!keys.Contains(redisKey)) { keys.Add(redisKey); Client.Set(allRedisKey, keys); } if (expiry.HasValue) { Client.StringSet(redisKey, value.ToJson(), expiry.Value); } else { Client.StringSet(redisKey, value.ToJson()); } return true; } /// <summary> /// 重新設置過期時間 /// </summary> /// <param name="key"></param> /// <param name="expiry"></param> public static void ResetItemTimeout(string key, TimeSpan expiry) { var redisKey = ToKey(key); Client.Expire(redisKey, expiry); } /// <summary> /// Exist /// </summary> /// <param name="key">原始key</param> /// <returns></returns> public static bool Exist(string key) { var redisKey = ToKey(key); return Client.Exist(redisKey); } /// <summary> /// 計數器 增加 能設置過期時間的都設置過期時間 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="expiry"></pa