前言
本節我們來介紹一款強大的庫Polly,Polly是一種.NET彈性和瞬態故障處理庫,允許我們以非常順暢和線程安全的方式來執諸如行重試,斷路,超時,故障恢復等策略。 Polly針對對.NET 4.0,.NET 4.5和.NET Standard 1.1以及.NET Core實現,該項目作者現已成為.NET基金會一員,項目一直在不停迭代和更新,項目地址【https://github.com/App-vNext/Polly】,你值得擁有。接下來我們以.NET Framework 4.5來演示它的強大功能。
Introduce Polly
首先我們得下載Polly包,最新版本為5.3.1,如下:

該庫實現了七種恢復策略,下面我一一為您來介紹。
重試策略(Retry)
重試策略針對的前置條件是短暫的故障延遲且在短暫的延遲之后能夠自我糾正。允許我們做的是能夠自動配置重試機制。
斷路器(Circuit-breaker)
斷路器策略針對的前置條件是當系統繁忙時,快速響應失敗總比讓用戶一直等待更好。保護系統故障免受過載,Polly可以幫其恢復。
超時(Timeout)
超時策略針對的前置條件是超過一定的等待時間,想要得到成功的結果是不可能的,保證調用者不必等待超時。
隔板隔離(Bulkhead Isolation)
隔板隔離針對的前置條件是當進程出現故障時,多個失敗一直在主機中對資源(例如線程/ CPU)一直占用。下游系統故障也可能導致上游失敗。這兩個風險都將造成嚴重的后果。都說一粒老鼠子屎攪渾一鍋粥,而Polly則將受管制的操作限制在固定的資源池中,免其他資源受其影響。
緩存(Cache)
緩存策略針對的前置條件是數據不會很頻繁的進行更新,為了避免系統過載,首次加載數據時將響應數據進行緩存,如果緩存中存在則直接從緩存中讀取。
回退(Fallback)
操作仍然會失敗,也就是說當發生這樣的事情時我們打算做什么。也就是說定義失敗返回操作。
策略包裝(PolicyWrap)
策略包裝針對的前置條件是不同的故障需要不同的策略,也就意味着彈性靈活使用組合。
幾種策略使用
一旦從事IT就得警惕異常並友好擁抱異常而非不聞不問,這個時候我們利用try{}catch{}來處理。
try
{
var a = 0;
var b = 1 / a;
}
catch (DivideByZeroException ex)
{
throw ex;
}
若我們想重試三次,此時我們只能進行循環三次操作。我們只能簡單進行處理,自從有了Polly,什么重試機制,超時都不在話下,下面我們來簡短介紹各種策略。Polly默認處理策略需要指定拋出的具體異常或者執行拋出異常返回的結果。處理單個類型異常如下:
Policy .Handle<DivideByZeroException>()
上述異常指嘗試除以0,下面我們演示下具體使用,我們嘗試除以0並用Polly指定該異常並重試三次。
static int Compute()
{
var a = 0;
return 1 / a;
}
try
{
var retryTwoTimesPolicy =
Policy
.Handle<DivideByZeroException>()
.Retry(3, (ex, count) =>
{
Console.WriteLine("執行失敗! 重試次數 {0}", count);
Console.WriteLine("異常來自 {0}", ex.GetType().Name);
});
retryTwoTimesPolicy.Execute(() =>
{
Compute();
});
}
catch (DivideByZeroException e)
{
Console.WriteLine($"Excuted Failed,Message: ({e.Message})");
}

如果我們想指定處理多個異常類型通過OR即可。
Policy .Handle<DivideByZeroException>() .Or<ArgumentException>()
當然還有更加強大的功能,比如在微信支付時,微信回調我們的應用程序時,此時若失敗,想必微信那邊也會做重試機制,例如隔一段時間重試調用一次,重復調用幾次后仍失敗則不再回調。我們利用Polly則可以演示等待重試機制。
/// <summary>
/// 拋出異常
/// </summary>
static void ZeroExcepcion()
{
throw new DivideByZeroException();
}
/// <summary>
/// 異常信息
/// </summary>
/// <param name="e"></param>
/// <param name="tiempo"></param>
/// <param name="intento"></param>
/// <param name="contexto"></param>
static void ReportaError(Exception e, TimeSpan tiempo, int intento, Context contexto)
{
Console.WriteLine($"異常: {intento:00} (調用秒數: {tiempo.Seconds} 秒)\t執行時間: {DateTime.Now}");
}
try
{
var politicaWaitAndRetry = Policy
.Handle<DivideByZeroException>()
.WaitAndRetry(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(3),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(7)
}, ReportaError);
politicaWaitAndRetry.Execute(() =>
{
ZeroExcepcion();
});
}
catch (Exception e)
{
Console.WriteLine($"Executed Failed,Message:({e.Message})");
}

我們講完默認策略和重試策略,再來看看反饋策略,翻譯的更通俗一點則是執行失敗后返回的結果,此時要為Polly指定返回類型,然后指定異常,最后調用Fallback方法。
static string ThrowException()
{
throw new Exception();
}
var fallBackPolicy =
Policy<string>
.Handle<Exception>()
.Fallback("執行失敗,返回Fallback");
var fallBack = fallBackPolicy.Execute(() =>
{
return ThrowException();
});
Console.WriteLine(fallBack);

包裹策略說到底就是混合多種策略,並執行。
var fallBackPolicy =
Policy<string>
.Handle<Exception>()
.Fallback("執行失敗,返回Fallback");
var fallBack = fallBackPolicy.Execute(() =>
{
return ThrowException();
});
Console.WriteLine(fallBack);
var politicaWaitAndRetry =
Policy<string>
.Handle<Exception>()
.Retry(3, (ex, count) =>
{
Console.WriteLine("執行失敗! 重試次數 {0}", count);
Console.WriteLine("異常來自 {0}", ex.GetType().Name);
});
var mixedPolicy = Policy.Wrap(fallBackPolicy, politicaWaitAndRetry);
var mixedResult = mixedPolicy.Execute(ThrowException);
Console.WriteLine($"執行結果: {mixedResult}");

至此關於Polly的基本介紹就已結束,該庫還是非常強大,更多特性請參考上述github例子,接下來我們來看看兩種具體場景。
ASP.NET Web APi使用Polly重試機制
在Polly v4.30中以上可以利用HandleResult指定返回結果,如下:
Policy .HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.NotFound)
基於此我們完全可以利用執行Web APi中的響應策略,如下:
public readonly RetryPolicy<HttpResponseMessage> _httpRequestPolicy;
拿到響應中狀態碼,若為500則重試三次。
_httpRequestPolicy = Policy.HandleResult<HttpResponseMessage>(
r => r.StatusCode == HttpStatusCode.InternalServerError)
.WaitAndRetryAsync(3,
retryAttempt => TimeSpan.FromSeconds(retryAttempt));
上述獲取請求響應策略在構造函數中獲取。
public class PollyController : ApiController
{
public readonly RetryPolicy<HttpResponseMessage> _httpRequestPolicy;
public PollyController()
{
_httpRequestPolicy = Policy.HandleResult<HttpResponseMessage>(
r => r.StatusCode == HttpStatusCode.InternalServerError)
.WaitAndRetryAsync(3,
retryAttempt => TimeSpan.FromSeconds(retryAttempt));
}
}
此時調用接口時執行策略的Execute或者ExecuteAsync方法即可。
public async Task<IHttpActionResult> Get()
{
var httpClient = new HttpClient();
string requestEndpoint = "http://localhost:4096";
HttpResponseMessage httpResponse = await _httpRequestPolicy.ExecuteAsync(() => httpClient.GetAsync(requestEndpoint));
IEnumerable<string> numbers = await httpResponse.Content.ReadAsAsync<IEnumerable<string>>();
return Ok(numbers);
}
你以為僅限於在Web APi中使用嗎?在其他框架中也可以使用,例如EntityFramework 6.x中,在EntityFramework 6+上出現了執行策略,也就是執行重試機制,這個時候我們依然可以借助Polly輪子來實現。
EntityFramework 6.x使用Polly重試機制
在EntityFramework 6.x中有如下執行策略接口,看起來是不是和Polly中的Execute方法是不是很類似。
//
// 摘要:
// A strategy that is used to execute a command or query against the database, possibly
// with logic to retry when a failure occurs.
public interface IDbExecutionStrategy
{
//
// 摘要:
// Indicates whether this System.Data.Entity.Infrastructure.IDbExecutionStrategy
// might retry the execution after a failure.
bool RetriesOnFailure { get; }
//
// 摘要:
// Executes the specified operation.
//
// 參數:
// operation:
// A delegate representing an executable operation that doesn't return any results.
void Execute(Action operation);
//
// 摘要:
// Executes the specified operation and returns the result.
//
// 參數:
// operation:
// A delegate representing an executable operation that returns the result of type
// TResult.
//
// 類型參數:
// TResult:
// The return type of operation.
//
// 返回結果:
// The result from the operation.
TResult Execute<TResult>(Func<TResult> operation);
//
// 摘要:
// Executes the specified asynchronous operation.
//
// 參數:
// operation:
// A function that returns a started task.
//
// cancellationToken:
// A cancellation token used to cancel the retry operation, but not operations that
// are already in flight or that already completed successfully.
//
// 返回結果:
// A task that will run to completion if the original task completes successfully
// (either the first time or after retrying transient failures). If the task fails
// with a non-transient error or the retry limit is reached, the returned task will
// become faulted and the exception must be observed.
Task ExecuteAsync(Func<Task> operation, CancellationToken cancellationToken);
//
// 摘要:
// Executes the specified asynchronous operation and returns the result.
//
// 參數:
// operation:
// A function that returns a started task of type TResult.
//
// cancellationToken:
// A cancellation token used to cancel the retry operation, but not operations that
// are already in flight or that already completed successfully.
//
// 類型參數:
// TResult:
// The result type of the System.Threading.Tasks.Task`1 returned by operation.
//
// 返回結果:
// A task that will run to completion if the original task completes successfully
// (either the first time or after retrying transient failures). If the task fails
// with a non-transient error or the retry limit is reached, the returned task will
// become faulted and the exception must be observed.
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> operation, CancellationToken cancellationToken);
}
EntityFramework 6.x中的執行策略說到底就是數據庫連接問題即彈性連接,若考慮到數據庫過渡負載問題,此時應用程序和數據庫之間存在網絡問題的話。可能數據庫連接在幾秒內才返回,此時也沒有什么很大的問題,我們完全可以再嘗試一次,此時或許過了連接頻繁期,保證連接立馬恢復。如果數據庫連接一會恢復不了呢?或許是五分鍾,又或者是半個小時。如果我們只是一味盲目的進行重試,這顯然不可取。如果我們的應用程序連接超時時間超過了20秒,若我們選擇繼續連接到數據庫,我們將很快用完我們應用程序池中的工作線程。一直等待數據庫的響應。此時網站將完全無響應,同時會給用戶頁面無響應的友好提醒。這是Polly庫中描述斷路器的很好例子,換句話說如果我們捕獲了m個數量的SqlExceptions,假設數據庫有其他問題存在,導致我們不能在n秒內再嘗試連接數據庫。此時在數據庫連接上存在一個問題,那就是阻塞了我們的應用程序工作線程被掛起,我們試圖連接數據庫,我們假設不可用的話,但是我們要打破這種不可用,那就用Polly吧。
我們看到上述EntityFramework 6.x實現了IDbExecutionStrategy接口,但沒有實現如Polly中的斷路器模式,EntityFramework 6.x中的執行策略只是重試機制而已。 比如SqlAzureExecutionStrategy將在指定的時間段內重試指定的次數,直到一段時間段過去,重試指數過后,接着就是失敗。 同時所有后續調用將執行相同操作,重試並失敗。 這是調用數據庫時最好的策略嗎? 不敢肯定,或許Polly中的斷路器模式值得我們借鑒。我們自己來實現上述執行策略接口。
public class CirtuitBreakerExecutionStrategy : IDbExecutionStrategy
{
private Policy _policy;
public CirtuitBreakerExecutionStrategy(Policy policy)
{
_policy = policy;
}
public void Execute(Action operation)
{
_policy.Execute(() =>
{
operation.Invoke();
});
}
public TResult Execute<TResult>(Func<TResult> operation)
{
return _policy.Execute(() =>
{
return operation.Invoke();
});
}
public async Task ExecuteAsync(Func<Task> operation, CancellationToken cancellationToken)
{
await _policy.ExecuteAsync(() =>
{
return operation.Invoke();
});
}
public async Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> operation, CancellationToken cancellationToken)
{
return await _policy.ExecuteAsync(() =>
{
return operation.Invoke();
});
}
public bool RetriesOnFailure { get { return true; } }
}
接下來在基於代碼配置文件中設置我們上述自定義實現的斷路器模式。
public class EFConfiguration : DbConfiguration
{
public Policy _policy;
public EFConfiguration()
{
_policy = Policy.Handle<Exception>().CircuitBreaker(3, TimeSpan.FromSeconds(60));
SetExecutionStrategy("System.Data.SqlClient", () => new CirtuitBreakerExecutionStrategy(_policy));
}
}
上述自定義實現執行策略不保證一定有用或許也是一種解決方案呢。

