一起聊聊使用redis實現分散式快取

2022-07-14 18:01:23
本篇文章給大家帶來了關於的相關知識,其中主要整理了分散式快取的相關問題,分散式就是有多個應用程式組成,可能分佈在不同的伺服器上,最終都是在為web端提供服務,下面一起來看一下,希望對大家有幫助。

推薦學習:

分散式快取描述:

分散式快取重點是在分散式上,相信大家接觸過的分散式有很多中,像分散式開發,分散式部署,分散式鎖、事物、系統 等有很多。使我們對分散式本身就有一個很明確的認識,分散式就是有多個應用程式組成,可能分佈在不同的伺服器上,最終都是在為web端提供服務。
分散式快取有以下幾點優點:

  1. 所有的Web伺服器上的快取資料都是相同的,不會因為應用程式不同,伺服器的不同導致快取資料的不一樣。
  2. 快取的是獨立的不受Web伺服器的重新啟動或被刪除新增的影響,也就是說這些Web的改變不到導致快取資料的改變。

傳統的單體應用架構因為使用者的存取量的不高,快取的存在大多數都是儲存使用者的資訊,以及一些頁面,大多數的操作都是直接和DB進行讀寫互動,這種架構簡單,也稱為簡單架構,
傳統的OA專案比如ERP,SCM,CRM等系統因為使用者量不大也是因為大多數公司業務的原因,單體應用架構還是很常用的架構,但是有些系統隨著使用者量的增加,業務的擴張擴充套件,導致DB的瓶頸的出現。

以下我所瞭解到的關於這種情況的處理有以下兩種

(1):當使用者存取量不大,但是讀寫的資料量很大的時候,我們一般採取的是,對DB進行讀寫分離、一主多從、對硬體進行升級的方式來解決DB瓶頸的問題。
這樣的缺點也同樣純在:

1、使用者量大的時候怎麼辦?,
2、對於效能的提升有限,
3、價效比不高。提升一點效能就需要花費很多代價,(打個比方,現在的I/O吞吐量是0.9的需要提升到1.0,我們在增加機器設定的情況下這個價格確實很可觀的)

(2):當使用者存取量也增加的時候,我們就需要引入快取了來解決了,一張圖描述快取的大致的作用。

快取主要針對的是不經常發生改變的並且存取量很大的資料,DB資料庫可以理解為只作為資料固化的或者只用來讀取經常發生改變的資料,上圖中我沒有畫SET的操作,就是想特意說明一下,快取的存在可以作為一個臨時的資料庫,我們可以通過定時的任務的方式去同步快取和資料庫中的資料,這樣做的好處是可以轉移資料庫的壓力到快取中。

快取的出現解決了資料庫壓力的問題,但是當以下情況發生的時候,快取就不在起到作用了,快取穿透、快取擊穿、快取雪崩這三種情況

快取穿透:我們的程式中用快取的時候一般採取的是先去快取中查詢我們想要的快取資料,如果快取中不存在我們想要的資料的話,快取就失去了做用(快取失效)我們就是需要伸手向DB庫去要資料,這個時候這種動作過多資料庫就崩潰了,這種情況需要我們去預防了。比如說:我們向快取獲取一個使用者資訊,但是故意去輸入一個快取中不存在的使用者資訊,這樣就避過了快取,把壓力重新轉移到資料上面了。對於這種問題我們可以採取,把第一次存取的資料進行快取,因為快取查不到使用者資訊,資料庫也查詢不到使用者資訊,這個時候避免重複的存取我們把這個請求快取起來,把壓力重新轉向快取中,有人會有疑問了,當存取的引數有上萬個都是不重複的引數並且都是可以躲避快取的怎麼辦,我們同樣把資料存起來設定一個較短過期時間清除快取。

快取擊穿:事情是這樣的,對於一些設定了過期時間的快取KEY,在過期的時候,程式被高並行的存取了(快取失效),這個時候使用互斥鎖來解決問題,

互斥鎖原理:通俗的描述就是,一萬個使用者存取了,但是隻有一個使用者可以拿到存取資料庫的許可權,當這個使用者拿到這個許可權之後重新建立快取,這個時候剩下的存取者因為沒有拿到許可權,就原地等待著去存取快取。

永不過期:有人就會想了,我不設定過期時間不就行了嗎?可以,但是這樣做也是有缺點的,我們需要定期的取更新快取,這個時候快取中的資料比較延遲。

快取雪崩:是指多種快取設定了同一時間過期,這個時候大批次的資料存取來了,(快取失效)資料庫DB的壓力又上來了。解決方法在設定過期時間的時候在過期時間的基礎上增加一個亂數儘可能的保證快取不會大面積的同事失效。

專案准備

1、首先安裝Redis,可以參考這裡
2、然後下載安裝:使用者端工具:RedisDesktopManager(方便管理)
3、在我們的專案Nuget中 參照 Microsoft.Extensions.Caching.Redis

為此我們新建一個ASP.NET Core MVC專案,在專案Startup類的ConfigureServices方法中先註冊Redis服務:

public void ConfigureServices(IServiceCollection services)
{
    //將Redis分散式快取服務新增到服務中
    services.AddDistributedRedisCache(options =>
    {
        //用於連線Redis的設定  Configuration.GetConnectionString("RedisConnectionString")讀取設定資訊的串
        options.Configuration = "localhost";// Configuration.GetConnectionString("RedisConnectionString");
        //Redis範例名DemoInstance
        options.InstanceName = "DemoInstance";
    });
    services.AddMvc();
}

也可以在上面註冊Redis服務的時候,指定Redis伺服器的IP地址、埠號和登入密碼:

public void ConfigureServices(IServiceCollection services)
{
    //將Redis分散式快取服務新增到服務中
    services.AddDistributedRedisCache(options =>
    {
        //用於連線Redis的設定  Configuration.GetConnectionString("RedisConnectionString")讀取設定資訊的串
        options.Configuration = "192.168.1.105:6380,password=1qaz@WSX3edc$RFV";//指定Redis伺服器的IP地址、埠號和登入密碼
        //Redis範例名DemoInstance
        options.InstanceName = "DemoInstance";
    });
    services.AddMvc();
}

後面我們會解釋上面options.InstanceName設定的Redis範例名DemoInstance是用來做什麼的

此外還可以在services.AddDistributedRedisCache方法中指定Redis伺服器的超時時間,如果呼叫後面介紹的IDistributedCache介面中的方法,對Redis伺服器進行的操作超時了,會丟擲RedisConnectionException和RedisTimeoutException異常,所以下面我們在註冊Redis服務的時候,指定了三個超時時間:

public void ConfigureServices(IServiceCollection services)
{
    //將Redis分散式快取服務新增到服務中
    services.AddDistributedRedisCache(options =>
    {
        options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions()
        {
            Password = "1qaz@WSX3edc$RFV",
            ConnectTimeout = 5000,//設定建立連線到Redis伺服器的超時時間為5000毫秒
            SyncTimeout = 5000,//設定對Redis伺服器進行同步操作的超時時間為5000毫秒
            ResponseTimeout = 5000//設定對Redis伺服器進行操作的響應超時時間為5000毫秒
        };

        options.ConfigurationOptions.EndPoints.Add("192.168.1.105:6380");
        options.InstanceName = "DemoInstance";
    });
    services.AddMvc();
}

其中ConnectTimeout是建立連線到Redis伺服器的超時時間,而SyncTimeout和ResponseTimeout是對Redis伺服器進行資料操作的超時時間。注意上面我們使用了options.ConfigurationOptions屬性來設定Redis伺服器的IP地址、埠號和登入密碼

IDistributedCache 介面

在專案中參照:using Microsoft.Extensions.Caching.Distributed; 使用IDistributedCache

IDistributedCache介面包含同步和非同步方法。 介面允許在分散式快取實現中新增、檢索和刪除項。 IDistributedCache介面包含以下方法:
Get、 GetAsync
採用字串鍵並以byte[]形式檢索快取項(如果在快取中找到)。
Set、SetAsync
使用字串鍵向快取新增或更改項(byte[]形式)。
Refresh、RefreshAsync
根據鍵重新整理快取中的項,並重置其可調過期超時值(如果有)。
Remove、RemoveAsync
根據鍵刪除快取項。如果傳入Remove方法的鍵在Redis中不存在,Remove方法不會報錯,只是什麼都不會發生而已,但是如果傳入Remove方法的引數為null,則會丟擲異常。

如上所述,由於IDistributedCache介面的Set和Get方法,是通過byte[]位元組陣列來向Redis存取資料的,所以從某種意義上來說不是很方便,下面我封裝了一個RedisCache類,可以向Redis中存取任何型別的資料。

其中用到了Json.NET Nuget包,來做Json格式的序列化和反序列化:

using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using System.Text;

namespace AspNetCoreRedis.Assembly
{
    /// <summary>
    /// RedisCache快取操作類
    /// </summary>
    public class RedisCache
    {
        protected IDistributedCache cache;

        /// <summary>
        /// 通過IDistributedCache來構造RedisCache快取操作類
        /// </summary>
        /// <param name="cache">IDistributedCache物件</param>
        public RedisCache(IDistributedCache cache)
        {
            this.cache = cache;
        }

        /// <summary>
        /// 新增或更改Redis的鍵值,並設定快取的過期策略
        /// </summary>
        /// <param name="key">快取鍵</param>
        /// <param name="value">快取值</param>
        /// <param name="distributedCacheEntryOptions">設定Redis快取的過期策略,可以用其設定快取的絕對過期時間(AbsoluteExpiration或AbsoluteExpirationRelativeToNow),也可以設定快取的滑動過期時間(SlidingExpiration)</param>
        public void Set(string key, object value, DistributedCacheEntryOptions distributedCacheEntryOptions)
        {
            //通過Json.NET序列化快取物件為Json字串
            //呼叫JsonConvert.SerializeObject方法時,設定ReferenceLoopHandling屬性為ReferenceLoopHandling.Ignore,來避免Json.NET序列化物件時,因為物件的迴圈參照而丟擲異常
            //設定TypeNameHandling屬性為TypeNameHandling.All,這樣Json.NET序列化物件後的Json字串中,會包含序列化的型別,這樣可以保證Json.NET在反序列化物件時,去讀取Json字串中的序列化型別,從而得到和序列化時相同的物件型別
            var stringObject = JsonConvert.SerializeObject(value, new JsonSerializerSettings()
            {
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                TypeNameHandling = TypeNameHandling.All
            });

            var bytesObject = Encoding.UTF8.GetBytes(stringObject);//將Json字串通過UTF-8編碼,序列化為位元組陣列

            cache.Set(key, bytesObject, distributedCacheEntryOptions);//將位元組陣列存入Redis
            Refresh(key);//重新整理Redis
        }

        /// <summary>
        /// 查詢鍵值是否在Redis中存在
        /// </summary>
        /// <param name="key">快取鍵</param>
        /// <returns>true:存在,false:不存在</returns>
        public bool Exist(string key)
        {
            var bytesObject = cache.Get(key);//從Redis中獲取鍵值key的位元組陣列,如果沒獲取到,那麼會返回null

            if (bytesObject == null)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// 從Redis中獲取鍵值
        /// </summary>
        /// <typeparam name="T">快取的型別</typeparam>
        /// <param name="key">快取鍵</param>
        /// <param name="isExisted">是否獲取到鍵值,true:獲取到了,false:鍵值不存在</param>
        /// <returns>快取的物件</returns>
        public T Get<T>(string key, out bool isExisted)
        {
            var bytesObject = cache.Get(key);//從Redis中獲取鍵值key的位元組陣列,如果沒獲取到,那麼會返回null

            if (bytesObject == null)
            {
                isExisted = false;
                return default(T);
            }

            var stringObject = Encoding.UTF8.GetString(bytesObject);//通過UTF-8編碼,將位元組陣列反序列化為Json字串

            isExisted = true;

            //通過Json.NET反序列化Json字串為物件
            //呼叫JsonConvert.DeserializeObject方法時,也設定TypeNameHandling屬性為TypeNameHandling.All,這樣可以保證Json.NET在反序列化物件時,去讀取Json字串中的序列化型別,從而得到和序列化時相同的物件型別
            return JsonConvert.DeserializeObject<T>(stringObject, new JsonSerializerSettings()
            {
                TypeNameHandling = TypeNameHandling.All
            });
        }

        /// <summary>
        /// 從Redis中刪除鍵值,如果鍵值在Redis中不存在,該方法不會報錯,只是什麼都不會發生
        /// </summary>
        /// <param name="key">快取鍵</param>
        public void Remove(string key)
        {
            cache.Remove(key);//如果鍵值在Redis中不存在,IDistributedCache.Remove方法不會報錯,但是如果傳入的引數key為null,則會丟擲異常
        }

        /// <summary>
        /// 從Redis中重新整理鍵值
        /// </summary>
        /// <param name="key">快取鍵</param>
        public void Refresh(string key)
        {
            cache.Refresh(key);
        }
    }
}

使用測試

然後我們在ASP.NET Core MVC專案中,新建一個CacheController,然後在其Index方法中來測試RedisCache類的相關方法:

public class CacheController : Controller
{
    protected RedisCache redisCache;

    //由於我們前面在Startup類的ConfigureServices方法中呼叫了services.AddDistributedRedisCache來註冊Redis服務,所以ASP.NET Core MVC會自動依賴注入下面的IDistributedCache cache引數
    public CacheController(IDistributedCache cache)
    {
        redisCache = new RedisCache(cache);
    }

    public IActionResult Index()
    {
        bool isExisted;
        isExisted = redisCache.Exist("abc");//查詢鍵值"abc"是否存在
        redisCache.Remove("abc");//刪除不存在的鍵值"abc",不會報錯

        string key = "Key01";//定義快取鍵"Key01"
        string value = "This is a demo key !";//定義快取值

        redisCache.Set(key, value, new DistributedCacheEntryOptions()
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
        });//設定鍵值"Key01"到Redis,使用絕對過期時間,AbsoluteExpirationRelativeToNow設定為當前系統時間10分鐘後過期

        //也可以通過AbsoluteExpiration屬性來設定絕對過期時間為一個具體的DateTimeOffset時間點
        //redisCache.Set(key, value, new DistributedCacheEntryOptions()
        //{
        //    AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(10)
        //});//設定鍵值"Key01"到Redis,使用絕對過期時間,AbsoluteExpiration設定為當前系統時間10分鐘後過期

        var getVaue = redisCache.Get<string>(key, out isExisted);//從Redis獲取鍵值"Key01",可以看到getVaue的值為"This is a demo key !"

        value = "This is a demo key again !";//更改快取值

        redisCache.Set(key, value, new DistributedCacheEntryOptions()
        {
            SlidingExpiration = TimeSpan.FromMinutes(10)
        });//將更改後的鍵值"Key01"再次快取到Redis,這次使用滑動過期時間,SlidingExpiration設定為10分鐘

        getVaue = redisCache.Get<string>(key, out isExisted);//再次從Redis獲取鍵值"Key01",可以看到getVaue的值為"This is a demo key again !"

        redisCache.Remove(key);//從Redis中刪除鍵值"Key01"

        return View();
    }
}

前面我們在專案的Startup類ConfigureServices方法中,呼叫services.AddDistributedRedisCache註冊Redis服務的時候,有設定options.InstanceName = "DemoInstance",那麼這個InstanceName到底有什麼用呢?

當我們在上面的CacheController中呼叫Index方法的下面程式碼後:

string key = "Key01";//定義快取鍵"Key01"
string value = "This is a demo key !";//定義快取值

redisCache.Set(key, value, new DistributedCacheEntryOptions()
{
    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});//設定鍵值"Key01"到Redis,使用絕對過期時間,AbsoluteExpirationRelativeToNow設定為當前系統時間10分鐘後過期

我們使用redis-cli登入到Redis伺服器中,使用Keys *指令檢視當前Redis服務中儲存的所有鍵時,可以看到結果如下:

可以看到雖然我們程式碼中向Redis存入的鍵是"Key01",但是實際上在Redis服務中儲存的鍵是"DemoInstanceKey01",所以實際上真正存入Redis服務中的鍵是「InstanceName+鍵」這種組合鍵,因此我們可以通過設定不同的InstanceName來為不同的Application在Redis中做資料隔離,這就是InstanceName的作用

推薦學習:

以上就是一起聊聊使用redis實現分散式快取的詳細內容,更多請關注TW511.COM其它相關文章!