【.NET Core项目实战-统一认证平台】开篇及目录索引
一、背景
首先说声抱歉,可能是因为假期综合症(其实就是因为懒哈)的原因,已经很长时间没更新博客了,现在也调整的差不多了,准备还是以每周1-2篇的进度来更新博客,并完成本项目所有功能。
言归正传,本重构项目是在我根据实际需求重构,由于还未完全写完,所以也没进行压测,在2月份时,张善友老师给我留言说经过压测发现我重构的Ocelot
网关功能性能较差,其中根本原因就是缓存模块,由于重构项目的缓存强依赖Redis
缓存,造成性能瓶颈,发现问题后,我也第一时间进行测试,性能影响很大,经过跟张老师请教,可以使用二级缓存来解决性能问题,首先感谢张老师关注并指点迷津,于是就有了这篇文章,如何把现有缓存改成二级缓存并使用。
二、改造思路
为了解决redis
的强依赖性,首先需要把缓存数据存储到本地,所有请求都优先从本地提取,如果提取不到再从redis
提取,如果redis
无数据,在从数据库中提取。提取流程如下:
MemoryCache > Redis > db
此种方式减少提取缓存的网络开销,也合理利用了分布式缓存,并最终减少数据库的访问开销。但是使用此种方案也面临了一个问题是如何保证集群环境时每个机器本地缓存数据的一致性,这时我们会想到redis的发布、订阅特性,在数据发生变动时更新redis数据并发布缓存更新通知,由每个集群机器订阅变更事件,然后处理本地缓存记录,最终达到集群缓存的缓存一致性。
但是此方式对于缓存变更非常频繁的业务不适用,比如限流策略(准备还是使用分布式redis缓存实现),但是可以扩展配置单机限流时使用本地缓存实现,如果谁有更好的实现方式,也麻烦告知下集群环境下限流的实现,不胜感激。
三、改造代码
首先需要分析下目前改造后的Ocelot网关在哪些业务中使用的缓存,然后把使用本地缓存的的业务重构,增加提取数据流程,最后提供网关外部缓存初始化接口,便于与业务系统进行集成。
1.重写缓存方法
找到问题的原因后,就可以重写缓存方法,增加二级缓存支持,默认使用本地的缓存,新建CzarMemoryCache
类,来实现IOcelotCache<T>
方法,实现代码如下。
using Czar.Gateway.Configuration;
using Czar.Gateway.RateLimit;
using Microsoft.Extensions.Caching.Memory;
using Ocelot.Cache;
using System;
namespace Czar.Gateway.Cache
{
/// <summary>
/// 金焰的世界
/// 2019-03-03
/// 使用二级缓存解决集群环境问题
/// </summary>
public class CzarMemoryCache<T> : IOcelotCache<T>
{
private readonly CzarOcelotConfiguration _options;
private readonly IMemoryCache _cache;
public CzarMemoryCache(CzarOcelotConfiguration options,IMemoryCache cache)
{
_options = options;
_cache = cache;
}
public void Add(string key, T value, TimeSpan ttl, string region)
{
key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix,region, key);
if (_options.ClusterEnvironment)
{
var msg = value.ToJson();
if (typeof(T) == typeof(CachedResponse))
{//带过期时间的缓存
_cache.Set(key, value, ttl); //添加本地缓存
RedisHelper.Set(key, msg); //加入redis缓存
RedisHelper.Publish(key, msg); //发布
}
else if (typeof(T) == typeof(CzarClientRateLimitCounter?))
{//限流缓存,直接使用redis
RedisHelper.Set(key, value, (int)ttl.TotalSeconds);
}
else
{//正常缓存,发布
_cache.Set(key, value, ttl); //添加本地缓存
RedisHelper.Set(key, msg); //加入redis缓存
RedisHelper.Publish(key, msg); //发布
}
}
else
{
_cache.Set(key, value, ttl); //添加本地缓存
}
}
public void AddAndDelete(string key, T value, TimeSpan ttl, string region)
{
Add(key, value, ttl, region);
}
public void ClearRegion(string region)
{
if (_options.ClusterEnvironment)
{
var keys = RedisHelper.Keys(region + "*");
RedisHelper.Del(keys);
foreach (var key in keys)
{
RedisHelper.Publish(key, ""); //发布key值为空,处理时删除即可。
}
}
else
{
_cache.Remove(region);
}
}
public T Get(string key, string region)
{
key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
if(region== CzarCacheRegion.CzarClientRateLimitCounterRegion&& _options.ClusterEnvironment)
{//限流且开启了集群支持,默认从redis取
return RedisHelper.Get<T>(key);
}
var result = _cache.Get<T>(key);
if (result == null&& _options.ClusterEnvironment)
{
result= RedisHelper.Get<T>(key);
if (result != null)
{
if (typeof(T) == typeof(CachedResponse))
{//查看redis过期时间
var second = RedisHelper.Ttl(key);
if (second > 0)
{
_cache.Set(key, result, TimeSpan.FromSeconds(se