Перейти к основному содержимому
Нормативная версия документации - английская

Эта страница переведена с английского языковой моделью. При любом расхождении верен английский оригинал.

Выбор транспорта

Это руководство сравнивает три структуры Redis, которые читает сервис: поток, список и канал Pub/Sub.

Пример - сервис магазина, который работает со всеми тремя. Счета по заказам выставляются из потока, миниатюры строятся из списка, а изменения цен приходят по каналу.

Потоки: надёжный журнал​

src/main.rs
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "orders")]
struct Order {
id: u64,
amount_cents: u64,
}

#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "invoices")]
struct Invoice {
order_id: u64,
amount_cents: u64,
}

#[subscriber(RedisStream::new("orders").group("billing"), reply)]
async fn bill(order: &Order) -> Invoice {
tracing::info!(order.id, "order billed");
Invoice {
order_id: order.id,
amount_cents: order.amount_cents,
}
}

Поток Redis хранит каждую запись, пока сервис её не обрежет. RedisStream читает поток через группу потребителей. Каждая группа получает каждую запись один раз, а внутри группы её обрабатывает один потребитель. Подтверждённая запись получает XACK. Запись, которую потребитель не подтвердил, остаётся в списке неподтверждённых записей группы, поэтому сбой ничего не теряет. Поток можно прочитать заново с более ранней записи через перемотку.

Заказы - это деньги, поэтому они идут через поток. bill отвечает на каждый заказ счётом Invoice, который попадает в поток invoices.

Списки: очередь заданий​

src/main.rs
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "thumbnails")]
struct Thumbnail {
image: String,
}

#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "thumbnails.ready")]
struct ThumbnailReady {
image: String,
}

#[subscriber(RedisList::new("thumbnails").reliable(), reply)]
async fn render(job: &Thumbnail) -> ThumbnailReady {
tracing::info!(job.image, "thumbnail rendered");
ThumbnailReady {
image: job.image.clone(),
}
}

Список Redis - это очередь: производитель добавляет запись, и ровно один потребитель её извлекает. RedisList::new извлекает запись командой BRPOP. Запись покидает Redis до запуска обработчика, и сбой в обработчике её теряет. reliable вместо этого переносит запись в список обработки и удаляет её только после подтверждения обработчиком. У списка нет групп и нет повторного чтения.

Построение миниатюры - работа для одного воркера, поэтому задания идут через надёжный список. render объявляет о каждой готовой миниатюре в канале thumbnails.ready.

Pub/Sub: широковещательная рассылка​

src/main.rs
#[derive(Debug, PartialEq, Deserialize, Serialize, Outgoing)]
#[outgoing(name = "prices")]
struct PriceChange {
sku: String,
price_cents: u64,
}

#[subscriber(RedisPubSub::new("prices"))]
async fn refresh_price(change: &PriceChange) -> HandlerOutcome {
tracing::info!(change.sku, change.price_cents, "price cache refreshed");
HandlerOutcome::ack()
}

Сообщение Pub/Sub получает каждый подписчик, подключённый в момент публикации. Redis ничего не хранит: подписчик, который не подключён, сообщение не увидит. У RedisPubSub нет подтверждения, поэтому исход, который возвращает обработчик, ничего не завершает.

Изменение цены важно только экземплярам, которые работают сейчас и обновляют свой кеш, поэтому оно идёт через канал.

Сравнение​

ПотокСписокPub/Sub
Хранится, пока никто не читаетпока не обрезанпока не извлечённет
Кто получает сообщениеодин потребитель в каждой группеодин потребителькаждый подключённый подписчик
ackXACKнадёжный: LREM из списка обработки; простой: ничегоничего
retryкопия, добавленная в потокнадёжный: обратно в список; простой: теряетсятеряется
retry_afterкопия после задержкикопия после задержкикопия после задержки
Пакетодно чтение до n записейсобирает клиентсобирает клиент
Повторное чтениеперемотка группынетнет

Страницы о подтверждении описывают каждый исход полностью, а руководство Переобработка, задержки и dead-letter показывает, как работают задержка и предел попыток на каждой структуре. Пакет на потоке - это COUNT одного чтения. Список и канал доставляют по одной записи, поэтому пакет того же размера собирает клиент.

Одно имя занимает одна структура. Поток и список - это ключи определённого типа, и запись другого типа возвращает ошибку WRONGTYPE. Канал вообще не ключ: XADD под именем канала создаёт поток, которого подписчики канала никогда не увидят.

Один сервис, три транспорта​

src/main.rs
#[ruststream::app]
fn app() -> impl App {
RustStream::new(AppInfo::new("shop", "0.1.0")).with_broker(
RedisBroker::standalone("redis://localhost:6379"),
|b| {
b.include(bill).out_reply(stream::Publish);
b.include(render).out_reply(pubsub::Publish::default());
b.include(refresh_price);
},
)
}

Сервис на одном транспорте импортирует прелюдию этого транспорта, например ruststream_fred::stream::prelude. Этот сервис сочетает транспорты, поэтому импортирует ruststream_fred::prelude. В ней политика публикации каждого транспорта сохраняет имя своего модуля: stream::Publish пишет командой XADD, pubsub::Publish командой PUBLISH. Тип ответа задаёт, куда уходит ответ, а политика в точке монтирования задаёт, как он туда попадает.

Три обработчика устроены одинаково. Чтобы перенести обработчик на другой транспорт, меняют дескриптор в его атрибуте и политику в его точке монтирования, а тело остаётся прежним.

Тесты в файле примера публикуют в каждый транспорт и читают то, что отправили в ответ обработчики, и всё это внутри процесса.