你們常常出現同步調用Redis超時的問題,但改爲異步以後發現錯誤很是少了,但卻可能經過先後記日誌之類的發現Redis命令很是慢。git
PS: 之後代碼都在Windows bash中運行,StackExchange.Redis版本爲1.2.6github
先快速重現問題和解決問題,你們先運行下面的代碼redis
public static async Task Main(string[] args) { ThreadPool.SetMinThreads(8, 8); using (var connection = await ConnectionMultiplexer.ConnectAsync("localhost")) { connection.PreserveAsyncOrder = false; var db = connection.GetDatabase(0); var sw = Stopwatch.StartNew(); await Task.WhenAll(Enumerable.Range(0, 10) .Select(_ => Task.Run(() => { db.StringGet("aaa"); Thread.Sleep(1000); }))); Console.WriteLine(sw.ElapsedMilliseconds); } }
運行發現拋出StackExchange.Redis.RedisTimeoutException,爲何呢?是由於當前工做線程根本不夠用,同步等待時已經超時。具體請看源代碼bash
若是將上面的ThreadPool.SetMinThreads(8, 8)改爲ThreadPool.SetMinThreads(100, 100)呢?是否是不拋異常了呢。網絡
再說異步接口變慢的問題,你們先運行下面的代碼:異步
public static async Task Main(string[] args) { var tcs = new TaskCompletionSource<bool>(); var sw = Stopwatch.StartNew(); Console.WriteLine($"Main1: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}"); var task = Task.Run(() => { Thread.Sleep(10); Console.WriteLine($"Run1: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}"); tcs.TrySetResult(true); Console.WriteLine($"Run2: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}"); Thread.Sleep(10000); }); var a = tcs.Task.ContinueWith(_ => { Console.WriteLine($"a: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}"); }); var b = tcs.Task.ContinueWith(_ => { Console.WriteLine($"b: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}"); }); var c = tcs.Task.ContinueWith(_ => { Console.WriteLine($"c: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}"); }); await tcs.Task; Console.WriteLine($"Main2: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}"); Thread.Sleep(100); await Task.Delay(10); Console.WriteLine($"Main3: {sw.ElapsedMilliseconds}, ThreadId: {Environment.CurrentManagedThreadId}"); }
最終輸出結果發現Run1和Main2是使用相同的線程吧,而Run2的ElapsedMilliseconds基本上就是在Run1的基礎上加100。async
而後再回到調用Redis代碼上學習
static async Task Main(string[] args) {
ThreadPool.SetMinThreads(100, 100); using (var connection = await ConnectionMultiplexer.ConnectAsync("localhost")) { var db = connection.GetDatabase(0); var sw = Stopwatch.StartNew(); await Task.WhenAll(Enumerable.Range(0, 10) .Select(_ => Task.Run(async () => { await db.StringGetAsync("aaa"); Thread.Sleep(100); }))); Console.WriteLine(sw.ElapsedMilliseconds); } }
大家發現輸出是100多仍是1000多?爲何?原來是由於sdk中有一個特殊的設置,要保護異步代碼執行的順序,而後咱們在GetDatabase行以前加一個代碼connection.PreserveAsyncOrder = false;優化
而後再運行一次看看結果是多少呢?經過上面再作代碼基本上能夠肯定異步慢是和TaskCompletionSource和關係的,具體請看sdk的源代碼。spa
總結上面兩點,簡單得經過SetMinThreads和connection.PreserveAsyncOrder = false能夠解決絕大部分問題,但更多其餘深層次的問題怎麼發現呢?
下面就要介紹StackExchange.Redis兩個神器ConnectionCounters和IProfiler
public class ConnectionCounters { /// <summary> /// Operations that have been requested, but which have not yet been sent to the server /// </summary> public int PendingUnsentItems { get; } /// <summary> /// Operations that have been sent to the server, but which are awaiting a response /// </summary> public int SentItemsAwaitingResponse { get; } /// <summary> /// Operations for which the response has been processed, but which are awaiting asynchronous completion /// </summary> public int ResponsesAwaitingAsyncCompletion { get; } }
每一個屬性表示當前redis鏈接的待完成的命令當前所處的狀態。經過字面意思就能夠知道PendingUnsentItems表示已經進行待發送隊列還未發送出去的命令;SentItemsAwaitingResponse表示已經發送出去但尚未收到響應結果的命令;ResponsesAwaitingAsyncCompletion則表示已經收到響應的命令,但尚未調用TaskCompletionSource<T>().TrySetResult()的命令。
其中PendingUnsentItems和SentItemsAwaitingResponse過大的緣由基本上是由於網絡阻塞了,你須要檢查一下網絡帶寬或者redis的value是否很大。
ResponsesAwaitingAsyncCompletion則是由於await以後的代碼,如上面示例中的代碼,線程佔用了很長的同步時間,須要優化代碼和將PreserveAsyncOrder設置爲false。
發現問題就須要解決問題,也就須要深層次得去學習才能解決問題。我不喜歡寫文章,但發現最近有好幾篇說redis超時的問題,最終我仍是想把本身的踩坑的心得分享給你們。
這在裏說一個好消息,那就是StackExchange.Redis 2.0已經從重構了異步隊列,使用管道方式解決異步慢的問題