1. 程式人生 > >從零開始搭建前後端分離的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的專案框架之八MemoryCache與redis快取的使用

從零開始搭建前後端分離的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的專案框架之八MemoryCache與redis快取的使用

 

 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"></param>
        /// <returns></returns>
        public static bool SetStringIncr(string key, long value = 1, TimeSpan? expiry = null, bool needRest0 = false)
        {
            var redisKey = ToKey(key);
            try
            {
                if (expiry.HasValue)
                {
                    if (Exist(key) && needRest0)
                    {
                        var exitValue = GetStringIncr(key);
                        Client.SetStringIncr(redisKey, value - exitValue, expiry.Value);
                    }
                    else
                    {
                        Client.SetStringIncr(redisKey, value, expiry.Value);
                    }
                }
                else
                {
                    if (Exist(key) && needRest0)
                    {
                        var exitValue = GetStringIncr(key);
                        Client.SetStringIncr(redisKey, value - exitValue);
                    }
                    else
                    {
                        Client.SetStringIncr(redisKey, value);
                    }
                }
            }
            catch (Exception e)
            {
                LogHelper.Logger.Fatal($"計數器-增加錯誤,原因:{e.Message}");
                return false;
            }
            return true;
        }

        /// <summary>
        /// 讀取計數器
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public static long GetStringIncr(string key)
        {
            var redisKey = ToKey(key);
            return Client.GetStringIncr(redisKey);
        }

        /// <summary>
        /// 計數器 - 減少
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public static bool StringDecrement(string key, long value = 1)
        {
            var redisKey = ToKey(key);
            try
            {
                Client.StringDecrement(redisKey, value);
                return true;
            }
            catch (Exception e)
            {
                LogHelper.Logger.Fatal($"計數器-減少錯誤,原因:{e.Message}");
                return false;
            }
        }

        /// <summary>
        /// 刪除
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public static bool Delete(string key)
        {
            var redisKey = ToKey(key);
            return Client.Delete(redisKey);
        }

        /// <summary>
        /// 清空
        /// </summary>
        public static void Clear()
        {
            //因為codis不支援keys之類的命令,所以只能自己記錄下來,然後通過這個來清理。
            var redisKey = ToKey("||Keys||");

            var keys = Client.Get<List<string>>(redisKey);
            var notExists = new List<string>();
            foreach (var key in keys)
            {
                if (Client.Exist(key))
                    Client.Delete(key);
                else
                    notExists.Add(key);
            }
            if (notExists.Count > 0)
            {
                keys.RemoveAll(s => notExists.Contains(s));
                Client.Set(redisKey, keys);
            }
        }
    }
RedisCache

  到這來基本就快可以拿來測試是否可以用了。但是前提是得把 Redis 給執行起來。

  將上面的 Redis安裝包安裝,並啟動所安裝資料夾中的 redis-server.exe 程式,若出現閃退情況,就執行 redis-cli.exe 程式,然後輸入 shutdown 按下回車,重新執行 redis-server.exe 程式,就會出現這個介面。

  到這來,新增一個測試方法來看看效果。藉助Redis視覺化工具檢視結果如下

  測試完美成功,由於時間問題,上面 RedisCache只有字串的方法,沒有加其它型別的方法。有需要的自己加咯~

 

  在下一篇中將介紹如何在NetCore中如何使用 過濾器來進行許可權驗證

 

  有需要原始碼的在下方評論或私信~給我的SVN訪客賬戶密碼下載,程式碼未放在GitHub上。svn中新加上了Redis安裝包及視覺化工