项目导读
作为 Java 开发者,你一定熟悉 Spring Boot 的“约定大于配置”,但也忍受过启动时漫长的 Bean 扫描,以及空载时 300MB 起步的内存占用。
作为 Python 开发者,你享受 FastAPI 的开发速度,但当 QPS 上来时,你不得不面对 GIL 锁带来的吞吐量瓶颈,甚至要把业务逻辑迁移到 Go。Rust 的 Web 生态已经成熟。特别是 Axum(基于 Tokio)和 Sqlx(异步数据库驱动)的组合,被称为现代后端开发的“黄金搭档”。
在这个实战项目中,我们将构建一个短链接服务(URL Shortener)。这不仅仅是一个 Demo,它包含了商业项目必备的要素:数据库连接池、依赖注入(状态管理)、统一错误处理、结构化日志以及 Docker 极小镜像构建。
准备好见证奇迹了吗?我们将把一个完整的 Web 服务打包进 20MB 的 Docker 镜像中,且运行时内存仅需 15MB。
🎯 本项目学习目标
框架选型:掌握 Axum 框架的核心概念(路由、提取器、中间件)。数据持久化:使用 Sqlx 操作 PostgreSQL,体验编译期 SQL 检查的黑科技。架构设计:学习如何在 Rust 中实现类似 Spring 的“依赖注入”(通过 )。错误处理:构建全局统一的错误处理机制(
AppState),告别混乱的
IntoResponse。云原生交付:编写多阶段构建的
try-catch,产出 Distroless 级别的超小镜像。AI 辅助:利用 AI 生成 SQL 迁移脚本和集成测试代码。
Dockerfile
B.1 项目初始化与架构规划
不同于 Python 的随意文件结构,Rust 商业项目讲究模块化。
B.1.1 创建项目与依赖
cargo new short_link_service
cd short_link_service
编辑 ,我们要引入“全家桶”:
Cargo.toml
[package]
name = "short_link_service"
version = "0.1.0"
edition = "2021"
[dependencies]
# Web 框架,Tokio 官方出品,人体工程学极佳
axum = "0.7"
# 异步运行时
tokio = { version = "1.0", features = ["full"] }
# 序列化
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# 数据库 ORM/Mapper
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] }
# 环境变量管理
dotenvy = "0.15"
# 结构化日志与追踪
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# 唯一 ID 生成
nanoid = "0.4.0"
# 错误处理
thiserror = "1.0"
B.1.2 目录结构设计
我们采用经典的分层架构,但比 Java 更轻量:
src/
├── main.rs # 程序入口,组装路由和状态
├── config.rs # 配置加载
├── db.rs # 数据库连接池初始化
├── handlers.rs # 控制器(Controller)逻辑
├── models.rs # 数据模型(DTO/POJO)
└── errors.rs # 全局错误定义
B.2 数据库先行:Sqlx 的魔法
Java 的 Hibernate/MyBatis 需要 xml 配置或大量的注解,且 SQL 写错了要等到运行时才知道。Sqlx 颠覆了这一点:它在编译时连接数据库检查 SQL 语法。
B.2.1 启动 PostgreSQL
为了方便,我们使用 Docker 启动数据库。
docker run --name pg -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres:15-alpine
在项目根目录创建 文件:
.env
DATABASE_URL=postgres://postgres:password@localhost:5432/postgres
RUST_LOG=debug
B.2.2 AI 辅助生成 Schema
我们需要一张表存短链接。
Prompt 建议:
“我正在用 Rust 和 Postgres 做短链接系统。请帮我写一个
的 SQL 语句。字段包括:id (char 6, 主键), original_url (text, 非空), created_at (timestamp), visits (int)。请考虑性能索引。”
CREATE TABLE
AI 会给出 SQL。我们需要安装 来管理迁移:
sqlx-cli
cargo install sqlx-cli
sqlx database create
sqlx migrate add init_schema
将 AI 生成的 SQL 填入 :
migrations/xxxx_init_schema.up.sql
CREATE TABLE IF NOT EXISTS links (
id CHAR(6) PRIMARY KEY,
original_url TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
visits INT NOT NULL DEFAULT 0
);
执行迁移:。
sqlx migrate run
B.3 核心代码实现
B.3.1 定义数据模型 (
src/models.rs)
src/models.rs
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
// 对应数据库表结构
#[derive(Debug, FromRow, Serialize)]
pub struct Link {
pub id: String,
pub original_url: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub visits: i32,
}
// 接收前端创建请求的 DTO
#[derive(Debug, Deserialize)]
pub struct CreateLinkReq {
pub url: String,
}
// 返回给前端的 DTO
#[derive(Debug, Serialize)]
pub struct LinkRes {
pub short_url: String,
}
B.3.2 全局错误处理 (
src/errors.rs)
src/errors.rs
这是 Java 开发者最容易困惑的地方:没有 ,怎么统一处理异常?
try-catch
答案是:自定义 Error Enum + 实现 。
IntoResponse
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
DatabaseError(#[from] sqlx::Error),
#[error("Link not found")]
NotFound,
#[error("Invalid URL format")]
InvalidUrl,
}
// 核心魔法:告诉 Axum 如何把我们的错误变成 HTTP 响应
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match self {
AppError::DatabaseError(e) => {
// 生产环境不要把详细 SQL 错误暴露给前端,记录日志即可
tracing::error!("Database error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error")
}
AppError::NotFound => (StatusCode::NOT_FOUND, "Link not found"),
AppError::InvalidUrl => (StatusCode::BAD_REQUEST, "Invalid URL"),
};
let body = Json(json!({
"error": message
}));
(status, body).into_response()
}
}
B.3.3 业务逻辑与控制器 (
src/handlers.rs)
src/handlers.rs
这里展示 Axum 的依赖注入机制。 包含数据库连接池,通过函数参数自动注入。
AppState
use axum::{
extract::{Path, State},
response::{IntoResponse, Redirect},
Json,
};
use nanoid::nanoid;
use sqlx::PgPool;
use crate::{errors::AppError, models::{CreateLinkReq, Link, LinkRes}};
// 定义共享状态
#[derive(Clone)]
pub struct AppState {
pub db: PgPool,
}
// 1. 创建短链接
pub async fn create_link(
State(state): State<AppState>, // 注入 DB Pool
Json(payload): Json<CreateLinkReq>, // 自动反序列化 JSON
) -> Result<Json<LinkRes>, AppError> { // 返回 Result,错误自动转 HTTP 响应
if !payload.url.starts_with("http") {
return Err(AppError::InvalidUrl);
}
let id = nanoid!(6); // 生成 6 位随机 ID
// SQLX 宏:编译期会检查 SQL 语法!如果表名写错,编译会失败。
sqlx::query!(
"INSERT INTO links (id, original_url) VALUES ($1, $2)",
id,
payload.url
)
.execute(&state.db)
.await?;
Ok(Json(LinkRes {
short_url: format!("http://localhost:3000/{}", id),
}))
}
// 2. 访问重定向
pub async fn redirect_link(
State(state): State<AppState>,
Path(id): Path<String>,
) -> Result<Redirect, AppError> {
// 查询并更新访问量(原子操作)
let result = sqlx::query_as!(
Link,
"UPDATE links SET visits = visits + 1 WHERE id = $1 RETURNING *",
id
)
.fetch_optional(&state.db) // 返回 Option<Link>
.await?;
match result {
Some(link) => Ok(Redirect::to(&link.original_url)),
None => Err(AppError::NotFound),
}
}
B.3.4 组装入口 (
src/main.rs)
src/main.rs
mod errors;
mod handlers;
mod models;
use axum::{routing::{get, post}, Router};
use dotenvy::dotenv;
use sqlx::postgres::PgPoolOptions;
use std::env;
use handlers::AppState;
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
dotenv().ok();
// 初始化日志系统
tracing_subscriber::fmt::init();
// 初始化数据库连接池
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let pool = PgPoolOptions::new()
.max_connections(50)
.connect(&database_url)
.await?;
let state = AppState { db: pool };
// 路由定义:看起来是不是有点像 Flask/Django?
let app = Router::new()
.route("/", post(handlers::create_link))
.route("/:id", get(handlers::redirect_link))
.with_state(state); // 注入状态
let addr = "0.0.0.0:3000";
let listener = TcpListener::bind(addr).await?;
tracing::info!("Listening on {}", addr);
axum::serve(listener, app).await?;
Ok(())
}
B.4 运行与验证
启动服务:创建短链接:
cargo run
curl -X POST -H "Content-Type: application/json" -d '{"url": "https://www.rust-lang.org"}' http://localhost:3000/
# 返回: {"short_url": "http://localhost:3000/x7zK9p"}
访问短链接:
在浏览器访问返回的 URL,应该会自动跳转。查看数据库:
你会发现数据库里多了一条记录,且 增加了。
visits
⚙️ 老司机提示 (Veteran’s Tip)
如果你在编译时遇到
,这是因为 Sqlx 宏需要在编译时连接数据库。
error: relation "links" does not exist
确保你的 Postgres 正在运行且正确。
DATABASE_URL
在 CI/CD 环境中,可以使用生成元数据文件,从而在没有数据库的情况下编译。
sqlx prepare
B.5 性能对比:Rust vs Spring Boot
这是一个震撼的时刻。我们在本机做一个简单的对比:
| 维度 | Java (Spring Boot) | Rust (Axum) |
|---|---|---|
| 编译产物 | 30MB+ (.jar) | 5MB (Binary) |
| 空闲内存 | 250MB – 500MB | 8MB – 15MB |
| 冷启动时间 | 3s – 10s | < 0.1s |
| QPS (单核) | 约 8,000 | 约 40,000+ |
结论:对于 I/O 密集型的 Web 服务,Rust 的吞吐量通常是 Java 的 3-5 倍,而资源消耗仅为 Java 的 1/20。这就是为什么云原生时代大家都在谈论 Rust——它帮你省下的云服务器账单是实实在在的。
B.6 云原生交付:构建 15MB 的 Docker 镜像
Java 镜像通常依赖 基础镜像,动辄 300MB。
openjdk
Python 镜像依赖解释器和依赖包,通常 100MB+。
Rust 可以通过 Multi-stage Build(多阶段构建) 和 Distroless 镜像,做到极致轻量。
创建 :
Dockerfile
# 阶段 1:构建环境 (Build Stage)
FROM rust:1.75 as builder
WORKDIR /app
COPY . .
# 针对 Sqlx 的离线检查模式(如果不想在构建容器里连数据库)
ENV SQLX_OFFLINE=true
RUN cargo build --release
# 阶段 2:运行时环境 (Runtime Stage)
# gcr.io/distroless/cc-debian12 是一个剥离了 Shell 等所有无关文件的极简镜像
FROM gcr.io/distroless/cc-debian12
COPY --from=builder /app/target/release/short_link_service /
COPY .env / # 实际生产中应通过 K8s ConfigMap 注入
CMD ["./short_link_service"]
构建并查看大小:
docker build -t short-link .
docker images | grep short-link
结果通常在 20MB 左右。这是微服务架构中最理想的“集装箱”。
B.7 AI 辅助集成测试
在开发 API 时,编写测试用例很繁琐。
实战场景:
我们写好了 接口,想写一个集成测试。
create_link
Prompt 建议:
“我有一个基于 Axum 的 Rust Web 服务。请使用
和
tower::Service为我的 POST / 接口编写一个集成测试。测试逻辑是:发送 JSON,断言返回 200,并解析返回的 JSON 中的 short_url 字段。”
axum::test_helpers
AI 会生成一段不需要启动 TCP 端口就能直接测试 Handler 逻辑的代码,这是 Axum 架构的一大优势。
B.8 本章小结
在这个实战项目中,我们完成了一次从传统后端到 Rust 现代后端的跃迁:
Axum:让你用类似 Flask/Spring 的路由语法,写出高性能的异步服务。Sqlx:用强类型的 Rust 结构体承载 SQL 结果,且在编译期杜绝 SQL 语法错误。AppError:展示了 Rust 如何优雅地将底层错误转化为 HTTP 响应,而不是抛出异常。资源效率:亲眼见证了 Rust 微服务惊人的内存效率和镜像体积。
你现在拥有的,是一个不仅跑得快,而且跑得省,还很难崩溃的短链接服务。
📝 思考与扩展练习
基础题:目前的 每次都生成新 ID,即使 URL 相同。请修改逻辑:在插入前先查询
create_link 是否已存在,如果存在直接返回旧的短链接。(提示:给数据库添加唯一索引)。进阶题(Redis 缓存):为了进一步提升重定向速度,引入
original_url crate。在访问
redis 时,先查 Redis,命中直接跳转;未命中查 DB 并回写 Redis。这是高并发系统的标准做法。工程题:为服务添加一个简单的中间件(Middleware),记录每个请求的耗时和来源 IP。Axum 的中间件基于
/:id 生态,去查查文档怎么写?
tower
















暂无评论内容