使用Polly向ASP.NET Core中的HttpClientFactory添加交叉剪切内存缓存

Couple days ago I Added Resilience and Transient Fault handling to your .NET Core HttpClient with Polly. Polly provides a way to pre-configure instances of HttpClient which apply Polly policies to every outgoing call. That means I can say things like "Call this API and try 3 times if there's any issues before you panic," as an example. It lets me move the cross-cutting concerns - the policies - out of the business part of the code and over to a central location or module, or even configuration if I wanted.

几天前,我通过Polly向您的.NET Core HttpClient添加了弹性和瞬态故障处理波莉提供了一种预配置HttpClient实例的方法,该实例将Polly策略应用于每个传出调用。 举例来说,这意味着我可以说“调用此API并在出现任何问题之前尝试3次”。 它使我可以将跨领域的关注点-策略-从代码的业务部分移至中央位置或模块,或者如果需要的话甚至可以移至配置。

I've been upgrading my podcast of late at https://www.hanselminutes.com to ASP.NET Core 2.1 and Razor Pages with SimpleCast for the back end. Since I've removed SQL Server as my previous data store and I'm now sitting entirely on top of a third party API I want to think about how often I call this API. As a rule, there's no reason to call it any more often that a change might occur.

最近,我一直在将https://www.hanselminutes.com上的播客升级到后端具有SimpleCast的ASP.NET Core 2.1和Razor Pages。 由于我已经删除了SQL Server作为以前的数据存储,而现在我完全坐在第三方API的上面,因此我想考虑一下我多久调用一次此API。 通常,没有理由再频繁地调用它,以免发生更改。

I publish a new show every Thursday at 5pm PST, so I suppose I could cache the feed for 7 days, but sometimes I make description changes, add links, update titles, etc. The show gets many tens of thousands of hits per episode so I definitely don't want to abuse the SimpleCast API, so I decided that caching for 4 hours seemed reasonable.

我在太平洋标准时间每周四下午5点发布一个新节目,所以我想可以将提要存储7天,但有时我会更改描述,添加链接,更新标题等。该节目每集获得数万点击我绝对不想滥用SimpleCast API,因此我认为缓存4小时似乎是合理的。

I went and wrote a bunch of caching code on my own. This is fine and it works and has been in production for a few months without any issues.

我自己去写了一堆缓存代码。 这很好,它可以正常工作并且已经投入生产几个月,没有任何问题。

A few random notes:

一些随机注释:

  • Stuff is being passed into the Constructor by the IoC system built into ASP.NET Core

    ASP.NET Core中内置的IoC系统将东西传递给构造器

    • That means the HttpClient, Logger, and MemoryCache are handed to this little abstraction. I don't new them up myself

      这意味着将HttpClient,Logger和MemoryCache交给了这个小抽象。 我不知道起来自己

  • All my "Show Database" is, is a GetShows()

    我所有的“显示数据库”都是GetShows()

    • That means I have TestDatabase that implements IShowDatabase that I use for some Unit Tests. And I could have multiple implementations if I liked.

      这意味着我有TestDatabase,它实现了我用于某些单元测试的IShowDatabase。 如果愿意,我可以有多种实现。
  • Caching here is interesting.

    在这里缓存很有趣。

    • Sure I could do the caching in just a line or two, but a caching double check is more needed that one often realizes.

      当然,我可以在一两行中进行缓存,但是经常需要对缓存进行双重检查。
    • I check the cache, and if I hit it, I am done and I bail. Yay!

      我检查了缓存,如果命中了,就完成了,然后保释。 好极了!
    • If not, Let's wait on a semaphoreSlim. This a great, simple way to manage waiting around a limited resource. I don't want to accidentally have two threads call out to the SimpleCast API if I'm literally in the middle of doing it already.

      如果没有,让我们等待semaphoreSlim 。 这是一种管理有限资源等待的好方法。 如果我真的已经在中间,我不想意外地有两个线程调用SimpleCast API。

      • "The SemaphoreSlim class represents a lightweight, fast semaphore that can be used for waiting within a single process when wait times are expected to be very short."

        SemaphoreSlim类表示一种轻量级的,快速的信号量,当预期的等待时间非常短时,可用于在单个进程中进行等待。”

      If not, Let's wait on a semaphoreSlim. This a great, simple way to manage waiting around a limited resource. I don't want to accidentally have two threads call out to the SimpleCast API if I'm literally in the middle of doing it already.

      如果没有,让我们等待semaphoreSlim 。 这是一种管理有限资源等待的好方法。 如果我真的已经在中间,我不想意外地有两个线程调用SimpleCast API。

    • So I check again inside that block to see if it showed up in the cache in the space between there and the previous check. Doesn't hurt to be paranoid.

      因此,我再次在该块中进行检查,以查看它是否出现在缓存中与上一个检查之间的空间中。 偏执不伤人。
    • Got it? Cool. Store it away and release as we finally the try.

      得到它了? 凉。 将其存储起来,并在我们最终尝试时释放。

Don't copy paste this. My GOAL is to NOT have to do any of this, even though it's valid.

不要复制粘贴。 我的目标是不必这样做,即使它是有效的。

public class ShowDatabase : IShowDatabase
{
private readonly IMemoryCache _cache;
private readonly ILogger _logger;
private SimpleCastClient _client;

public ShowDatabase(IMemoryCache memoryCache,
ILogger<ShowDatabase> logger,
SimpleCastClient client)
{
_client = client;
_logger = logger;
_cache = memoryCache;
}

static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1);

public async Task<List<Show>> GetShows()
{
Func<Show, bool> whereClause = c => c.PublishedAt < DateTime.UtcNow;

var cacheKey = "showsList";
List<Show> shows = null;

//CHECK and BAIL - optimistic
if (_cache.TryGetValue(cacheKey, out shows))
{
_logger.LogDebug($"Cache HIT: Found {cacheKey}");
return shows.Where(whereClause).ToList();
}

await semaphoreSlim.WaitAsync();
try
{
//RARE BUT NEEDED DOUBLE PARANOID CHECK - pessimistic
if (_cache.TryGetValue(cacheKey, out shows))
{
_logger.LogDebug($"Amazing Speed Cache HIT: Found {cacheKey}");
return shows.Where(whereClause).ToList();
}

_logger.LogWarning($"Cache MISS: Loading new shows");
shows = await _client.GetShows();
_logger.LogWarning($"Cache MISS: Loaded {shows.Count} shows");
_logger.LogWarning($"Cache MISS: Loaded {shows.Where(whereClause).ToList().Count} PUBLISHED shows");

var cacheExpirationOptions = new MemoryCacheEntryOptions();
cacheExpirationOptions.AbsoluteExpiration = DateTime.Now.AddHours(4);
cacheExpirationOptions.Priority = CacheItemPriority.Normal;

_cache.Set(cacheKey, shows, cacheExpirationOptions);
return shows.Where(whereClause).ToList(); ;
}
catch (Exception e)
{
_logger.LogCritical("Error getting episodes!");
_logger.LogCritical(e.ToString());
_logger.LogCritical(e?.InnerException?.ToString());
throw;
}
finally
{
semaphoreSlim.Release();
}
}
}

public interface IShowDatabase
{
Task<List<Show>> GetShows();
}

Again, this is great and it works fine. But the BUSINESS is in _client.GetShows() and the rest is all CEREMONY. Can this be broken up? Sure, I could put stuff in a base class, or make an extension method and bury it in there, so use Akavache or make a GetOrFetch and start passing around Funcs of "do this but check here first":

同样,这很棒,而且效果很好。 但是业务位于_client.GetShows()中,其余全部为CEREMONY 。 可以分手吗? 当然,我可以将东西放在基类中,或者制作一个扩展方法并将其埋在其中,因此请使用Akavache或制作一个GetOrFetch并开始传递Funcs的“这样做,但请先在这里检查”:

IObservable<T> GetOrFetchObject<T>(string key, Func<Task<T>> fetchFunc, DateTimeOffset? absoluteExpiration = null);

Could I use Polly and refactor via subtraction?

我可以使用Polly通过减法重构吗?

Per the Polly docs:

根据Polly文档:

The Polly CachePolicy is an implementation of read-through cache, also known as the cache-aside pattern. Providing results from cache where possible reduces overall call duration and can reduce network traffic.

Polly CachePolicy是通读缓存的实现,也称为“备用缓存模式” 。 在可能的情况下提供高速缓存的结果可减少总体通话时间,并可以减少网络流量。

First, I'll remove all my own caching code and just make the call on every page view. Yes, I could write the Linq a few ways. Yes, this could all be one line. Yes, I like Logging.

首先,我将删除所有自己的缓存代码,仅在每个页面视图上进行调用。 是的,我可以通过几种方式来编写Linq。 是的,这全都是一行。 是的,我喜欢记录。

public async Task<List<Show>> GetShows()
{
_logger.LogInformation($"Loading new shows");
List<Show> shows = await _client.GetShows();
_logger.LogInformation($"Loaded {shows.Count} shows");
return shows.Where(c => c.PublishedAt < DateTime.UtcNow).ToList(); ;
}

No caching, I'm doing The Least.

没有缓存,我正在做“最小”。

Polly supports both the .NET MemoryCache that is per process/per node, an also .NET Core's IDistributedCache for having one cache that lives somewhere shared like Redis or SQL Server. Since my podcast is just one node, one web server, and it's low-CPU, I'm not super worried about it. If Azure WebSites does end up auto-scaling it, sure, this cache strategy will happen n times. I'll switch to Distributed if that becomes a problem.

Polly支持每个进程/每个节点的.NET MemoryCache ,也支持.NET Core的IDistributedCache,以使一个缓存位于Redis或SQL Server之类的共享位置。 由于我的播客只是一个节点,一台Web服务器,而且它的CPU数量很低,所以我对此并不感到担心。 如果Azure WebSite确实可以对其进行自动缩放,则可以肯定,此缓存策略将发生n次。 如果出现问题,我将切换到“分布式”。

I'll add a reference to Polly.Caching.MemoryCache in my project.

我将在项目中添加对Polly.Caching.MemoryCache的引用。

I ensure I have the .NET Memory Cache in my list of services in ConfigureServices in Startup.cs:

我确保在Startup.cs的ConfigureServices中的服务列表中有.NET内存缓存:

services.AddMemoryCache();

STUCK ...现在!(STUCK...for now!)

AND...here is where I'm stuck. I got this far into the process and now I'm either confused OR I'm in a Chicken and the Egg Situation.

而且...这是我被困住的地方。 我已经深入到流程中,现在要么感到困惑,要么处于鸡与蛋的状况。

Forgive me, friends, and Polly authors, as this Blog Post will temporarily turn into a GitHub Issue. Once I work through it, I'll update this so others can benefit. And I still love you; no disrespect intended.

原谅我,朋友和Polly作者,因为此博客文章将暂时转变为GitHub问题。 一旦研究完成,我将对其进行更新,以便其他人可以从中受益。 我仍然爱你; 没有不尊重的意图。

The Polly.Caching.MemoryCache stuff is several months old, and existed (and worked) well before the new HttpClientFactory stuff I blogged about earlier.

Polly.Caching.MemoryCache东西已有几个月的历史了,并且在之前写过新的HttpClientFactory东西之前就已经存在(并且可以正常工作)。

I would LIKE to add my Polly Caching Policy chained after my Transient Error Policy:

补充我的波利缓存策略我的瞬时错误政策后链接:

services.AddHttpClient<SimpleCastClient>().
AddTransientHttpErrorPolicy(policyBuilder => policyBuilder.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 2,
durationOfBreak: TimeSpan.FromMinutes(1)
)).
AddPolicyHandlerFromRegistry("myCachingPolicy"); //WHAT I WANT?

However, that policy hasn't been added to the Policy Registry yet. It doesn't exist! This makes me feel like some of the work that is happening in ConfigureServices() is a little premature. ConfigureServices() is READY, AIM and Configure() is FIRE/DO-IT, in my mind.

但是,该策略尚未添加到“策略注册表”中。 它不存在! 这让我觉得ConfigureServices()中正在进行的某些工作还为时过早。 在我看来,ConfigureServices()是READY,AIM,Configure()是FIRE / DO-IT。

If I set up a Memory Cache in Configure, I need to use the Dependency System to get the stuff I want, like the .NET Core IMemoryCache that I put in services just now.

如果在“配置”中设置了内存缓存,则需要使用“依赖系统”来获取所需的内容,例如我刚才在服务中放入的.NET Core IMemoryCache。

public void Configure(IApplicationBuilder app, IPolicyRegistry<string> policyRegistry, IMemoryCache memoryCache)
{
MemoryCacheProvider memoryCacheProvider = new MemoryCacheProvider(memoryCache);
var cachePolicy = Policy.CacheAsync(memoryCacheProvider, TimeSpan.FromMinutes(5));
policyRegistry.Add("cachePolicy", cachePolicy);
...

But at this point, it's too late! I need this policy up earlier...or I need to figure a way to tell the HttpClientFactory to use this policy...but I've been using extension methods in ConfigureServices to do that so far. Perhaps some exception methods are needed like AddCachingPolicy(). However, from what I can see:

但是,现在为时已晚! 我需要早些时候制定此策略...或者我需要找到一种方法告诉HttpClientFactory使用此策略...但是到目前为止,我一直在使用ConfigureServices中的扩展方法来做到这一点。 也许需要一些异常方法,例如AddCachingPolicy()。 但是,从我可以看到:

I'm likely bumping into a point in time thing. I will head to bed and return to this post in a day and see if I (or you, Dear Reader) have solved the problem in my sleep.

我很可能碰到一个时间点的东西。 我将上床睡觉,一天后返回这篇文章,看看我(或您,亲爱的读者)是否已经解决了我的睡眠问题。

"code making and code breaking" by earthlightbooks - Licensed under CC-BY 2.0 - Original source via Flickr

“代码接通和断代码”由earthlightbooks -下许可CC-BY 2.0 -原始来源经由Flickr的

Sponsor: Check out JetBrains Rider: a cross-platform .NET IDE. Edit, refactor, test and debug ASP.NET, .NET Framework, .NET Core, Xamarin or Unity applications. Learn more and download a 30-day trial!

赞助商:查看JetBrains Rider:一个跨平台的.NET IDE 。 编辑,重构,测试和调试ASP.NET,.NET Framework,.NET Core,Xamarin或Unity应用程序。 了解更多信息并下载30天试用版

翻译自: https://www.hanselman.com/blog/adding-crosscutting-memory-caching-to-an-httpclientfactory-in-aspnet-core-with-polly

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值