【Rust + Actix Web】现代后端开发:从零构建高并发 Web 应用

目录

    • 项目概述
    • 环境准备
    • 项目创建与依赖配置
    • 系统架构设计
    • 核心代码实现
      • 1. 数据库模型 (`src/models.rs`)
      • 2. 应用状态管理 (`src/state.rs`)
      • 3. 核心业务逻辑 (`src/handlers.rs`)
      • 4. 主应用入口 (`src/main.rs`)
    • 高并发优化策略
      • 1. 异步处理模型
      • 2. 连接池配置优化
      • 3. 缓存策略设计
    • 性能测试结果
    • 部署方案
      • Docker 部署配置 (`Dockerfile`)
      • Kubernetes 部署配置 (`deployment.yaml`)
    • 总结

本文将带你从零开始构建一个基于 Rust 和 Actix Web 的高并发 Web 应用,涵盖完整开发流程、关键技术实现和性能优化策略。

项目概述

我们将构建一个高性能的 URL 缩短服务,具备以下功能:

  • 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"

系统架构设计

客户端
Nginx 负载均衡
Actix Web 实例1
Actix Web 实例2
Actix Web 实例N
Redis 缓存
PostgreSQL 数据库

核心代码实现

1. 数据库模型 (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,
}

2. 应用状态管理 (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),
        })
    }
}

3. 核心业务逻辑 (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)
}

4. 主应用入口 (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
}

高并发优化策略

1. 异步处理模型

Client Actix Worker Redis PostgreSQL POST / (创建短链) 检查缓存? 缓存未命中 插入新记录 操作成功 SETEX short_code 201 Created GET /{short_code} GET short_code 返回原始URL 异步更新计数 302 Redirect Client Actix Worker Redis PostgreSQL

2. 连接池配置优化

// 优化数据库连接池
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?;

3. 缓存策略设计

// 使用 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%

部署方案

Docker 部署配置 (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"]

Kubernetes 部署配置 (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

总结

通过本文,我们学习了:

  1. 使用 Actix Web 构建完整 Web 应用
  2. 实现 PostgreSQL 数据存储
  3. 集成 Redis 作为高性能缓存
  4. 应用高并发优化策略
  5. 提供容器化部署方案

Rust 和 Actix Web 的组合为构建高并发、安全可靠的 Web 服务提供了强大基础。其异步处理模型和内存安全特性,特别适合构建需要高性能和高可靠性的后端服务。

你可能感兴趣的:(前端,rust,前端,开发语言)