面向 Postgres 的开源 Durable Objects
摘要
Solid Objects 是一个开源库,为 PostgreSQL、SQLite 和 MySQL 实现了 durable objects 模型,提供有状态且有序的处理,无需供应商锁定或额外基础设施。
暂无内容
查看缓存全文
缓存时间: 2026/09/10 11:14
# 开源Postgres持久对象
来源:https://solidobjects.dev/blog/introducing-solidobjects
持久对象是一个优秀的模型。一个对象,一个身份,一次调用一个,且状态能超越请求生命周期。获取这个模型意味着需要接受其他方面。
- **厂商锁定**。该模型运行在单一厂商平台上。离开意味着需要重写那些当初使其值得采用的部分。
- **不可预测的定价**。每个操作都被计量。一位未上线的开发者在八天内因一个失控的告警循环被收取了34,000美元费用(https://news.ycombinator.com/item?id=47787042),既无用户也无任何预警。
- **需要运行守护进程**。像celld(https://github.com/denoland/celld)这样的自托管替代方案,将厂商替换为每个节点上的一个进程,外加一个用于复制的存储桶。
- **又一个有状态系统**。又需要备份、监控和恢复的东西。
Solid Objects为你提供了相同的模型,但作为一个库实现。每个对象都有一个身份、持久状态和有序邮箱,所有这些都存在于你已有的SQLite、PostgreSQL或MySQL数据库中。无需守护进程、无需代理服务器、无需账户。
我在Shopify发布其用于库存预留的相同架构(https://shopify.engineering/scaling-inventory-reservations)的两天前发布了第一个版本。得出了相同的结论,但彼此独立。
## 我为何构建这个
我维护的一个应用每五分钟运行一次定时任务。每次运行都加载数据库中所有活跃账户,检查是否有账户应被关停。一周下来,这导致了2014次运行和37分钟的队列时间。结果只发现了8个完整的账户。
在同一应用的其他地方,一个计划的发布任务在键值存储中为目标使用每个目标一个键。一个作业每半小时扫描所有目标,通过正则表达式解析键来恢复目标,且仍然可能延迟多达三十分钟。控制器会将该作业的第二个延迟副本入队以弥补时间差。每一个部分的添加都是为了弥补前一个部分的不足。
我一度认为这是本地问题,直到我读到Brian Chesky在一个标题为“当房东在Airbnb取消预订时会发生什么?”的帖子中回复。那其实不是取消。"房东没有取消,是我们双重预订了。"(https://news.ycombinator.com/item?id=7254986)一个整个产品都是预订的公司,竟然发布了我正在搭建脚手架试图防止的那个bug。
这让我面临两个都不喜欢的选项。迁移到Cloudflare持久对象,这是正确的模型,但要把状态、账单和离开的能力都交出去。或者继续保留扫描任务。
第三个选项其实就在我面前。截止时间已经在数据库里了。我也把计划任务移到了那里,这样每个实体在自身设置改变时就启动一个提醒,并在正确的时间唤醒自己。我并行运行旧的扫描任务,直到两者在生产环境中达成一致,然后删除了定时任务条目。
接着Shopify发布了相同的举措:将库存预留从Redis移到MySQL(https://shopify.engineering/scaling-inventory-reservations),每个单位一行,使用`SKIP LOCKED`获取。读到它是一种某种形式的 vindication。这不是一个被重新发现的老问题。它现在正冲击着人们。
## 它替代了什么
手动实现这个过程始于行锁,一旦行锁无法跨越两个请求,就需要Redis锁。过期需要一个延迟作业,而这需要一个`expires_at`列和一个清扫器,因为你发现它随着进程一起终止。然后是重试代码,以及可能与其跟随的写入不一致的广播。
六个必须保持一致的部分。Solid Objects用一个对象替代了它们。
## 今日可用的功能
**ticket-sale.ts**
```
import { Actor } from "solid-objects"
class TicketSale extends Actor {
static actorType = "TicketSale"
remaining = 100
reserve({ buyer }) {
if (this.remaining === 0) return false
this.remaining -= 1
this.schedule({ at: "10 minutes", key: `release:${buyer}` })
return true
}
release({ buyer }) {
this.remaining += 1
}
}
await TicketSale.ref("event-42").reserve({ buyer: "Ada" })
```
两个请求可以在同一时刻从两个进程调用`reserve`。两者都进入`event-42`的邮箱,运行时一次提交一个回合,因此计数永远不会低于零。十分钟的释放计划存储为一行,因此部署后它仍然会触发。
一个双人魔法牌桌。Alice和Bob各持一个座位,有生命值、牌库数量和各自战场上的卡牌。一张桌子,一个actor。双方座位的每次抓牌、横置和移动都进入同一个邮箱并按顺序提交,因此两个屏幕无法产生分歧。已上线运行于shuffleupandplay.com(https://shuffleupandplay.com/)。
## 回合如何工作
```
调用 ──▶ 邮箱 ──▶ 租约 ──▶ 回合 ──▶ 一个事务
有序 有围栏 │
按ID 按ID ├──▶ 状态
├──▶ 提醒
├──▶ 效果(发件箱)
└──▶ 广播
```
每个身份获得一个有序邮箱和一个有围栏的租约。一个进程赢得租约并运行回合,然后状态、提醒、效果和广播一起提交。失去租约的工作者无法提交。Redis是可选的,仅用于缩短唤醒延迟。
## 成本如何
在装有SQLite的笔记本电脑上,一个持久调用从入队到提交完成,在单个进程内的p50时间约为2.6毫秒。仅靠轮询跨两个进程时,等待时间约为一秒,这就是PostgreSQL通知和可选Redis唤醒存在的原因。
测试工具和注意事项详见docs/benchmarks.md(https://github.com/cardmagic/solid-objects-js/blob/main/docs/benchmarks.md)。这些是开发者笔记本电脑上的测量值。它们不预测应用容量。
## 安装
**终端**
```
# Node 24+
npm install solid-objects
npx solid-objects quickstart --yes
# Rails 7.1+
bundle add solid_objects
bin/rails solid_objects:install
```
无需守护进程、无需代理服务器、无需账户。安装会向你的数据库添加表,就像Solid Queue和Solid Cache那样。在Rails上,视图通过响应式ERB和Turbo Streams更新;在Node上,通过已提交的投影更新;在SQLite WASM上,浏览器运行时会将离线写入重放回服务器。
## 它不是什么
它没有边缘部署,没有区域间路由,没有跨两个身份的事务。交付是有序且至少一次的,因此外部效果必须是幂等的。没有恰好一次交付。它处于1.0版本之前。
它在一个拥有超过100,000用户的应用中生产运行。演示站点是shuffleupandplay.com(https://shuffleupandplay.com/),目前尚无第三方生产使用。如果你的整个不变量能在一个请求内完成,请使用事务。
## 下一步
可重复的基准测试,PostgreSQL和MySQL上更多的浸泡时间,然后是1.0版本。
← 所有文章(https://solidobjects.dev/blog/)· RSS(https://solidobjects.dev/feed.xml)
相似文章
无需 Cloudflare 的 Durable Objects,使用您已有的数据库
介绍 Solid Objects,一个开源库,它在现有 SQL 数据库上实现了 Cloudflare 的 Durable Objects 模型,让开发者可以将其作为自托管依赖使用,无需额外基础设施。
pg_durable: 微软开源数据库内持久化执行
微软开源了pg_durable,这是一个PostgreSQL扩展,支持长时间运行的SQL函数的持久化执行,具有自动检查点和容错恢复功能。
We're building Postgres in Rust. Using the LLVM of databases
Turso announces a project to build a modern, Rust-based version of Postgres using its core database engine, positioning itself as the LLVM of databases with features like self-updating materialized views.
Show HN: DBOSify – 基于Postgres的Temporal即插即用替代方案
DBOSify 是 Temporal Python 的即插即用替代方案,它使用 Postgres 替代 Temporal 服务器,无需额外基础设施即可实现持久化工作流。
PostgreSQL 适用于一切场景
本文认为,PostgreSQL 是一个功能全面的数据库解决方案,足以替代搜索引擎、消息队列和缓存等多种专用技术,从而简化 IT 架构。