本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。
流上的事务
本指南以全有或全无的方式把一组消息发布到 Redis 流。
示例是一个订单服务。 它成批确认订单,并把每个打好的包裹按每件商品一次发货的方式发出。
一批的回复放在一个块中
- 宏
- 手写
/// 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())
}
/// Confirms a batch of orders. The confirmations of one batch are published together or not at
/// all.
struct Confirm;
impl Handle<[Order], Vec<Confirmation>> for Confirm {
fn handle(
&self,
orders: &[Order],
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<Vec<Confirmation>, Vec<HandlerOutcome>>> {
if orders.iter().any(|order| order.items.is_empty()) {
tracing::warn!(
orders = orders.len(),
"a batch with an empty order is dropped"
);
return ready(Err(orders.iter().map(|_| HandlerOutcome::drop()).collect()));
}
ready(Ok(orders
.iter()
.map(|order| Confirmation { order_id: order.id })
.collect()))
}
}
事务把它的发布按发布顺序组成一个 MULTI 和 EXEC 块发送。
confirmations 的读者要么看到一批中的每条确认,要么一条也看不到。
confirm 是一个批处理器:它接收一个切片,为每个订单返回一条确认。
Err 不发布任何内容,并以它的结果结算整批。
挂载
- 宏
- 手写
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();
})
}
fn service(broker: RedisBroker) -> impl App {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(broker, |b| {
let confirm = subscriber(RedisStream::new("orders").group("confirmations"), Confirm);
b.include(confirm.batch(nonzero!(32)).reply().build())
.out_reply(TransactionalPublish)
.transactional();
let ship = subscriber(RedisStream::new("packed").group("shipping"), Ship);
b.include(ship.build())
.out(Shipments, TransactionalPublish)
.build();
})
}
.out_reply(TransactionalPublish).transactional() 要求使用借用形式。
运行时在回复发布者上打开一个事务,把这批的回复发布进去,然后提交。
TransactionalPublish 是 RedisTransactionalPublish 在流 prelude 中的名字。
它和 Publish 一样用 XADD 发送,但只在支持事务的部署上运行。
一批的大小就是拉取它的那次读取的 COUNT,所以这里一个事务最多包含 32 条确认。
处理器持有的事务
- 宏
- 手写
/// 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(),
}
}
/// Publishes one shipment per item of a parcel, committed as one block.
struct Ship;
impl<Slot> Handle<Parcel, (), Outs<(Slot,)>> for Ship
where
Slot: OutEntry<Shipments, Wire: OwnedTransactions>,
{
async fn handle(
&self,
parcel: &Parcel,
outs: &Outs<(Slot,)>,
_ctx: &mut Context<'_>,
) -> Result<(), HandlerOutcome> {
let mut transaction = outs
.get(Shipments)
.transaction()
.await
.map_err(|_| 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.
transaction
.abort()
.await
.map_err(|_| HandlerOutcome::retry())?;
return Err(HandlerOutcome::drop());
}
let shipment = Shipment {
order_id: parcel.order_id,
item: item.clone(),
};
transaction
.message(&shipment)
.publish()
.await
.map_err(|_| HandlerOutcome::retry())?;
}
transaction
.commit()
.await
.map_err(|_| HandlerOutcome::retry())
}
}
Out 槽位上的 transaction() 返回一个持有自己缓冲区的值。
它的发布进入缓冲区,commit 把缓冲区作为一个块发送,abort 丢弃缓冲区。
commit 和 abort 都会消耗这个值,所以第二次提交或结束后的发布无法编译。
一个发布者上可以同时打开多个持有型事务,结算其中一个从不影响其他事务。
既没有提交也没有中止就被丢弃的事务,按中止处理,并在日志中留下一条警告。
owned_transaction() 是同样的东西,只是针对值:每次发布接收一个去向和一个值,并用默认编解码器对值编码。
它适合在启动钩子中使用,例如在 after_startup 中为流写入初始数据。
“没有回滚”的含义
Redis 把一个块中的命令排队,然后一起执行;它不会撤销这些命令。
服务器拒绝入队的命令,例如参数个数错误的命令,会让整个块作废,commit 返回一个错误。
在块执行过程中失败的命令,不会撤销它之前已执行的命令。
对于一组 XADD,这发生在服务器内存耗尽,或者某个键保存的是另一种类型的时候。
提交失败的事务已经被消耗,缓冲区也没有了。
处理器返回 retry,投递再次到来,并构建一个新的事务。
事务在哪里运行
事务在单机服务器和 Sentinel 部署上运行。
MULTI 块不能跨越哈希槽,所以在集群上,挂载了 TransactionalPublish 的服务无法启动。
列表和 Pub/Sub 的发布者没有事务。
事务覆盖的是发布。
在消费一侧,.pipeline().atomic() 把处理器自己的 Redis 命令和其投递的确认放在一个块中。
测试
示例文件中的测试在进程内运行,进程内 Redis 会执行 MULTI 和 EXEC。
它们检查一批的确认和一个包裹的各次发货是一起到达的,并检查被丢弃的一批和被中止的包裹不发布任何内容。
另一个测试用集群 broker 启动同一个服务,并检查它无法启动。