Rust

2025年07月16日更新 7 人订阅
原价: ¥ 6 限时优惠
专栏简介 Rust编程语言之错误处理 Rust 语言之 flod Rust编程语言之Cargo、Crates.io详解 Rust编程语言之枚举与模式匹配 Rust语言 - 接口设计的建议之受约束(Constrained) Rust编程语言之无畏并发 Rust语言 - 接口设计的建议之灵活(flexible) Rust语言 - 接口设计的建议之显而易见(Obvious) Rust语言 - 接口设计的建议之不意外(unsurprising) Rust 实战:构建实用的 CLI 工具 HTTPie Rust编程语言学习之高级特性 Rust内存管理揭秘:深度剖析指针与智能指针 解决Rust中数组和切片的编译时大小问题 《Rust编程之道》学习笔记一 Rust Async 异步编程 简易教程 使用 Async Rust 构建简单的 P2P 节点 Rust编程语言入门之模式匹配 Rust async 编程 Rust编程语言之编写自动化测试 Rust编程语言之函数式语言特性:迭代器和闭包 《Rust编程之道》学习笔记二 Rust Tips 比较数值 使用 Rust 开发一个微型游戏 Rust编程初探:深入理解Struct结构体 深入理解Rust中的内存管理:栈、堆与静态内存详解 深入理解 Rust 结构体:经典结构体、元组结构体和单元结构体的实现 深入掌握 Rust 结构体:从模板到实例化的完整指南 深入理解Rust中的结构体:逻辑与数据结合的实战示例 深入理解 Rust 枚举:从基础到实践 掌握Rust字符串的精髓:String与&str的最佳实践 全面解析 Rust 模块系统:实战案例与应用技巧 Rust 中的 HashMap 实战指南:理解与优化技巧 掌握Rust模式匹配:从基础语法到实际应用 Rust 中的面向对象编程:特性与实现指南 深入理解 Rust 的 Pin 和 Unpin:理论与实践解析 Rust Trait 与 Go Interface:从设计到实战的深度对比 从零开始:用 Rust 和 Axum 打造高效 Web 应用 Rust 错误处理详解:掌握 anyhow、thiserror 和 snafu Rust 如何优雅实现冒泡排序 链表倒数 K 节点怎么删?Python/Go/Rust 实战 用 Rust 玩转数据存储:JSON 文件持久化实战 Rust实战:打造高效字符串分割函数 如何高效学习一门技术:从知到行的飞轮效应 Rust 编程入门:Struct 让代码更优雅 Rust 编程:零基础入门高性能开发 用 Rust 写个猜数游戏,编程小白也能上手! Rust 入门教程:变量到数据类型,轻松掌握! 深入浅出 Rust:函数、控制流与所有权核心特性解析 从零开始:用 Rust 和 Axum 打造高效 Web 服务 Rust 集合类型解析:Vector、String、HashMap 深入浅出Rust:泛型、Trait与生命周期的硬核指南 Rust实战:博物馆门票限流系统设计与实现 用 Rust 打造高性能图片处理服务器:从零开始实现类似 Thumbor 的功能 Rust 编程入门实战:从零开始抓取网页并转换为 Markdown 深入浅出 Rust:高效处理二进制数据的 Bytes 与 BytesMut 实战 Rust智能指针:解锁内存管理的进阶之道 用 Rust 打造命令行利器:从零到一实现 mini-grep 解锁Rust代码组织:轻松掌握Package、Crate与Module Rust 所有权:从内存管理到生产力释放 深入解析 Rust 的面向对象编程:特性、实现与设计模式 Rust + Protobuf:从零打造高效键值存储项目 bacon 点燃 Rust:比 cargo-watch 更爽的开发体验 用 Rust 打造微型游戏:从零开始的 Flappy Dragon 开发之旅 函数式编程的Rust之旅:闭包与迭代器的深入解析与实践 探索Rust编程之道:从设计哲学到内存安全的学习笔记 精读《Rust编程之道》:吃透语言精要,彻底搞懂所有权与借用 Rust 避坑指南:搞定数值比较,别再让 0.1 + 0.2 != 0.3 困扰你! 告别 Vec!掌握 Rust bytes 库,解锁零拷贝的真正威力 告别竞态条件:基于 Axum 和 Serde 的 Rust 并发状态管理最佳实践 Rust 异步编程实践:从 Tokio 基础到阻塞任务处理模式 Rust 网络编程实战:用 Tokio 手写一个迷你 TCP 反向代理 (minginx) 保姆级教程:Zsh + Oh My Zsh 终极配置,让你的 Ubuntu 终端效率倍增 不止于后端:Rust 在 Web 开发中的崛起之路 (2024数据解读) Rust核心利器:枚举(Enum)与模式匹配(Match),告别空指针,写出优雅健壮的代码 Rust 错误处理终极指南:从 panic! 到 Result 的优雅之道 想用 Rust 开发游戏?这份超详细的入门教程请收好! 用 Rust 实现 HTTPie:一个现代 CLI 工具的构建过程 Rust 异步实战:从0到1,用 Tokio 打造一个高性能并发聊天室 深入 Rust 核心:彻底搞懂指针、引用与智能指针 Rust 生产级后端实战:用 Axum + sqlx 打造高性能短链接服务

Rust 生产级后端实战:用 Axum + sqlx 打造高性能短链接服务

Rust生产级后端实战:用Axum+sqlx打造高性能短链接服务当我们在谈论后端开发时,“高性能”和“高可靠”是永恒的追求。正因如此,以安全和并发著称的Rust成为了越来越多开发者构建下一代服务的首选。但是,如何将Rust的语言优势,真正转化为一个健壮、高效、可维护的生产级应用呢

Rust 生产级后端实战:用 Axum + sqlx 打造高性能短链接服务

当我们在谈论后端开发时,“高性能”和“高可靠”是永恒的追求。正因如此,以安全和并发著称的 Rust 成为了越来越多开发者构建下一代服务的首选。但是,如何将 Rust 的语言优势,真正转化为一个健壮、高效、可维护的生产级应用呢?

理论千遍,不如上手一战。

本文将摒弃空谈,通过一个最经典的后端项目——URL 短链接服务——来向您完整展示一个 Rust 生产级后端项目的诞生全过程。我们将使用当前最受欢迎的技术栈:Axum 作为 Web 框架,sqlx 作为数据库交互工具,从零开始,一步步“打造”我们的高性能服务。

跟随本文,您不仅能收获一个完整的项目,更将深入掌握:

  • 生产级的数据库交互:如何用 sqlx 优雅地处理数据冲突,实现原子性操作。
  • 生产级的代码模式:如何正确管理应用状态、处理错误,并理解框架(Axum)的那些“潜规则”。
  • 生产级的开发思维:从遇到问题、分析问题到最终解决问题,体验一个工程师在真实开发中的完整心路历程。

这篇文章是为所有渴望用 Rust 构建真实、可靠应用的开发者准备的。让我们即刻启程,探索 Rust 在生产环境中的真正实力!

技术选型:ORM 还是 sqlx

Rust 数据库处理

  • ORM
    • Diesel
    • Sea-ORM
  • SQL toolkit:sqlx

为什么不推荐使用 ORM

  • 性能
  • 不太需要的额外抽象
  • SQL injection
  • 过于中庸,限制太多
  • 语言绑定,平台绑定

因此,在本次实战中,我们选择 sqlx 作为数据库工具,它能让我们在享受类型安全的同时,发挥出原生 SQL 的最大威力。

使用 sqlx 不使用 orm ,但是得到了orm的好处。

建议阅读 sqlx 文档和 GitHub 下的 example 学习。

构建高效且复杂的 SQL 是每个工程师的基本功 构建一个 URL shortener

  • Tokio
  • axum
  • sqlx
  • nanoid

实操

安装依赖

➜ cargo add sqlx --features postgres --features runtime-tokio --features tls-rustls

代码实现 shortener.rs

use anyhow::Result;
use axum::{
    Json, Router,
    extract::{Path, State},
    http::HeaderMap,
    response::IntoResponse,
    routing::{get, post},
};
use nanoid::nanoid;
use reqwest::{StatusCode, header::LOCATION};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use tokio::net::TcpListener;
use tracing::{info, level_filters::LevelFilter, warn};
use tracing_subscriber::{Layer as _, fmt::Layer, layer::SubscriberExt, util::SubscriberInitExt};

#[derive(Debug, Deserialize)]
struct ShortenReq {
    url: String,
}

#[derive(Debug, Serialize)]
struct ShortenRes {
    url: String,
}

#[derive(Debug, Clone)]
struct AppState {
    db: PgPool,
}

const LISTEN_ADDR: &str = "localhost:9876";

#[tokio::main]
async fn main() -> Result<()> {
    dotenvy::dotenv()?;

    let layer = Layer::new().with_filter(LevelFilter::INFO);
    tracing_subscriber::registry().with(layer).init();

    let url = std::env::var("DATABASE_URL")?;
    // 一般情况下,AppState 都需要使用 Arc 来包裹,因为每一次 state 被使用的时候,都会被 clone 出一个新的 AppState
    // 这里不使用 Arc 是因为 PgPool 内部已经使用了 Arc,所以 AppState 内部不需要再包裹一层 Arc, #[derive(Debug, Clone)] 也会自动生成
    // 如果内部没有使用 Arc,那么一定不要使用 Clone
    // let state = Arc::new(AppState::try_new(&url).await?);
    let state = AppState::try_new(&url).await?;
    info!("Connected to database: {url}");

    let listener = TcpListener::bind(LISTEN_ADDR).await?;
    info!("listening on {}", LISTEN_ADDR);

    let app = Router::new()
        .route("/", post(shorten))
        .route("/{id}", get(redirect))
        .with_state(state);

    axum::serve(listener, app.into_make_service()).await?;

    Ok(())
}

async fn shorten(
    State(state): State<AppState>, // 注意:这里如果 State(state) 在 Json 之后会导致编译错误 body extractor 只能有一个并且要放在最后
    Json(data): Json<ShortenReq>,
) -> Result<impl IntoResponse, StatusCode> {
    let id = state.shorten(&data.url).await.map_err(|e| {
        warn!("Failed to shorten URL: {e}");
        StatusCode::UNPROCESSABLE_ENTITY
    })?;

    let body = Json(ShortenRes {
        url: format!("http://{}/{}", LISTEN_ADDR, id),
    });
    Ok((StatusCode::CREATED, body))
}

async fn redirect(
    Path(id): Path<String>,
    State(state): State<AppState>,
) -> Result<impl IntoResponse, StatusCode> {
    let url = state
        .get_url(&id)
        .await
        .map_err(|_| StatusCode::NOT_FOUND)?;

    let mut headers = HeaderMap::new();
    headers.insert(LOCATION, url.parse().unwrap());

    Ok((StatusCode::FOUND, headers))
}

impl AppState {
    async fn try_new(url: &str) -> Result<Self> {
        let pool = PgPool::connect(url).await?;
        // create tables if not exists
        sqlx::query(
            r#"CREATE TABLE IF NOT EXISTS urls (
                id CHAR(6) PRIMARY KEY,
                url TEXT NOT NULL UNIQUE,
                created_at TIMESTAMP NOT NULL DEFAULT NOW()
            )"#,
        )
        .execute(&pool)
        .await?;

        Ok(Self { db: pool })
    }

    async fn shorten(&self, url: &str) -> Result<String> {
        let id = nanoid!(6);
        sqlx::query("INSERT INTO urls (id, url) VALUES ($1, $2)")
            .bind(&id)
            .bind(url)
            .execute(&self.db)
            .await?;

        Ok(id)
    }

    async fn get_url(&self, id: &str) -> Result<String> {
        let record: (String,) = sqlx::query_as("SELECT url FROM urls WHERE id = $1")
            .bind(id)
            .fetch_one(&self.db)
            .await?;

        Ok(record.0)
    }
}

这段代码是一个使用 Rust 语言和 Axum Web 框架构建的高性能URL短链接服务。它的核心功能有两个:

1) 通过 POST 请求接收一个原始的长 URL,为其生成一个唯一的6位短 ID,存入 PostgreSQL 数据库,并返回完整的短链接地址。 2) 通过 GET 请求访问这个短链接(使用短 ID),服务器会从数据库中查询到对应的原始长 URL,并返回一个 HTTP 302 重定向响应,让浏览器跳转到原始地址。整个服务是异步的,利用 tokio 作为运行时,并使用 sqlx 库与数据库进行异步交互,nanoid 用于生成简短的唯一ID,tracing 用于日志记录。

特别注意:

一、位于 main 函数中,关于 AppState 是否需要 Arc 包装。

// 这里不使用 Arc 是因为 PgPool 内部已经使用了 Arc,所以 AppState 内部不需要再包裹一层 Arc, #[derive(Debug, Clone)] 也会自动生成
// 如果内部没有使用 Arc,那么一定不要使用 Clone
// let state = Arc::new(AppState::try_new(&url).await?);
let state = AppState::try_new(&url).await?;

一般情况下,AppState 都需要使用 Arc 来包裹,因为每一次 state 被使用的时候,都会被 clone 出一个新的 AppState。

这里不使用 Arc 是因为 PgPool 内部已经使用了 Arc,所以 AppState 内部不需要再包裹一层 Arc。

这是在 Axum (以及其他 Rust Web 框架) 中关于状态管理的重要设计模式。通常,当多个请求需要并发访问共享数据(如此处的数据库连接池 PgPool)时,需要将这个共享状态 AppState 包装在 Arc (Atomically Reference Counted, 原子引用计数指针) 中。Arc 允许多个所有者安全地共享数据而不会产生数据竞争。每次请求处理时克隆 Arc 的成本非常低,因为它只增加一个引用计数,而不是复制整个数据。

/// An alias for [`Pool`][crate::pool::Pool], specialized for Postgres.
pub type PgPool = crate::pool::Pool<Postgres>;

pub struct Pool<DB: Database>(pub(crate) Arc<PoolInner<DB>>);

然而,此处的代码是个特例sqlxPgPool 类型在内部已经实现为 Arc 包装的连接池。因此,PgPool 本身就是可以被安全且廉价地克隆的。在这种情况下,再用 Arc<AppState> 进行包装就显得多余了 (Arc<Arc<...>>)。直接在 AppState 结构体上派生 #[derive(Clone)] 就足够了,其克隆操作实际上就是在高效地克隆内部的 PgPool

开发者在开发时,要了解你所使用的库的内部实现,以避免不必要的封装和复杂性。

二、位于 shorten 函数签名中,关于参数顺序的注释。

async fn shorten(
    Json(data): Json<ShortenReq>,
    State(state): State<AppState>,
) -> Result<impl IntoResponse, StatusCode> {

State(state) 在 Json 之后会导致编译错误。

运行编译报错

rust-ecosystem-learning on  main [!?] is 📦 0.1.0 via 🦀 1.88.0 took 25m 25.0s 
➜ cargo run --example shortener
   Compiling rust-ecosystem-learning v0.1.0 (/Users/qiaopengjun/Code/Rust/rust-ecosystem-learning)

error[E0277]: the trait bound `fn(Json<...>, ...) -> ... {shorten}: Handler<_, _>` is not satisfied
   --> examples/shortener.rs:49:26
    |
49  |         .route("/", post(shorten))
    |                     ---- ^^^^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(Json<ShortenReq>, State<AppState>) -> ... {shorten}`
    |                     |
    |                     required by a bound introduced by this call
    |
    = note: Consider using `#[axum::debug_handler]` to improve the error message
    = help: the following other types implement trait `Handler<T, S>`:
              `MethodRouter<S>` implements `Handler<(), S>`
              `axum::handler::Layered<L, H, T, S>` implements `Handler<T, S>`
note: required by a bound in `post`
   --> /Users/qiaopengjun/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-0.8.4/src/routing/method_routing.rs:445:1
    |
445 | top_level_handler_fn!(post, POST);
    | ^^^^^^^^^^^^^^^^^^^^^^----^^^^^^^
    | |                     |
    | |                     required by a bound in this function
    | required by this bound in `post`
    = note: the full name for the type has been written to '/Users/qiaopengjun/Code/Rust/rust-ecosystem-learning/target/debug/examples/shortener-8b2d0ea2a3cea3c5.long-type-16073425591754131837.txt'
    = note: consider using `--verbose` to print the full type name to the console
    = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
warning: `rust-ecosystem-learning` (example "shortener") generated 6 warnings
error: could not compile `rust-ecosystem-learning` (example "shortener") due to 1 previous error; 6 warnings emitted

body extractor 只能有一个并且要放在最后

async fn shorten(
    State(state): State<AppState>, // 注意:这里如果 State(state) 在 Json 之后会导致编译错误 body extractor 只能有一个并且要放在最后
    Json(data): Json<ShortenReq>,
) -> Result<impl IntoResponse, StatusCode> {

这是 Axum 框架的一个关键规则:处理请求体的提取器(Extractor)必须是处理函数参数列表中的最后一个参数。

shorten 函数中,Json<ShortenReq> 是一个提取器,它会读取并解析 HTTP 请求的主体(body)到一个 ShortenReq 结构体中。请求体是一个数据流,一旦被读取消耗后就不能再次读取。Axum 框架为了保证逻辑的正确性和防止意外错误,在编译时就强制规定,任何消耗请求体的提取器(如 Json, Form, Bytes)都必须放在参数列表的末尾。State(state) 也是一个提取器,但它不消耗请求体,而是从应用的状态中提取共享数据。如果把 State(state) 放在 Json(data) 之后,就会违反这个规则,导致编译失败。

这里对于刚接触 Axum 的开发者来说是一个需要注意的点,要避免在参数顺序上犯错。

创建 shortener 数据库


rust-ecosystem-learning on  main [!?] is 📦 0.1.0 via 🦀 1.88.0 
➜ psql                                           
psql (17.4 (Homebrew))
Type "help" for help.

qiaopengjun=# \l
                                                        List of databases
    Name     |    Owner    | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules |        Access privileges        
-------------+-------------+----------+-----------------+---------+-------+--------+-----------+---------------------------------
 blockscout  | blockscout  | UTF8     | libc            | C       | C     |        |           | =Tc/blockscout                 +
             |             |          |                 |         |       |        |           | blockscout=CTc/blockscout
 edu_bazaar  | qiaopengjun | UTF8     | libc            | C       | C     |        |           | =Tc/qiaopengjun                +
             |             |          |                 |         |       |        |           | qiaopengjun=CTc/qiaopengjun    +
             |             |          |                 |         |       |        |           | edu_bazaar_user=CTc/qiaopengjun
 postgres    | qiaopengjun | UTF8     | libc            | C       | C     |        |           | 
 qiaopengjun | qiaopengjun | UTF8     | libc            | C       | C     |        |           | 
 template0   | qiaopengjun | UTF8     | libc            | C       | C     |        |           | =c/qiaopengjun                 +
             |             |          |                 |         |       |        |           | qiaopengjun=CTc/qiaopengjun
 template1   | qiaopengjun | UTF8     | libc            | C       | C     |        |           | =c/qiaopengjun                 +
             |             |          |                 |         |       |        |           | qiaopengjun=CTc/qiaopengjun
 vrf_service | qiaopengjun | UTF8     | libc            | C       | C     |        |           | 
(7 rows)

qiaopengjun=# create database shortener;
CREATE DATABASE
qiaopengjun=# \l
                                                        List of databases
    Name     |    Owner    | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules |        Access privileges        
-------------+-------------+----------+-----------------+---------+-------+--------+-----------+---------------------------------
 blockscout  | blockscout  | UTF8     | libc            | C       | C     |        |           | =Tc/blockscout                 +
             |             |          |                 |         |       |        |           | blockscout=CTc/blockscout
 edu_bazaar  | qiaopengjun | UTF8     | libc            | C       | C     |        |           | =Tc/qiaopengjun                +
             |             |          |                 |         |       |        |           | qiaopengjun=CTc/qiaopengjun    +
             |             |          |                 |         |       |        |           | edu_bazaar_user=CTc/qiaopengjun
 postgres    | qiaopengjun | UTF8     | libc            | C       | C     |        |           | 
 qiaopengjun | qiaopengjun | UTF8     | libc            | C       | C     |        |           | 
 shortener   | qiaopengjun | UTF8     | libc            | C       | C     |        |           | 
 template0   | qiaopengjun | UTF8     | libc            | C       | C     |...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论