Polly+AspectCore实现熔断降级

项目源码:https://gitee.com/fan-microservices/polly

以下是杨中科通过Polly+AspectCore.Core实现的熔断降级

github:https://github.com/yangzhongke/RuPeng.HystrixCore

安装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。

参考:https://www.cnblogs.com/edisonchou/p/9159644.html

posted @ 2020-04-12 11:34  .Neterr  阅读(571)  评论(0编辑  收藏  举报