首頁  >  文章  >  資料庫  >  一起聊聊使用redis實現分散式緩存

一起聊聊使用redis實現分散式緩存

WBOY
WBOY轉載
2022-07-14 17:01:172643瀏覽

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

一起聊聊使用redis實現分散式緩存

推薦學習:Redis影片教學

分散式快取描述:

#分散式快取重點是在分散式上,相信大家接觸過的分散式有很多中,像分散式開發,分散式部署,分散式鎖定、事物、系統等有很多。使我們對分散式本身就有一個很明確的認識,分散式就是有多個應用程式組成,可能分佈在不同的伺服器上,最終都是在為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影片教學

#

以上是一起聊聊使用redis實現分散式緩存的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除