Polly+AspectCore实现熔断降级
项目源码:https://gitee.com/fan-microservices/polly
以下是杨中科通过Polly+AspectCore.Core实现的熔断降级
安装Nuget
NuGet>Install-Package Polly
NuGet>Install-Package AspectCore.Core
NuGet>Install-Package Microsoft.Extensions.Caching.Memory
查看HystrixCommandAttribute源码
using AspectCore.DynamicProxy;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Threading.Tasks;
[AttributeUsage(AttributeTargets.Method)]
public class HystrixCommandAttribute : AbstractInterceptorAttribute
{
/// <summary>
/// 最多重试几次,如果为0则不重试
/// </summary>
public int MaxRetryTimes { get; set; } = 0;
/// <summary>
/// 重试间隔的毫秒数
/// </summary>
public int RetryIntervalMilliseconds { get; set; } = 100;
/// <summary>
/// 是否启用熔断
/// </summary>
public bool IsEnableCircuitBreaker { get; set; } = false;
/// <summary>
/// 熔断前出现允许错误几次
/// </summary>
public int ExceptionsAllowedBeforeBreaking { get; set; } = 3;
/// <summary>
/// 熔断多长时间(毫秒)
/// </summary>
public int MillisecondsOfBreak { get; set; } = 1000;
/// <summary>
/// 执行超过多少毫秒则认为超时(0表示不检测超时)
/// </summary>
public int TimeOutMilliseconds { get; set; } = 0;
/// <summary>
/// 缓存多少毫秒(0表示不缓存),用“类名+方法名+所有参数ToString拼接”做缓存Key
/// </summary>
public int CacheTTLMilliseconds { get; set; } = 0;
private static ConcurrentDictionary<MethodInfo, Policy> policies
= new ConcurrentDictionary<MethodInfo, Policy>();
private static readonly IMemoryCache memoryCache
= new MemoryCache(new Microsoft.Extensions.Caching.Memory.MemoryCacheOptions());
/// <summary>
/// HystrixCommandAttribute
/// </summary>
/// <param name="fallBackMethod">降级的方法名</param>
public HystrixCommandAttribute(string fallBackMethod)
{
this.FallBackMethod = fallBackMethod;
}
public string FallBackMethod { get; set; }
public override async Task Invoke(AspectContext context, AspectDelegate next)
{
//一个HystrixCommand中保持一个policy对象即可
//其实主要是CircuitBreaker要求对于同一段代码要共享一个policy对象
//根据反射原理,同一个方法的MethodInfo是同一个对象,但是对象上取出来的HystrixCommandAttribute
//每次获取的都是不同的对象,因此以MethodInfo为Key保存到policies中,确保一个方法对应一个policy实例
policies.TryGetValue(context.ServiceMethod, out Policy policy);
lock (policies)//因为Invoke可能是并发调用,因此要确保policies赋值的线程安全
{
if (policy == null)
{
policy = Policy.NoOpAsync();//创建一个空的Policy
if (IsEnableCircuitBreaker)
{
policy = policy.WrapAsync(Policy.Handle<Exception>().CircuitBreakerAsync(ExceptionsAllowedBeforeBreaking, TimeSpan.FromMilliseconds(MillisecondsOfBreak), (ex, ts) =>
{
// assuem to do logging
Console.WriteLine($"CircuitBreaker -- ts = {ts.Seconds}s, ex.message = {ex.Message}");
}, () => { }));
}
if (TimeOutMilliseconds > 0)
{
policy = policy.WrapAsync(Policy.TimeoutAsync(() => TimeSpan.FromMilliseconds(TimeOutMilliseconds), Polly.Timeout.TimeoutStrategy.Pessimistic));
}
if (MaxRetryTimes > 0)
{
policy = policy.WrapAsync(Policy.Handle<Exception>().WaitAndRetryAsync(MaxRetryTimes, i => TimeSpan.FromMilliseconds(RetryIntervalMilliseconds)));
}
Policy policyFallBack = Policy
.Handle<Exception>()
.FallbackAsync(async (ctx, t) =>
{
AspectContext aspectContext = (AspectContext)ctx["aspectContext"];
var fallBackMethod = context.ServiceMethod.DeclaringType.GetMethod(this.FallBackMethod);
Object fallBackResult = fallBackMethod.Invoke(context.Implementation, context.Parameters);
//不能如下这样,因为这是闭包相关,如果这样写第二次调用Invoke的时候context指向的
//还是第一次的对象,所以要通过Polly的上下文来传递AspectContext
//context.ReturnValue = fallBackResult;
aspectContext.ReturnValue = fallBackResult;
}, async (ex, t) => { });
policy = policyFallBack.WrapAsync(policy);
//放入
policies.TryAdd(context.ServiceMethod, policy);
}
}
//把本地调用的AspectContext传递给Polly,主要给FallbackAsync中使用,避免闭包的坑
Context pollyCtx = new Context();
pollyCtx["aspectContext"] = context;
//Install-Package Microsoft.Extensions.Caching.Memory
if (CacheTTLMilliseconds > 0)
{
//用类名+方法名+参数的下划线连接起来作为缓存key
string cacheKey = "HystrixMethodCacheManager_Key_" + context.ServiceMethod.DeclaringType
+ "." + context.ServiceMethod + string.Join("_", context.Parameters);
//尝试去缓存中获取。如果找到了,则直接用缓存中的值做返回值
if (memoryCache.TryGetValue(cacheKey, out var cacheValue))
{
context.ReturnValue = cacheValue;
}
else
{
//如果缓存中没有,则执行实际被拦截的方法
await policy.ExecuteAsync(ctx => next(context), pollyCtx);
//存入缓存中
using (var cacheEntry = memoryCache.CreateEntry(cacheKey))
{
cacheEntry.Value = context.ReturnValue;
cacheEntry.AbsoluteExpiration = DateTime.Now + TimeSpan.FromMilliseconds(CacheTTLMilliseconds);
}
}
}
else//如果没有启用缓存,就直接执行业务方法
{
await policy.ExecuteAsync(ctx => next(context), pollyCtx);
}
}
}
如何使用
using AspectCore.DynamicProxy;
using System;
using System.Threading;
namespace HystrixCoreTests
{
class Program
{
static void Main(string[] args)
{
ProxyGeneratorBuilder proxyGeneratorBuilder = new ProxyGeneratorBuilder();
using (IProxyGenerator proxyGenerator = proxyGeneratorBuilder.Build())
{
Person p = proxyGenerator.CreateClassProxy<Person>();
Console.WriteLine(p.HelloAsync("yzk").Result);
}
Console.ReadKey();
}
}
}
以上案例使用的是AspectCore.Core
,比较麻烦,可以使用AspectCore的DI扩展包简化流程
安装Nuget
NuGet>Install-Package AspectCore.Extensions.DependencyInjection
program
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
// 略
.UseServiceProviderFactory(new DynamicProxyServiceProviderFactory());
startup
改写Startup类的ConfigureService方法,把返回值从void改为IServiceProvider
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ProductService>();
services.ConfigureDynamicProxy();
services.AddControllers();
}
ProductService
public class ProductService
{
[HystrixCommand(nameof(GetAllProductsFallBackAsync),
IsEnableCircuitBreaker = true,
ExceptionsAllowedBeforeBreaking = 3,
MillisecondsOfBreak = 1000 * 5)]
public virtual async Task<string> GetAllProductsAsync(string productType)
{
Console.WriteLine($"-->>Starting get product type : {productType}");
string str = null;
str.ToString();
return $"OK {productType}";
}
/// <summary>
/// 回退
/// </summary>
/// <param name="productType"></param>
/// <returns></returns>
public virtual async Task<string> GetAllProductsFallBackAsync(string productType)
{
Console.WriteLine($"-->>FallBack : Starting get product type : {productType}");
return $"OK for FallBack {productType}";
}
}
这里假设我们主要针对GetAllProductsAsync这个方法进行熔断保护,假设它会调用另一个Service的获取产品的接口,这个接口会访问核心数据库,其每天的访问量很大,我们对此接口进行熔断保护,设置在启用熔断保护前允许两次故障(这里主要指异常),熔断保护时间为5s。