如何使用redis实现分布式缓存

62次阅读
没有评论

共计 9308 个字符,预计需要花费 24 分钟才能阅读完成。

本文丸趣 TV 小编为大家详细介绍“如何使用 redis 实现分布式缓存”,内容详细,步骤清晰,细节处理妥当,希望这篇“如何使用 redis 实现分布式缓存”文章能帮助大家解决疑惑,下面跟着丸趣 TV 小编的思路慢慢深入,一起来学习新知识吧。

分布式缓存描述:

分布式缓存重点是在分布式上,相信大家接触过的分布式有很多中,像分布式开发,分布式部署,分布式锁、事物、系统 等有很多。使我们对分布式本身就有一个很明确的认识,分布式就是有多个应用程序组成,可能分布在不同的服务器上,最终都是在为 web 端提供服务。
分布式缓存有以下几点优点:

所有的 Web 服务器上的缓存数据都是相同的,不会因为应用程序不同,服务器的不同导致缓存数据的不一样。

缓存的是独立的不受 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 实现分布式缓存”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注丸趣 TV 行业资讯频道。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-13发表,共计9308字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)