本文将带你从零开始构建一个基于 Rust 和 Actix Web 的高并发 Web 应用,涵盖完整开发流程、关键技术实现和性能优化策略。
我们将构建一个高性能的 URL 缩短服务,具备以下功能:
确保已安装 Rust 工具链:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup update
cargo new url_shortener
cd url_shortener
在 Cargo.toml
中添加依赖:
[package]
name = "url_shortener"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.4.0"
serde = { version = "1.0", features = ["derive"] }
dotenvy = "0.15.7"
sqlx = { version = "0.7.2", features = ["postgres", "runtime-tokio-native-tls"] }
uuid = { version = "1.6.1", features = ["v4"] }
redis = { version = "0.23.3", features = ["tokio-comp"] }
parking_lot = "0.12.1"
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] }
anyhow = "1.0.79"
src/models.rs
)use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
#[derive(Debug, FromRow, Serialize, Deserialize)]
pub struct UrlMapping {
pub id: Uuid,
pub original_url: String,
pub short_code: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub access_count: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateUrlMapping {
pub original_url: String,
}
src/state.rs
)use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use redis::Client;
use std::sync::Arc;
pub struct AppState {
pub db_pool: PgPool,
pub redis_client: Arc<Client>,
}
impl AppState {
pub async fn new() -> anyhow::Result<Self> {
dotenvy::dotenv().ok();
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let redis_url = std::env::var("REDIS_URL")
.expect("REDIS_URL must be set");
let db_pool = PgPoolOptions::new()
.max_connections(50)
.connect(&database_url)
.await?;
let redis_client = Client::open(redis_url)?;
Ok(Self {
db_pool,
redis_client: Arc::new(redis_client),
})
}
}
src/handlers.rs
)use crate::{models, state::AppState};
use actix_web::{web, HttpResponse};
use redis::AsyncCommands;
use std::time::Duration;
const CACHE_TTL: usize = 3600; // 1小时缓存
pub async fn create_short_url(
data: web::Json<models::CreateUrlMapping>,
state: web::Data<AppState>,
) -> HttpResponse {
let short_code = generate_short_code();
let new_mapping = models::UrlMapping {
id: uuid::Uuid::new_v4(),
original_url: data.original_url.clone(),
short_code: short_code.clone(),
created_at: chrono::Utc::now(),
access_count: 0,
};
// 存储到数据库
match sqlx::query!(
r#"
INSERT INTO url_mappings (id, original_url, short_code, created_at, access_count)
VALUES ($1, $2, $3, $4, $5)
"#,
new_mapping.id,
new_mapping.original_url,
new_mapping.short_code,
new_mapping.created_at,
new_mapping.access_count
)
.execute(&state.db_pool)
.await
{
Ok(_) => {
// 缓存结果
let mut conn = state.redis_client.get_async_connection().await.unwrap();
let _: () = conn
.set_ex(&short_code, &new_mapping.original_url, CACHE_TTL)
.await
.unwrap();
HttpResponse::Created().json(serde_json::json!({
"short_url": format!("/{}", short_code)
}))
}
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
}
}
pub async fn redirect_to_original(
path: web::Path<String>,
state: web::Data<AppState>,
) -> HttpResponse {
let short_code = path.into_inner();
// 首先尝试从缓存获取
let mut conn = state.redis_client.get_async_connection().await.unwrap();
if let Ok(original_url) = conn.get::<_, String>(&short_code).await {
// 更新访问计数(异步后台任务)
let state_clone = state.clone();
let short_code_clone = short_code.clone();
tokio::spawn(async move {
let _ = increment_access_count(&state_clone, &short_code_clone).await;
});
return HttpResponse::TemporaryRedirect()
.append_header(("Location", original_url))
.finish();
}
// 缓存未命中,查询数据库
match sqlx::query_as!(
models::UrlMapping,
r#"SELECT * FROM url_mappings WHERE short_code = $1"#,
short_code
)
.fetch_one(&state.db_pool)
.await
{
Ok(mapping) => {
// 更新缓存
let _: () = conn
.set_ex(&mapping.short_code, &mapping.original_url, CACHE_TTL)
.await
.unwrap();
// 更新访问计数
increment_access_count(&state, &mapping.short_code).await;
HttpResponse::TemporaryRedirect()
.append_header(("Location", mapping.original_url))
.finish()
}
Err(_) => HttpResponse::NotFound().body("URL not found"),
}
}
async fn increment_access_count(state: &web::Data<AppState>, short_code: &str) -> anyhow::Result<()> {
sqlx::query!(
r#"UPDATE url_mappings SET access_count = access_count + 1 WHERE short_code = $1"#,
short_code
)
.execute(&state.db_pool)
.await?;
Ok(())
}
fn generate_short_code() -> String {
nanoid::nanoid!(6)
}
src/main.rs
)mod models;
mod state;
mod handlers;
mod errors;
use actix_web::{web, App, HttpServer};
use state::AppState;
use handlers::{create_short_url, redirect_to_original};
use sqlx::postgres::PgPoolOptions;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// 初始化应用状态
let app_state = AppState::new().await.expect("Failed to initialize app state");
// 创建数据库表(仅开发环境)
#[cfg(debug_assertions)]
{
let _ = sqlx::migrate!("./migrations")
.run(&app_state.db_pool)
.await;
}
// 启动 HTTP 服务器
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(app_state.clone()))
.route("/", web::post().to(create_short_url))
.route("/{short_code}", web::get().to(redirect_to_original))
})
.bind("0.0.0.0:8080")?
.workers(num_cpus::get() * 2) // 根据CPU核心数设置工作线程
.run()
.await
}
// 优化数据库连接池
let db_pool = PgPoolOptions::new()
.max_connections(50) // 最大连接数
.min_connections(5) // 最小保持连接
.connect_timeout(Duration::from_secs(5))
.idle_timeout(Duration::from_secs(300))
.connect(&database_url)
.await?;
// 使用 Redis 作为缓存层
let mut conn = state.redis_client.get_async_connection().await?;
// 设置缓存并指定TTL
let _: () = conn.set_ex(cache_key, value, CACHE_TTL).await?;
// 批量获取缓存
let keys = vec!["key1", "key2", "key3"];
let values: Vec<String> = conn.mget(keys).await?;
使用 wrk 进行压力测试:
wrk -t12 -c400 -d30s http://localhost:8080/abc123
测试结果:
指标 | 值 |
---|---|
请求总数 | 1,243,567 |
平均每秒请求 | 41,452 |
平均延迟 | 9.23ms |
99% 延迟 | 21.56ms |
错误率 | 0% |
Dockerfile
)FROM rust:1.70-slim-bullseye as builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bullseye-slim
RUN apt-get update && apt-get install -y libssl-dev ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/url_shortener /usr/local/bin
COPY --from=builder /app/migrations /migrations
ENV DATABASE_URL=postgres://user:pass@db:5432/url_shortener
ENV REDIS_URL=redis://redis:6379
EXPOSE 8080
CMD ["url_shortener"]
deployment.yaml
)apiVersion: apps/v1
kind: Deployment
metadata:
name: url-shortener
spec:
replicas: 8
selector:
matchLabels:
app: url-shortener
template:
metadata:
labels:
app: url-shortener
spec:
containers:
- name: app
image: url-shortener:latest
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: REDIS_URL
value: "redis://redis-service:6379"
resources:
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: url-shortener-service
spec:
selector:
app: url-shortener
ports:
- protocol: TCP
port: 80
targetPort: 8080
通过本文,我们学习了:
Rust 和 Actix Web 的组合为构建高并发、安全可靠的 Web 服务提供了强大基础。其异步处理模型和内存安全特性,特别适合构建需要高性能和高可靠性的后端服务。