流水线与多路复用
延迟很糟糕。 现代计算机以惊人的速度处理数据,高速网络(通常在重要服务器之间具有多个并行链接)提供了巨大的带宽,但是延迟意味着计算机要花费大量时间等待数据,这就是 continuation-based programming 变得越来越流行的几个原因之一。让我们看一下以下代码:
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]
在客户端角度,操作过程如下所示:
[req1]
[====waiting=====]
[resp1]
[req2]
[====waiting=====]
[resp2]
请谨记,这不是按比例缩放的。
流水线(Pipelining)
许多 redis 客户端都允许使用流水线。通过流水线发送多个消息,无需等待每个消息的回复,通常消息回复会在后续进行处理。在 .NET 中,可以通过基于 TPL 的 Task
/ Task<T>
APIs 创建未执行的 Task
。然后:
- 可调用
.Wait()
等待Task
完成。 - 可调用
.ContinueWith(...)
或者await
安排Task
的完成后执行的Task
。
例如,通过流水线获取两个字符串:
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个请求(等待每个响应)将至少需要20个数据包,但是在流水线中发送的20个请求使用更少的数据包(可能只用一个)。
Fire and Forget
流水线的一种特殊情况是我们明确不需要操作结果,不阻塞主程序,由后台排队处理。
// sliding expiration
db.KeyExpire(key, TimeSpan.FromMinutes(5), flags: CommandFlags.FireAndForget);
var value = (string)db.StringGet(key);
FireAndForget
标志让操作不阻塞主程序,并且立即返回默认值(无需理会)。这也适用于 *Async
方法:使用默认值返回一个已经完成的 Task<T>
。
多路复用(Multiplexing)
无论是单连接同步调用还是多连接并发调用,都会产生很多等待时间。而 StackExchange.Redis
通过多路复用把多个连接合并,利用流水线进行传输,有效利用空闲时间。无论是阻塞访问还是异步访问,都将通过流水线传输。
StackExchange.Redis
唯一没有实现 redis 特性:"blocking pops"(BLPOP,BRPOP 和 BRPOPLPUSH)。因为这允许单个调用者停止整个多路复用,从而阻塞 所有其他调用者。
StackExchange.Redis
唯一需要保留工作的时间是验证事务的前提条件,这就是为什么 StackExchange.Redis
将此类条件封装到内部托管的 Condition
实例中的原因。 查看更多事务(transactions)。 如果想要 "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, "");
这样就可以达到相同的目的,而无需阻塞操作。
- 数据不是通过 pub/sub 发送的;pub/sub API 仅用于通知工作程序检查更多工作。
- 如果没有工作程序,新工作保留在列表中;工作将不会执行。
- 只有一个工作程序可以 pop 一个值;当消费者比生产者多时,将通知一些消费者没有工作处理。
- 当您重新启动工作程序时,应该假设有工作,以便您处理所有积压工作。
- 除此之外,其语义与 "blocking pops" 相同
StackExchange.Redis
的多路复用在单个连接上达到极高的吞吐量。
并发(Concurrency)
流水线 / 多路复用 / 延迟值在 continuation-based asynchronous code 中也可以很好地发挥作用。 例如:
string value = await db.StringGetAsync(key);
if (value == null) {
value = await ComputeValueFromDatabase(...);
db.StringSet(key, value, flags: CommandFlags.FireAndForget);
}
return value;