iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Rust

Rust 後端入門系列 第 12

Day 12 Axum 狀態管理與資料共享

  • 分享至 

  • xImage
  •  

在打造高效、可靠的現代 Web 服務時,對「State(應用程式狀態)」與共用資源的正確認知,往往比任何框架的 API 還重要。狀態管理如果做得不好,在高併發情況下很快就會暴露出鎖競爭、資源耗盡或資料不一致等問題;反之,一套清楚的設計原則可以讓你的服務在保持可測試性與可維護性的同時,達到高吞吐量與低延遲。

理解 State 的運作機制

為什麼需要 State

  • Web 應用需要共享資源:資料庫連線池、快取、設定、客製化 client(例如外部 API client)。
  • 如果每個請求都重新建立資源,會造成大量開銷或資源浪費。State 提供一個把資源注入到路由處理流程的方法。

Axum 的 State 是怎麼傳遞的

  • 在建立 Router 時可以用 with_state 或 .with_state(新版 API 可能有差異)注入一個狀態值。這個值會被複製到每個 handler 的抽取器中(透過 extract::Extension 或 .state() 抽取)。
  • State 的型別通常要求 Clone,因為 Router 會把它放到 make_service/Service 層;實務上做法是把可共享的內部資料包在 Arc 中,讓 State 的 clone 是輕量的。

接下來是一個簡單的範例,教大家如何注入與取用 State

use axum::{Router, routing::get, extract::State};
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    greeting: Arc<String>,
}

async fn hello(state: State<AppState>) -> String {
    // greeting 是 Arc<String>,用 &* 來取得 &str 的顯示內容
    format!("hello, {}", &*state.greeting)
}

#[tokio::main]
async fn main() {
    let state = AppState {
        greeting: Arc::new("world".to_string()),
    };

    let app = Router::new().route("/", get(hello)).with_state(state);
	
		let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
	        .await
	        .unwrap();
	
	    axum::serve(listener, app)
	        .await
	        .unwrap();

}

Arc、Mutex、RwLock 在 Web 應用的使用

Web 伺服器是多執行緒、多協程並發處理請求;因此共享可變狀態必須保證安全(Send + Sync),並避免資料競爭(data race)。

  • Arc: 用來在執行緒間共享不可變資源或內含同步原語的資源。
  • Mutex / RwLock: 提供同步保護,分別適合單寫多讀、或多讀單寫場景。

何時用哪一個

  • 只讀資料(例如讀取快取):只需 Arc。
  • 多個 reader、少數 writer(例如共用 cache、路由表):Arc(避免寫入時阻斷多個讀)。
  • 需要非同步鎖(支援 await 內釋放鎖)則使用 tokio::sync::Mutex 或 tokio::sync::RwLock。標準庫的 Mutex 會在 await 中造成問題(可能導致死鎖或在 await 時持有鎖),因此在 async 環境推薦 tokio 的版本。

範例

使用 tokio::sync::RwLock 共享可變 HashMap

use axum::{Router, routing::get, extract::State};
use serde::Serialize;
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;

#[derive(Clone)]
struct AppState {
    users: Arc<RwLock<HashMap<i64, String>>>,
}

async fn list_users(State(state): State<AppState>) -> String {
    let users = state.users.read().await;
    format!("users: {:?}", users)
}

async fn add_user(State(state): State<AppState>) -> &'static str {
    let mut users = state.users.write().await;
    users.insert(1, "user1".to_string());
    "added"
}

#[tokio::main]
async fn main() {
    let state = AppState { users: Arc::new(RwLock::new(HashMap::new())) };
    let app = Router::new()
        .route("/", get(list_users))
        .route("/add", get(add_user))
        .with_state(state);
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();

    axum::serve(listener, app)
        .await
        .unwrap();
}

為什麼選 tokio::sync::RwLock 而不是 std::sync::RwLock

  • tokio 的鎖是 non-blocking(針對 async/await 設計),持鎖時如果執行 .await 不會阻塞整個執行緒池。若在持鎖期間去 await,標準鎖可能造成問題。以 web handler 經常會 await DB 操作為例,必須使用 async-aware 鎖。

請求間的資料共享策略

常見需求與對應策略

  • 短期請求內共享(例如 request-id、trace span):使用 Request extensions,通常由 middleware 或提取器注入。
  • 多請求短期共享(例如最近 N 次的統計、TTL cache):使用 in-memory cache(sharded HashMap + RwLock 或 dashmap),或用專門的 cache crate(moka、cached)。
  • 長期一致性資料(例如使用者資料):使用資料庫或外部 cache(Redis),避免把核心業務資料只存在 memory,因為多實例部署會造成不一致。
  • 全域讀寫小狀態(例如 feature toggles 在 runtime 修改):Arc,並提供管理 endpoint 做安全變更。

範例

用 moka 實作快取(先檢查快取,沒有時,才進行資料庫查詢)

Cargo.toml

moka = { version = "0.12", features = ["future"] }

main.rs

use axum::{Router, extract::State, routing::get};
use moka::future::Cache;
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    cache: Arc<Cache<String, String>>,
}

async fn get_value(State(state): State<AppState>) -> String {
    let key = "mykey".to_string();
    if let Some(v) = state.cache.get(&key).await {
        return format!("from cache: {}", v);
    }
    // 模擬 DB 查詢
    let value = "db-value".to_string();
    state.cache.insert(key.clone(), value.clone()).await;
    format!("from db: {}", value)
}

#[tokio::main]
async fn main() {
    let cache = Cache::new(100_000);
    let state = AppState { cache: Arc::new(cache) };
    let app = Router::new().route("/", get(get_value)).with_state(state);
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();

    axum::serve(listener, app)
        .await
        .unwrap();
}

什麼情況下,採用外部 cache(如 Redis)還是內部 cache(如 moka)?

  • 內部 cache(moka)適合單一實例或對一致性要求不高的場景,延遲低,整合簡單。
  • 分散式部署需跨實例共享快取或保證一致性,應選 Redis 或其他集中式快取。

上一篇
Day 11 Tower與中間件(Middleware)
系列文
Rust 後端入門12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言