文章目录
- [Rust + PostgreSQL 极简技术栈应用开发](#Rust + PostgreSQL 极简技术栈应用开发)
-
- 核心思路
- 环境准备
- 模块一:替代缓存
-
- 新建业务表与物化视图
- 缓存刷新
- [Axum 接口调用缓存](#Axum 接口调用缓存)
- 模块二:替代消息队列
- 模块三:替代搜索中间件
-
- 搜索表与索引设计
- [Axum 搜索接口实现](#Axum 搜索接口实现)
- 架构优势与局限
- 总结
Rust + PostgreSQL 极简技术栈应用开发
在中小规模后端开发场景中,我们常常会不自觉地陷入中间件堆砌的困境,即便业务逻辑并不复杂、并发量也未达到需要多中间件分流的程度,依然会习惯性地引入各类第三方中间件:用Redis 来缓存热点数据、RabbitMQ 来处理异步任务等等。这种堆砌现象,除了部分开发者对架构完善的认知偏差,更多时候还带有明显的面向面试编程成分,很多开发者在学习和实践中,过度侧重面试高频考点,盲目照搬大厂高并发场景下的架构方案,却忽略了中小团队、独立开发者的核心需求:简洁、可靠、易维护、低成本。最终导致项目部署复杂、维护成本飙升,排查问题时需要跨多个中间件定位,反而降低了开发效率和系统稳定性。
本文将实操演示如何用 PostgreSQL 替代缓存、消息队列、定时器、搜索、分布式锁等常用中间件,基于 Axum + SQLx + PostgreSQL 技术栈,打造极简且可靠的后端架构,适合中小团队、独立开发者快速落地。
核心思路
摒弃一个场景一个中间件的传统思路,利用 PostgreSQL 的以下特性替代对应中间件:
- 缓存:利用 PostgreSQL 的 Buffer Cache 机制 + pg_prewarm 扩展预热缓存,配合物化视图实现结果缓存,替代 Redis 轻量级缓存场景。
- 消息队列:用 PostgreSQL 表模拟队列,结合
SELECT FOR UPDATE SKIP LOCKED实现并发安全消费,搭配 pg_notify 实现消息通知,替代 RabbitMQ 轻量级队列场景。 - 搜索:利用 PostgreSQL 内置的全文搜索功能(tsvector + tsquery),配合全文索引,替代 Elasticsearch 等轻量级搜索中间件。
- 分布式锁:借助 PostgreSQL 的 pg_advisory_lock 函数或表级锁,实现分布式锁,替代 Redis 分布式锁。
这种架构的优势的是所有的操作都围绕 PostgreSQL 展开,无需跨组件同步数据,依托 PostgreSQL 的 ACID 事务保障数据一致性,同时减少中间件部署和维护成本。
环境准备
初始化项目与依赖
使用 Cargo 新建项目:
shell
cargo new rust-postgres-demo
cd rust-postgres-demo
在 Cargo.toml 中添加上依赖:
toml
[dependencies]
axum = { version = "0.8", features = ["json"] }
sqlx = { version = "0.8", features = [
"runtime-tokio-rustls",
"postgres",
"uuid",
"chrono",
"json",
"migrate",
] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["serde", "v4"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenvy = "0.15"
PostgreSQL 扩展安装
在这次项目中我们需要用到以下几个扩展,执行以下 SQL 命令(需超级用户权限):
sql
-- 用于生成 UUID
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 用于缓存预热,将表数据加载到 Buffer Cache
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
-- 用于查看缓存状态
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
初始化代码
在根目录创建 .env 文件并配置数据库连接地址:
plaintext
DATABASE_URL=postgres://postgres:password@localhost:5432/rust-postgres-demo
编辑 src/main.rs:
rust
use anyhow::Result;
use axum::Router;
use sqlx::PgPool;
use sqlx::postgres::PgPoolOptions;
use std::time::Duration;
#[derive(Clone)]
struct AppState {
pool: PgPool, // 业务连接池
}
// 初始化全局状态
async fn init_app_state() -> Result<AppState> {
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool = Arc::new(init_pg_pool(&database_url).await?);
Ok(AppState { pool })
}
// 初始化 PostgreSQL 连接池
async fn init_pg_pool(database_url: &str) -> Result<PgPool> {
let pool = PgPoolOptions::new()
.max_connections(20) // 最大连接数,根据业务并发量调整
.min_connections(5) // 最小空闲连接,保障业务响应速度
.acquire_timeout(Duration::from_secs(3)) // 连接获取超时,避免业务阻塞
.idle_timeout(Duration::from_secs(600)) // 空闲连接超时,释放闲置资源
.connect(database_url)
.await?;
Ok(pool)
}
#[tokio::main]
async fn main() -> Result<()> {
dotenvy::dotenv().ok();
let app_state = init_app_state().await?;
let app = Router::new().with_state(app_state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
模块一:替代缓存
对于传统缓存场景(如热点数据查询、接口结果缓存),可以直接使用 PostgreSQL 的 Buffer Cache 和物化视图即可实现,配合 pg_prewarm 预热缓存。
PostgreSQL 的 Buffer Cache 是服务器共享内存中的核心组件,用于存储关系页,平衡磁盘(毫秒级)和内存(纳秒级)的访问时间,只要数据页被缓存,后续访问无需磁盘操作,性能极高。我们可以通过 pg_prewarm 扩展主动将热点表加载到 Buffer Cache,再用物化视图存储复杂查询的结果,避免重复计算。
新建业务表与物化视图
sql
-- 用户表(核心业务表)
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 物化视图,缓存用户列表结果
CREATE MATERIALIZED VIEW IF NOT EXISTS mv_user_list AS
SELECT id, username, email FROM users ORDER BY created_at DESC;
-- 为物化视图创建索引,提升查询性能
CREATE INDEX IF NOT EXISTS idx_mv_user_list_id ON mv_user_list(id);
PostgreSQL 的物化视图(Materialized View)和普通视图(View)的核心区别在于数据存储与更新机制:普通视图是虚拟的,不存储数据,每次查询动态计算;物化视图实际存储查询结果,像表一样占用磁盘空间,需手动刷新以同步数据。物化视图适合需要快速读取复杂查询结果的场景(用空间换时间),而普通视图保证了实时性。
缓存刷新
缓存预热、定时刷新属于后台任务,你可以使用 crontab 定时触发,也可以手动触发:
rust
// 缓存预热:将物化视图加载到 Buffer Cache
async fn warm_up_cache(pool: &PgPool) -> Result<()> {
sqlx::query!("SELECT pg_prewarm('mv_user_list'::regclass, 'buffer')")
.fetch_one(pool)
.await?;
Ok(())
}
// 刷新物化视图,手动触发
async fn refresh_user_cache(pool: &PgPool) -> Result<()> {
sqlx::query!("REFRESH MATERIALIZED VIEW mv_user_list")
.execute(pool)
.await?;
Ok(())
}
Axum 接口调用缓存
接口查询属于核心业务,使用业务连接池,保障接口响应速度:
rust
use axum::{Json, extract::State};
use serde::Serialize;
// 定义用户响应结构体
#[derive(Serialize, sqlx::FromRow)]
struct UserDto {
id: uuid::Uuid,
username: String,
email: String,
}
// 接口:查询用户列表,从缓存中获取
async fn get_user_list(State(state): State<AppState>) -> Result<Json<Vec<UserDto>>, axum::Error> {
let users = sqlx::query_as!(
UserDto,
r#"
SELECT
id as "id!",
username as "username!",
email as "email!"
FROM mv_user_list
LIMIT 20
"#
)
.fetch_all(&state.pool)
.await
.map_err(|e| axum::Error::new(e))?;
Ok(Json(users))
}
模块二:替代消息队列
轻量级消息队列场景,比如异步通知、任务分发,用 PostgreSQL 表模拟队列,结合 SELECT FOR UPDATE SKIP LOCKED 实现并发安全消费,搭配 pg_notify 实现消息实时通知,
队列表设计
sql
-- 消息队列表
CREATE TABLE IF NOT EXISTS message_queue (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
queue_name VARCHAR(50) NOT NULL, -- 队列名称(支持多队列)
payload JSONB NOT NULL, -- 消息内容(JSON 格式,适配各类消息)
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 消息状态:pending/processing/completed/failed
retry_count INT NOT NULL DEFAULT 0, -- 重试次数
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 索引:提升消息查询、消费效率
CREATE INDEX IF NOT EXISTS idx_queue_name_status ON message_queue(queue_name, status);
生产者:发送消息
消息发送属于接口业务,使用业务连接池,保障接口响应速度:
rust
// 消息请求结构体
#[derive(Deserialize)]
struct SendMessageRequest {
queue_name: String,
payload: serde_json::Value,
}
// 接口:发送消息,生产者
async fn send_message(
State(state): State<AppState>,
Json(req): Json<SendMessageRequest>,
) -> Result<Json<uuid::Uuid>, axum::Error> {
// 插入消息到队列表
let message = sqlx::query!(
r#"
INSERT INTO message_queue (queue_name, payload)
VALUES ($1, $2)
RETURNING id
"#,
req.queue_name,
req.payload
)
.fetch_one(&state.pool)
.await
.map_err(|e| axum::Error::new(e))?;
// 发送 pg_notify 通知,告知消费者有新消息
sqlx::query!(
"SELECT pg_notify($1, $2)",
req.queue_name, // 通知通道(与队列名称一致)
message.id.to_string() // 消息 ID(作为 payload)
)
.fetch_one(&state.pool)
.await
.map_err(|e| axum::Error::new(e))?;
Ok(Json(message.id))
}
消费者:处理消息
消息消费属于核心业务异步处理,使用业务连接池,确保与业务数据操作的一致性:
rust
use sqlx::postgres::PgListener;
// 消息处理函数(可根据业务自定义)
async fn handle_message(payload: serde_json::Value) -> Result<()> {
// 模拟消息处理:打印消息内容
println!("处理消息:{}", serde_json::to_string(&payload)?);
// 实际业务:发送邮件、异步更新数据等
Ok(())
}
// 消费者:监听队列,处理消息
async fn message_consumer(state: AppState, queue_name: String) -> Result<()> {
// 创建 PgListener,监听指定通道
let mut listener = PgListener::connect_with(&state.pool).await?;
listener.listen(&queue_name).await?;
println!("消费者已启动,监听队列:{}", queue_name);
// 循环监听通知
loop {
match listener.recv().await {
Ok(notification) => {
// 解析消息 ID,获取消息详情
let message_id = notification.payload().parse::<uuid::Uuid>()?;
let mut tx = state.pool.begin().await?;
// 锁定消息(SKIP LOCKED 避免锁竞争),标记为 processing
let message = sqlx::query!(
r#"
SELECT id, payload FROM message_queue
WHERE id = $1 AND status = 'pending'
FOR UPDATE SKIP LOCKED
"#,
message_id
)
.fetch_optional(&mut *tx)
.await?;
if let Some(msg) = message {
// 处理消息
match handle_message(msg.payload).await {
Ok(_) => {
// 处理成功,标记为 completed
sqlx::query!(
"UPDATE message_queue SET status = 'completed', updated_at = NOW() WHERE id = $1",
msg.id
)
.execute(&mut *tx)
.await?;
}
Err(e) => {
// 处理失败,重试次数+1,超过3次标记为 failed
sqlx::query!(
r#"
UPDATE message_queue
SET retry_count = retry_count + 1,
status = CASE WHEN retry_count + 1 >= 3 THEN 'failed' ELSE 'pending' END,
updated_at = NOW()
WHERE id = $1
"#,
msg.id
)
.execute(&mut *tx)
.await?;
eprintln!("处理消息 {} 失败:{}", msg.id, e);
}
}
}
tx.commit().await?;
}
Err(e) => {
eprintln!("监听队列 {} 失败:{}", queue_name, e);
// 重试监听
tokio::time::sleep(Duration::from_secs(3)).await;
listener.listen(&queue_name).await?;
}
}
}
}
模块三:替代搜索中间件
轻量级搜索场景(如文章搜索、商品搜索),无需部署 Elasticsearch,利用 PostgreSQL 内置的全文搜索功能(tsvector + tsquery),配合全文索引,即可实现高效的关键词搜索、模糊搜索,满足中小规模搜索需求。
搜索表与索引设计
以文章搜索为例,设计文章表,添加全文搜索字段和索引:
sql
-- 文章表(需要搜索的核心表)
CREATE TABLE IF NOT EXISTS articles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
title VARCHAR(200) NOT NULL, -- 文章标题
content TEXT NOT NULL, -- 文章内容
author_id UUID NOT NULL REFERENCES users(id), -- 关联作者
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- 全文搜索向量字段(存储 title + content 的分词结果)
search_vector tsvector GENERATED ALWAYS AS (
to_tsvector('english', title || ' ' || content)
) STORED -- 自动生成并存储,避免每次查询重新计算
);
-- 创建全文索引,提升搜索性能
CREATE INDEX IF NOT EXISTS idx_articles_search ON articles USING GIN(search_vector);
to_tsvector('english', ...) 用于英文分词,中文分词需要安装 pg_jieba 扩展,然后替换为 to_tsvector('jieba', ...)。
Axum 搜索接口实现
rust
use serde::Deserialize;
use chrono::{DateTime, Utc};
// 搜索请求结构体
#[derive(Deserialize)]
struct SearchRequest {
keyword: String, // 搜索关键词
page: u32, // 页码
page_size: u32, // 每页条数
}
// 文章搜索响应结构体
#[derive(Serialize, sqlx::FromRow)]
struct ArticleSearchDto {
id: uuid::Uuid,
title: String,
content: String,
author_id: uuid::Uuid,
created_at: DateTime<Utc>,
match_score: Option<f32>, // 匹配度分数
}
// 接口:文章全文搜索(使用业务连接池)
async fn search_articles(
State(state): State<AppState>,
Json(req): Json<SearchRequest>,
) -> Result<Json<Vec<ArticleSearchDto>>, axum::Error> {
let offset = (req.page - 1) * req.page_size;
// 将搜索关键词转换为 tsquery,支持模糊匹配(:* 表示前缀匹配)
let query = format!("{}:*", req.keyword);
let articles = sqlx::query_as!(
ArticleSearchDto,
r#"
SELECT
id,
title,
content,
author_id,
created_at,
ts_rank(search_vector, to_tsquery('english', $1)) AS match_score
FROM articles
WHERE search_vector @@ to_tsquery('english', $1)
ORDER BY match_score DESC
LIMIT $2 OFFSET $3
"#,
// 参数绑定
query,
req.page_size as i64,
offset as i64
)
.fetch_all(&state.pool)
.await
.map_err(|e| axum::Error::new(e))?;
Ok(Json(articles))
}
架构优势与局限
这套架构的好处是降低运维成本:摒弃 Redis、RabbitMQ、Elasticsearch 等中间件,仅需部署 PostgreSQL,减少服务器资源占用和维护成本,尤其适合中小团队和独立开发者。
而且性能足够支撑中小规模场景:PostgreSQL 的 Buffer Cache、全文索引、advisory 锁性能,能满足大部分中小规模业务的缓存、搜索等需求;
但这套架构的局限性也比较明显,以下场景不建议使用:
- 高并发缓存场景:如秒杀、高频访问的热点数据,Redis 的单线程模型和内存操作性能优于 PostgreSQL。
- 高吞吐量队列场景:如每秒数万条消息的分发,RabbitMQ、Kafka 的吞吐量和消息投递机制更适合。
- 大规模复杂搜索场景:如千万级数据量、多维度筛选、分词精度要求极高的场景,Elasticsearch 的分布式搜索能力更有优势。
总结
对于中小规模后端项目、快速迭代的产品,用 PostgreSQL 替代缓存、消息队列、定时器、搜索等中间件,搭配 Axum + SQLx 技术栈,是一种性价比极高的架构选择。
最后提醒,技术选型没有绝对的优劣,适合自己业务场景的才是最好的。如果你的项目是中小规模、对性能要求不极致,不妨试试这种架构,相信能给你带来不一样的开发体验。