Rust + PostgreSQL 极简技术栈应用开发

文章目录

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 技术栈,是一种性价比极高的架构选择。

最后提醒,技术选型没有绝对的优劣,适合自己业务场景的才是最好的。如果你的项目是中小规模、对性能要求不极致,不妨试试这种架构,相信能给你带来不一样的开发体验。

相关推荐
雾岛听风6914 小时前
JavaScript基础语法速查手册
开发语言·前端·javascript
c++之路4 小时前
C++ STL
java·开发语言·c++
河阿里4 小时前
MyBatis-Plus:MyBatis的进阶开发
数据库·mybatis
geovindu4 小时前
go:Template Method Pattern
开发语言·后端·设计模式·golang·模板方法模式
卷Java4 小时前
上下文压缩
开发语言·windows·python
白晨并不是很能熬夜4 小时前
【RPC】第 4 篇:服务发现 — Zookeeper + 缓存容错
java·后端·程序人生·缓存·zookeeper·rpc·服务发现
日取其半万世不竭4 小时前
Minecraft Java版社区服搭建教程(Windows版)
java·开发语言·windows
wjs20244 小时前
HTML 文本格式化
开发语言
sjsjsbbsbsn4 小时前
向量数据库
数据库