面向 Postgres 的开源 Durable Objects

Hacker News Top 工具

摘要

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)

相似文章

PostgreSQL 适用于一切场景

Hacker News Top

本文认为,PostgreSQL 是一个功能全面的数据库解决方案,足以替代搜索引擎、消息队列和缓存等多种专用技术,从而简化 IT 架构。