在 Spanner 等分布式云数据库中构建可靠的消息队列和微服务会面临独特的挑战。本文档介绍了用于在 Spanner 队列中执行“恰好一次”处理和“最多一次”确认的核心概念和设计模式。
核心概念和幂等性困境
“幂等性”是指无论执行操作多少次,结果始终相同。在构建消息队列或工作交付系统时,有三种主要交付方法可解决幂等性问题:
- At-least-once::保证消息能够传送并得到处理。如果发生网络故障,消息可能会被多次传送和处理。
- At-most-once::消息最多传送一次。可防止重复处理,但如果发生故障,消息可能会丢失或丢弃。
- 正好一次:每条消息都只处理一次,既不会丢失也不会重复。
Spanner 队列会自动提供至少一次传送和最多一次确认。不过,您可以使用以下各部分中介绍的设计模式和示例,以编程方式减少重复处理和重新传送。
未知提交状态问题
在单机数据库中,事务要么成功,要么失败。在分布式云数据库中,当数据在多个物理数据中心之间复制时,可能会丢失事务。
当您的应用处理消息并提交确认(例如 DELETE FROM MessageQueue WHERE message_id = '123' ASSERT_ROWS_MODIFIED 1)时,请求会经过多个网络跃点:
[Your App] <--(gRPC)--> [Spanner API] <--(Google network)--> [Spanner Storage]
当 Spanner 存储空间领导者收到提交时,它会在存储空间副本之间进行同步。在达成共识并将数据写入磁盘后,交易会在数据库中成功提交。
不过,在数据提交到磁盘后,但在成功确认到达应用之前,可能会发生暂时性网络故障(例如代理重启或网络断开)。如果发生这种情况,您的应用会收到网络超时错误(DEADLINE_EXCEEDED 或 UNAVAILABLE)。
由于连接是带外中断的,因此应用无法知道连接是在提交成功之前还是之后中断的。这称为“未知提交状态”问题。
为什么自动重试可能存在风险
如果您的应用拦截到网络超时错误,并在未进行重复数据删除的情况下重试完全相同的交易,则可能会导致业务逻辑执行两次。例如,如果交易涉及向客户的信用卡扣款或配送商品,那么重试成功(但未确认)的交易会导致重复扣款或配送。
为了保护用户,官方 Google Cloud 客户端库绝不会自动重试因未知提交错误而失败的事务。它们会抛出明确的错误,提醒您交易结果未知,因此需要您的应用代码使用幂等性模式安全地处理重试。
一次性处理的实际策略
为了安全地重试交易并减少重复处理和消息重新传送,请实现以下主要设计模式之一。
断言已修改的行
如果您的所有业务逻辑(例如更新其他表)和队列消息确认都发生在单个 Spanner 事务中,您可以在 DELETE 语句中使用 ASSERT_ROWS_MODIFIED。这是最简单的“正好一次”处理策略,因为它不需要单独的去重表。
通过将 ASSERT_ROWS_MODIFIED 1 附加到语句中,只有在成功删除一条消息时,确认才会成功。如果发生网络故障,并且您的应用重试了交易,则消息不再位于队列中。重试会删除 0 行,并且语句会因 OUT_OF_RANGE 而失败。
OUT_OF_RANGE 错误是永久性的:相应行已消失,因此重新执行语句始终会修改 0 行并再次失败。它也是语句级的:只有 DELETE 会失败。事务保持打开状态,您已在其中进行的所有写入操作仍会缓存,如果您继续操作,这些写入操作将会提交。Spanner 不会为您中止或回滚事务。为了获得预期的“正好一次”保护,您必须确保事务回滚:
- 让错误传播:如果您使用基于 runner 的 API(例如 Go 中的
ReadWriteTransaction),请让OUT_OF_RANGE错误从事务函数中传播出来。客户端库将放弃事务并将其回滚,确保不会提交任何业务逻辑写入。 - 不捕获并继续:切勿在交易函数内捕获断言错误并继续。如果这样做,Spanner 仍会提交其他语句,从而导致此模式旨在防止的完全重复更新。
- 手动回滚:如果您手动管理事务(例如,使用 Go 中的
ReadWriteStmtBasedTransaction或 REST/gRPC API),则必须在断言失败时显式调用相应的回滚方法。
事务性发件箱
如果您的业务逻辑无法在单个 Spanner 事务中完成(例如多步工作流或跨多个系统的更新),或者消息处理需要调用外部服务(例如发送短信或处理付款),则无法以原子方式将业务逻辑与队列确认相关联。
在这些情况下,切勿在 Spanner 事务块内直接调用外部 API 或执行非事务性操作。如果 Spanner 重试或中止事务,您的应用可能会多次执行该业务逻辑。
您的应用应实现自己的幂等性策略,但您可以使用以下模式来减少重复工作:
快速操作(在 10 秒的默认租约期内完成):在收到消息时执行业务逻辑或外部 API 调用。仅在操作成功后确认消息。如果操作失败,或者工作器在确认之前崩溃,请勿确认消息;租约会过期,Spanner 会自动传送消息以供重试。
长时间运行的操作(耗时超过 10 秒):在收到消息时确认消息,并在同一事务中重新发送计划在未来传送的新消息(使用大于估计执行时间的传送延迟时间)。或者,使用
RENEWLEASE_QUEUE_NAME()TVF 定期延长消息租期。当业务逻辑或外部调用成功时,确认新入队或延期的消息。
多路复用会话幂等性
Spanner 会话可以多路复用。多路复用会话可在共享会话中跟踪内存中的事务状态。这样一来,客户端库便可更可靠地重新连接并解决未知的提交状态,从而防范客户端网络故障。
不过,如果前端服务器崩溃,多路复用会话无法消除未知的提交结果。如果托管会话事务表的特定服务器在提交后立即重启,则内存中状态会丢失,并在重新连接时返回未知事务结果错误。为应对此风险,请实现本页中的幂等性模式。
后续步骤
- 了解如何使用 Spanner 队列,包括最佳实践和监控。
- 探索更多 Spanner 队列场景和示例。
- 使用队列的精细访问权限控制来配置访问权限控制。