Конвейерная обработка против дозирования в Stackexchange.Redis
Я пытаюсь вставить большое (-ish) количество элементов в кратчайшие возможные сроки, и я попробовал эти две альтернативы:
1) Конвейеризация:
List<Task> addTasks = new List<Task>();
for (int i = 0; i < table.Rows.Count; i++)
{
DataRow row = table.Rows[i];
Task<bool> addAsync = redisDB.SetAddAsync(string.Format(keyFormat, row.Field<int>("Id")), row.Field<int>("Value"));
addTasks.Add(addAsync);
}
Task[] tasks = addTasks.ToArray();
Task.WaitAll(tasks);
2) Пакетирование:
List<Task> addTasks = new List<Task>();
IBatch batch = redisDB.CreateBatch();
for (int i = 0; i < table.Rows.Count; i++)
{
DataRow row = table.Rows[i];
Task<bool> addAsync = batch.SetAddAsync(string.Format(keyFormat, row.Field<int>("Id")), row.Field<int>("Value"));
addTasks.Add(addAsync);
}
batch.Execute();
Task[] tasks = addTasks.ToArray();
Task.WaitAll(tasks);
Я не замечаю существенной разницы во времени (на самом деле я ожидал, что пакетный метод будет более быстрым): для примерно 250 тыс. вставок я получаю около 7 сек для конвейерной обработки и около 8 сек для дозирования.
Чтение из документации по конвейерной обработке,
"Использование конвейерной обработки позволяет нам получить оба запроса в сети немедленно, устраняя большую часть задержки. Кроме того, он также помогает уменьшить фрагментацию пакетов: 20 запросов, отправленных индивидуально (ожидание каждого ответа) потребует не менее 20 пакетов, но 20 запросы, отправленные в конвейере, могут входить в гораздо меньшее количество пакетов (возможно, даже один).
Для меня это звучит очень похоже на поведение пакетной обработки. Интересно, есть ли за кулисами какая-то большая разница между ними, потому что при простой проверке с procmon
я вижу почти то же количество TCP Send
в обеих версиях.
Ответы
Ответ 1
За кулисами SE.Redis выполняет небольшую работу, пытаясь избежать фрагментации пакетов, поэтому неудивительно, что в вашем случае это очень похоже. Основное различие между дозированием и плоской конвейерной обработкой:
- пакет никогда не будет чередоваться с конкурирующими операциями на одном и том же мультиплексоре (хотя он может чередоваться на сервере, чтобы избежать необходимости использовать транзакцию
multi
/exec
или Lua script)
- пакет всегда будет избегать случайных пакетов, поскольку он знает обо всех данных раньше времени
- но в то же время вся партия должна быть завершена до того, как что-либо может быть отправлено, поэтому для этого требуется больше буферизации в памяти и может искусственно вводить задержку.
В большинстве случаев вы будете делать лучше, избегая пакетной обработки, поскольку SE.Redis достигает большей части того, что он делает автоматически, просто добавляя работу.
В качестве окончательного замечания; если вы хотите избежать локальных накладных расходов, один заключительный подход может быть:
redisDB.SetAdd(string.Format(keyFormat, row.Field<int>("Id")),
row.Field<int>("Value"), flags: CommandFlags.FireAndForget);
Это отправляет все по проводам, не дожидаясь ответов и не выделяя неполные Task
для представления будущих значений. Возможно, вы захотите сделать что-то вроде Ping
в конце без огня и забыть, чтобы проверить, что сервер все еще разговаривает с вами. Обратите внимание, что использование fire-and-forget означает, что вы не заметите никаких сообщений об ошибках сервера.