流水線和復用
糟糕的時間浪費。現代的計算機以驚人的速度產生大量的數據,而且高速網絡通道(通常在重要的服務器之間同時存在多個鏈路)提供了很高的帶寬,但是計算機花費了大量的時間在 等待數據 上面,這也是造成使用持久性鏈接的編程方式越來越流行的原因之一。常規的編碼方式:
string a = db.StringGet("a");
string b = db.StringGet("b");
就步驟而言,它看起來像下面這樣:
[req1] # client: the client library constructs request 1[c=>s] # network: request one is sent to the server[server] # server: the server processes request 1[s=>c] # network: response one is sent back to the client[resp1] # client: the client library parses response 1[req2][c=>s][server][s=>c][resp2]
高亮 client 端正在做的事情:
[req1][====waiting=====][resp1][req2][====waiting=====][resp2]
如果按照時間縮放的話,就會看到大部分的時間都耗在了 等待
上面。
Pipelining
由于上述的原因,很多 redis 客戶端允許使用 pipelining; 這是一種可以一次性通過管道發送多條消息的方式而不用一條一條的發送等.待,而且可以在等到回復時一并處理他們. .NET 的異步方法。
舉例來說
var aPending = db.StringGetAsync("a");
var bPending = db.StringGetAsync("b");
var a = db.Wait(aPending);
var b = db.Wait(bPending);
在這里使用 db.Wait
可以自動采用配置的超時時間, 但也可以使用 aPending.Wait()
或者 Task.WaitAll(aPending, bPending);
的方式,根據你自己的喜好來. 這樣一來 20 個請求可能合并為一個請求。
Fire and Forget
pipelining 的一種特殊場景是,我們并不關心某個操作的返回結果,這樣的話我們的代碼可以馬上接著往下執行,而那個放入到隊列中的操作將會在后臺執行. 通常來說,這意味我們可以把并行的操作放到一個單獨的 caller 的連接上執行. 這種情況下, 我們可以使用 flags
參數達到這樣的目的.
// sliding expiration
db.KeyExpire(key, TimeSpan.FromMinutes(5), flags: CommandFlags.FireAndForget);
var value = (string)db.StringGet(key);
FireAndForget
標志使得client library 正常的把要做的事情的排成隊列, 但是會馬上返回一個默認的值,這個默認值沒有任何的實用意義. 這也適用于 *Async
方法:會返回一個已經完成的 Task<T>
.
Multiplexing 多路復用
pipelining 已經很好了,但是很多情況下一個代碼塊只需要一個單獨的值 (或許這個代碼塊希望執行一些簡單的操作,但是這些操作是相互依賴的). 這也就是說我們仍然會消耗大量的時間在client和server之間交互等待. 假如有一個業務繁忙的應用, 或許是一個網站服務器. 這些應用往往都是并發的, 所以如果有20個并行的應用請求數據,你可能采用建立20個鏈接的方式,或者異步使用同一個鏈接(最后一個請求需要等之前19個完成). 或者做個讓步, 使用一個擁有5個鏈接的程序池 - 但是不管你怎么做,都會有大量的等待發生。StackExchange.Redis 沒有做以上的事情 它只是盡可能的充分復用一個鏈接. 當被不同的調用者同時調用時,它會 自動地將不同的請求放到隊列中, 所以無論請求使用同步或者異步的方式,要做的事情都會放到隊列中. 所以我們可能有10個或者20個 "get a and b" 的場景,他們都會盡快的獲取鏈接執行命令.
鑒于以上的原因,StackExchange.Redis 沒有( 將來也不會 )提供 "blocking pops" (BLPOP, BRPOP and BRPOPLPUSH). 因為這會使得一個單獨的調用者減慢整個的多路復用環境,堵塞其他所有的調用者. 唯一的例外 StackExchange.Redis 需要驗證事務的先決條件時需要保證工作的時間. 這也是StackExchange.Redis 將這些條件包裝在 Condition
的實例中. Read more about transactions here, 如果真的想用 "blocking pops", 這里墻裂建議你使用pub/sub的方式替代.
sub.Subscribe(channel, delegate {string work = db.ListRightPop(key);if (work != null) Process(work);
});
//...
db.ListLeftPush(key, newWork, flags: CommandFlags.FireAndForget);
sub.Publish(channel, "");
這實現了相同的目的,而不需要阻塞操作:
- data 沒有通過 pub/sub 的方式發送; pub/sub 只是用來通知worker去檢查隊列是否有數據
- 如果沒有worker,新的item 會存在list里面; 這項工作也不算是完全失敗
- 一個worker 只能 pop 一個值; 就算有很多的 consumer 消費這個隊列,其中的一些 consumer 被通知到也拿不到值去做接下來的事情
- 當你重啟一個worker時,你應該假定隊列中有 work 這樣可以消費積壓的work
- 但除此之外,語義與 blocking pops相同
StackExchange.Redis的復用特性使得使用普通的簡單代碼時,在一個連接上達到極高的吞吐量成為可能。.
Concurrency
pipeline / multiplexer / future-value 只會在 continuation-based 的異步代碼上起到較好的作用
string value = await db.StringGetAsync(key);
if (value == null) {value = await ComputeValueFromDatabase(...);db.StringSet(key, value, flags: CommandFlags.FireAndForget);
}
return value;
轉發請標注本文鏈接地址:(https://www.cnblogs.com/ArvinZhao/p/6825870.html)