手册

数据库支持租赁作品 (Db Backed Jobs With Leases)

带条件更新的租赁,可以挽救陷入困境的工作的观察者,以及为什么仅仅取消消息不足以支付生产费用。

分布式支付引擎

部分 13 的 22

一系列分布式支付架构弥合了捕获和完成之间的差距。

Distributed payment engine architecture diagram

我们在上一节中建立了重试算法;但该算法假设作业是由哪个工人运行的。如果多个worker从同一个作业队列中拉取,同一个支付作业可以同时由两个worker处理吗?

消息代理通过“可见性超时”或“ack/nack”解决了这个问题。但在数据库支持的作业队列中(许多支付系统中的首选方法,因为作业状态已经存在于数据库中)通过租赁模式提供相同的保证。```text Worker A Worker B │ SELECT ... FOR UPDATE? │ │ ya da conditional UPDATE │ ▼ ▼ Job'ı kilitlemeye çalışır Job'ı kilitlemeye çalışır → sadece biri kazanır


## 第一次提到的概念```text
📦 Lease
Bir worker'ın bir işi belirli bir süre için 'sahiplendiğini' işaretleyen zaman damgalı kayıt.

📦 Conditional UPDATE
Sadece beklenen koşul (örn. status = Pending) doğruysa satırı güncelleyen atomik SQL işlemi.

📦 Lease süresi (Lease TTL)
Bir worker'ın işi ne kadar süreyle sahiplenebileceğinin üst sınırı; bu süre geçince iş tekrar alınabilir hale gelir.

📦 Stuck Watcher
Lease'i süresi dolmuş ama tamamlanmamış işleri periyodik olarak tarayıp yeniden kuyruğa alan arka plan süreci.
```租约不是锁;如果持有锁的进程崩溃,锁可能会无限期地保留。由于租约是有期限的,即使worker崩溃了,作业也会在一段时间后自动释放。

## 获得租约的原子方式

读取然后更新(读然后写)以检索作业会出现竞争条件:两个工作人员可能读取同一行,都看到“空”,都尝试更新。正确的方法是将读取和条件移动到同一个原子表达式中:```sql
UPDATE payment_jobs
SET status = 'Processing',
    lease_owner = :workerId,
    lease_until = now() + interval '60 seconds'
WHERE id = :jobId
  AND (status = 'Pending' OR (status = 'Processing' AND lease_until < now()))
RETURNING id;
```如果此 UPDATE 未返回任何行,则该作业已经在另一个工作进程中或仍处于有效租约之下 — 该工作进程会默默地继续执行下一个作业。如果返回该行,则该工人现在是该作业的唯一所有者;直到租约到期。

## 为什么要通过心跳延长租期?

固定租用期(例如 60 秒)可能不足以满足每笔交易。可能需要很长时间的操作(例如 PSP 调用变得意外地长)应该在租约到期之前使用心跳进行扩展:```text
Worker işe başlar → lease_until = now + 60s
  ... iş sürüyor ...
Worker heartbeat gönderir → lease_until = now + 60s (yenilenir)
  ... iş tamamlanır ...
Worker status = Completed olarak işaretler
```无法发送心跳(关闭、与网络断开)的工作人员无法续订租约;时间到期,该工作将再次可用。这是确保自动处理崩溃场景的机制。

## 卡住的观察者:谁检测到租约过期的工作

当租约到期时,该作业不会自动“恢复”——工作人员必须再次 `SELECT` 。因此,定期运行的观察程序进程会扫描租约已过期但仍处于 `Processing` 状态的作业,并使它们可检索(或将它们直接分配给新工作人员)。```text
Watcher (her 30 saniyede bir)
  SELECT id FROM payment_jobs
  WHERE status = 'Processing' AND lease_until < now()
  → bu işler 'stuck' olarak işaretlenir veya doğrudan Pending'e döndürülür
```如果没有 Watcher,崩溃的工人留下的工作可能会永远处于“处理中”状态;付款永远不会在没有人注意到的情况下完成。

## 为什么经纪人的 Nack 单独是不够的

如果消息代理上的工作人员 `Nack` 消息(或可见性超时到期),则消息将返回到队列。这与数据库中的租约非常相似,但有两个重要的区别很重要:首先,代理自己的可见性窗口通常与业务状态的持久记录不同步(消息可能会丢失,或者被传递两次);其次, `Nack` 只是说“留下此消息”,永远不知道*在哪个阶段*该作业被卡住或已经尝试了多少次。数据库支持的租约将作业状态和试用历史记录保持在相同的事务限制内,并且可查询。

## 经常混淆的区别```text
❌ Lease = kilit (lock)
✓ Lease süresi dolan bir zaman sınırıdır; kilit process çökerse sonsuza kalabilir

❌ Read-then-write yeterlidir
✓ Read-then-write yarış durumuna açıktır; conditional UPDATE atomik olmalıdır

❌ Nack, lease'in yerini tamamen tutar
✓ Nack mesaj görünürlüğünü yönetir; lease iş durumunu ve deneme geçmişini kalıcı olarak tutar
```## DB支持的租约和代理可见性超时的比较

|标准|经纪商可见性超时 | DB支持的租赁|
| --- | --- | --- |
|状态疑问|有限公司|完整的 SQL |
|试用历史持久化 |取决于经纪人 |轻松同线|
|检测卡住的作业 |间接 |直接查询|

## 设计租赁时的清单

1. 租约是单个原子 `UPDATE ... WHERE` 语句还是先读后写?
2. 租用期限真的比最长预计处理时间长吗?
3.对于可能需要很长时间的交易,是否有心跳续租?
4.Stuck 观察者是否定期工作,或者租约已过期的作业是否永远保持“处理中”状态?
5. `lease_owner` 字段是否存储哪个工作实例接收作业以供诊断?
6. 试验次数是否会随着每次租赁而增加并永久保留?

## 本文中您应该记住的内容

1、租约不是锁;这是有时间限制的所有权主张并自动终止。
2. 获得租约需要原子条件UPDATE;先读后写很容易出现竞争条件。
3、长交易必须用心跳续租;否则租约可能会提前终止。
4.Stuck watcher 是一个强制后台进程,用于恢复崩溃的工人留下的工作。

> 作业队列的可靠性并不理想;当工人在中间崩溃时进行测试。

在下一节中,我们将研究建立在这种租赁机制之上的一类工作人员:协调工作人员,它在 PSP 上取得了成功,但改善了系统中仍悬而未决的付款。

FAQ

Frequently asked questions

什么是租赁?

带时间戳的记录,标记工人在特定时间段内“拥有”一份工作。

什么是条件更新?

仅当预期条件(例如状态 = Pending)为真时才更新行的原子 SQL 操作。

“租赁=锁定(lock)”是否正确?

租约是有期限的;如果进程崩溃,锁可能会永远存在

本节修复了什么?

消息代理通过“可见性超时”或“ack/nack”解决了这个问题。但在数据库支持的作业队列中(许多支付系统中的首选方法,因为作业状态已经存在于数据库中)通过**租赁**模式提供相同的保证。租约不是锁;这是有时间限制的所有权主张并自动终止。我们在上一节中建立了重试算法;但该算法假设作业是由*哪个工人*运行的。如果多个worker从同一个作业队列中拉取,同一个支付作业可以同时由两个worker处理吗?

学到的工程原理

  • 租约不是锁;有时间限制,自动结束。
  • 获取租约需要原子条件更新,而不是先读后写。
  • 如果没有卡住的观察者,崩溃的工人可能会永远失去工作。

继续阅读

继续阅读

系列中的下一个

随笔

付款对账工人施工

扫地机如何改善漂移:虽然PSP成功,但本地注册可能会过期;如何恢复老化的 FinalizePending。

系列中的下一个

同系列

Paylaş