当Rust遇见PostgreSQL,不仅是技术组合,更是成本革命
本期播客
当Rust遇见PostgreSQL,不仅是技术组合,更是成本革命
参考: https://kerkour.com/rust-postgres-everything
作为DBA,你可能每天都在为技术栈膨胀头疼:业务需要缓存上Redis,任务队列用RabbitMQ,领导选举靠etcd,再加上应用服务器和数据库……每个组件都要部署、监控、备份、调优。硬件成本飙升,运维复杂度爆炸。
有没有可能只用PostgreSQL + 一种语言,就搞定这一切?答案是肯定的,而且性能还能翻几倍。
最近一个真实案例:某后端服务原先用Go编写,处理批任务需要30分钟,至少4GB内存。重写为Rust并优化内存分配器后,同样任务5分钟内完成,内存只需512MB——性能提升6倍,内存占用减少87.5%!虽然架构调整贡献了一部分,但核心收益来自Rust的编译器和内存模型。更关键的是,空指针恐慌成为历史,绝大部分错误日志现在都来自外部服务,而不是代码缺陷。
Rust + PostgreSQL正在成为软件工程的“终局组合”:可靠、通用、快速、易学,且不受单一公司控制。对DBA而言,这意味着简化技术栈、降低硬件成本、提升运维敏捷性。
下面,我们从DBA视角拆解几个经过实战检验的模式,看看如何用Rust把PostgreSQL用到极致。
PostgreSQL 2026 年度大戏来了, 扫海报中的二维码报名, 选择早鸟或通票(都含午餐和周边礼品), 可私信我要优惠码, 数量有限先到先得!
模式一:抛弃ORM,拥抱sqlx
ORM在Rust里只会增加复杂度和体积。而sqlx在简单性、性能和功能间取得了完美平衡。
// 编译时检查SQL语法!
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(user_id)
.fetch_one(&pool)
.await?;
为什么DBA喜欢?
sqlx的宏能在编译期检查SQL语法和类型,无效查询根本过不了编译,杜绝线上语法错误。 原生支持连接池、类型转换,生成的代码直接调用PostgreSQL协议,无额外开销。
模式二:批量操作,用UNNEST一网打尽
sqlx 0.9+支持将迭代器绑定为数组,配合PostgreSQL的UNNEST,实现高效批量插入/更新。记住:单批不超过1万行,避免压垮数据库。
// 批量插入
const QUERY: &str = "INSERT INTO users (id, name)
SELECT * FROM UNNEST($1::UUID[], $2::TEXT[])";
sqlx::query(QUERY)
.bind(users.iter().map(|u| u.id))
.bind(users.iter().map(|u| u.name))
.execute(&db).await?;
// 批量更新
const QUERY: &str = "UPDATE users
SET name = user.name
FROM UNNEST($1::UUID[], $2::TEXT[]) AS user(id, name)
WHERE users.id = user.id";
sqlx::query(QUERY)
.bind(users.iter().map(|u| u.id))
.bind(users.iter().map(|u| u.name))
.execute(&db).await?;
原理:一次网络往返处理成千上万行,充分利用PostgreSQL的批处理能力。相比逐行操作,效率提升几个数量级。
模式三:领导选举?PostgreSQL advisory locks搞定
在水平扩展的架构中,有时需要确保某个任务(如定时清理)只有一个实例在执行。传统方案需要etcd或ZooKeeper,但PostgreSQL的咨询锁(advisory lock)就是为此而生。
pubasyncfntry_to_become_leader(pool: &Pool, lock_id: i64) -> Result<Connection, Error> {
loop {
letmut conn = pool.acquire().await?;
let is_leader: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock($1)")
.bind(lock_id)
.fetch_one(conn.as_mut())
.await?;
if is_leader {
returnOk(conn); // 成为领导者,连接必须保持
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
为什么可靠?
根据PostgreSQL锁管理机制,咨询锁完全在内存中,轻量且高效。只要持有锁的连接存活,锁就有效;连接关闭,锁自动释放。无需额外组件,领导选举一行SQL搞定。
模式四:用unlogged tables替代Redis
Redis很流行,但它是又一个有状态服务:估算内存困难,数据一致性需额外处理。对于低价值/临时数据(如会话缓存、计数),PostgreSQL的UNLOGGED表是完美替代。
CREATE UNLOGGED TABLE session_cache (
session_id TEXT PRIMARY KEY,
payload JSONB,
expires_at TIMESTAMPTZ
);
CREATEINDEXON session_cache (expires_at);
优势:
不写WAL:根据PostgreSQL存储管理,UNLOGGED表操作不记录WAL,写入速度提升数倍,且不产生需要vacuum的膨胀。 崩溃时不保证持久——这正是临时数据想要的特性:重启即清空,无需额外清理逻辑。 直接使用SQL:事务、索引、查询能力全保留,比Redis更灵活。
模式五:用PostgreSQL做任务队列(带SKIP LOCKED)
很多人怀疑PostgreSQL能否做队列,但只要用好SKIP LOCKED,它完全胜任。
表结构关键点:
主键用UUID v7:避免B-tree索引碎片(相比随机UUID) 状态用INT枚举:查询性能优于TEXT 关键索引: (scheduled_for)和(status)
CREATETABLE job_queue (
idUUID PRIMARY KEY,
scheduled_for TIMESTAMPTZ NOTNULL,
statusINTNOTNULL, -- 0=Queued, 1=Running, 2=Failed
payload JSONB NOTNULL
);
CREATEINDEXON job_queue (scheduled_for);
CREATEINDEXON job_queue (status);
拉取任务的核心SQL:
UPDATE job_queue
SETstatus = 1, updated_at = NOW()
WHEREidIN (
SELECTid
FROM job_queue
WHEREstatus = 0AND scheduled_for <= NOW()
ORDERBY scheduled_for
FORUPDATESKIPLOCKED
LIMIT $1
)
RETURNING *;
为何高效?
FOR UPDATE SKIP LOCKED:跳过已被其他worker锁定的行,避免竞争等待。结合PostgreSQL并发控制,实现高吞吐。ORDER BY scheduled_for:确保先执行最紧急的任务。索引覆盖:快速定位待处理任务。
第一性原理:为什么这些模式有效?
从PostgreSQL内核视角看:
WAL与持久化:UNLOGGED表通过绕过WAL换取速度,代价是崩溃不持久——这正是临时数据的物理特性需求。 锁机制:咨询锁和SKIP LOCKED充分利用了PostgreSQL已有的轻量锁和行级锁,无需额外组件。 MVCC:队列任务更新状态时,旧版本留在表中,但通过合适的索引和定期清理,可以控制膨胀。 进程模型:每个连接一个进程,配合连接池,Rust的异步运行时能高效复用。
Rust则通过零成本抽象和内存安全,将硬件效率推向极致,同时保证线程安全,减少DBA最头疼的“偶发性错误”。
边界条件:什么时候该收手?
没有银弹,这些模式也有局限:
如果条件崩塌:当数据量和并发超过PostgreSQL单机极限,可以:
水平分片:使用Citus等扩展 引入专用组件:但需权衡运维成本 考虑云原生数据库:如Snowflake、Neon(但放弃了“无供应商锁定”)
结语:简单即可持续
Rust + PostgreSQL的组合,本质上是将复杂性从基础设施层推回到应用层,而Rust正好有能力优雅地处理这种复杂性。对DBA而言,这意味着:
更少的组件:缓存、队列、锁服务都被吸收进数据库 更低的硬件成本:同样负载所需内存、CPU大幅下降 更高的可靠性:PostgreSQL的ACID和Rust的内存安全形成双重保障
在硬件价格只涨不跌的今天,这套组合可能是最具性价比的长期投资。