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

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

Транзакции в потоках

Это руководство публикует группы сообщений в потоки Redis по принципу «всё или ничего».

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

Ответы пакета одним блоком​

src/main.rs
/// Confirms a batch of orders. The confirmations of one batch are published together or not at
/// all.
#[subscriber(RedisStream::new("orders").group("confirmations"), reply)]
async fn confirm(orders: &[Order]) -> Result<Vec<Confirmation>, HandlerOutcome> {
if orders.iter().any(|order| order.items.is_empty()) {
tracing::warn!(
orders = orders.len(),
"a batch with an empty order is dropped"
);
return Err(HandlerOutcome::drop());
}
Ok(orders
.iter()
.map(|order| Confirmation { order_id: order.id })
.collect())
}

Транзакция отправляет свои публикации одним блоком MULTI и EXEC в порядке публикации. Читатель confirmations видит либо все подтверждения пакета, либо ни одного.

confirm - пакетный обработчик: он принимает срез и возвращает по одному подтверждению на заказ. Err ничего не публикует и завершает весь пакет своим исходом.

Монтирование​

src/main.rs
fn service(broker: RedisBroker) -> impl App {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(broker, |b| {
b.include(confirm.batch(nonzero!(32)))
.out_reply(TransactionalPublish)
.transactional();
b.include(ship).out(Shipments, TransactionalPublish).build();
})
}

.out_reply(TransactionalPublish).transactional() запрашивает заимствованную транзакцию. Рантайм открывает транзакцию на издателе ответов, публикует в неё ответы пакета и фиксирует её. TransactionalPublish - имя политики RedisTransactionalPublish в прелюдии потоков. Она отправляет командой XADD, как Publish, и работает только там, где работают транзакции.

Размер пакета равен COUNT чтения, которое его получает, поэтому здесь одна транзакция содержит не больше 32 подтверждений.

Транзакция, которой владеет обработчик​

src/main.rs
/// Publishes one shipment per item of a parcel, committed as one block.
#[subscriber(RedisStream::new("packed").group("shipping"))]
async fn ship(
parcel: &Parcel,
Out(shipments): Out<impl OwnedTransactions, Shipments>,
) -> HandlerOutcome {
let Ok(mut transaction) = shipments.transaction().await else {
return HandlerOutcome::retry();
};
for item in &parcel.items {
if item.is_empty() {
tracing::warn!(parcel.order_id, "a nameless item, the parcel ships nothing");
// The abort discards the shipments buffered so far.
return match transaction.abort().await {
Ok(()) => HandlerOutcome::drop(),
Err(_) => HandlerOutcome::retry(),
};
}
let shipment = Shipment {
order_id: parcel.order_id,
item: item.clone(),
};
if transaction.message(&shipment).publish().await.is_err() {
return HandlerOutcome::retry();
}
}
match transaction.commit().await {
Ok(()) => HandlerOutcome::ack(),
Err(_) => HandlerOutcome::retry(),
}
}

transaction() на слоте Out возвращает значение, которое владеет своим буфером. Его публикации попадают в буфер, commit отправляет буфер одним блоком, а abort отбрасывает его. commit и abort поглощают значение, поэтому повторная фиксация или публикация после завершения не компилируется. На одном издателе можно одновременно открыть несколько таких транзакций, и завершение одной никогда не затрагивает другую. Транзакция, удалённая без commit и abort, отбрасывается как при abort, с предупреждением в журнале.

owned_transaction() делает то же над значениями: каждая публикация принимает место назначения и значение и кодирует его кодеком по умолчанию. Она подходит для хука запуска вроде after_startup, который наполняет поток начальными данными.

Что значит «нет отката»​

Redis ставит команды блока в очередь и выполняет их вместе, но не отменяет их. Команда, которую сервер отказывается поставить в очередь, например с неверным числом аргументов, отбрасывает весь блок, и commit возвращает ошибку. Команда, которая возвращает ошибку во время выполнения блока, оставляет команды перед ней применёнными. Для группы XADD это случается, когда у сервера кончается память или ключ хранит другой тип.

Неудачная фиксация уже поглотила свою транзакцию, и буфера больше нет. Обработчик возвращает retry, доставка приходит снова и строит новую транзакцию.

Где работают транзакции​

Транзакции работают на одиночном сервере и в развёртывании с Sentinel. Блок MULTI не может охватывать несколько хеш-слотов, поэтому в кластере сервис с политикой TransactionalPublish не запускается. У издателей списков и Pub/Sub транзакций нет.

Транзакция охватывает публикации. На стороне потребителя .pipeline().atomic() помещает собственные команды обработчика в Redis и подтверждение его доставки в один блок.

Тестирование​

Тесты в файлах примера работают внутри процесса, где Redis внутри процесса выполняет MULTI и EXEC. Они проверяют, что подтверждения пакета и отгрузки посылки приходят вместе, а отброшенный пакет и прерванная посылка ничего не публикуют. Ещё один тест запускает тот же сервис с брокером кластера и проверяет, что сервис не запускается.