1. 你要的状态到底是哪一种
Rocket 里最常用的两类状态
- Managed State(全局托管状态)
- 应用级别、跨请求共享
- 由 Rocket 统一管理生命周期
- 按类型管理:同一类型最多一个实例
- 并发访问,因此必须线程安全(Send + Sync)
- Request-Local State(请求局部状态)
- 单次请求内有效,请求结束即释放
- 可缓存复用:同一种类型在同一个请求里只生成一次
- 特别适合“鉴权/解析/计算昂贵但可能被多次触发”的场景
2. Managed State:全局状态的标准姿势
2.1 两步走:manage + &State
第一步:启动时注入状态
usestd::sync::atomic::AtomicUsize;structHitCount{count:AtomicUsize,}#[launch]fnrocket()->_{rocket::build().manage(HitCount{count:AtomicUsize::new(0)})}第二步:路由里用&State<T>取出来(它是一个 request guard)
userocket::State;usestd::sync::atomic::Ordering;#[get("/count")]fncount(hit_count:&State<HitCount>)->String{letn=hit_count.count.load(Ordering::Relaxed);format!("Number of visits: {}",n)}2.2 一个类型只能 manage 一次:这是优点,不是限制
Rocket “按类型唯一”意味着:
- 你不会在项目里出现两个同类型的全局对象互相打架
- 依赖关系更清楚:看到
&State<Config>就知道全局只有一个 Config
如果你确实需要多个同类资源(比如两个 Redis 客户端),常见做法是:
- 用不同的“新类型”包装一层:
struct RedisA(Client)、struct RedisB(Client) - 或者用一个聚合结构:
struct AppState { redis_a: Client, redis_b: Client }
2.3 Rocket 会在启动期阻止“未托管状态”导致的运行时爆炸
如果你在路由里写了&State<T>,但启动时忘了.manage(T { .. }),Rocket 会拒绝启动,避免你上线后才发现某个路由一访问就 500。
这种检查背后是 Rocket 0.5 的 sentinel 机制:把“启动前就能发现的错误”尽量前置到 launch 阶段。
3. 在 Request Guard 里访问 Managed State:更高级的复用方式
因为State本身也是 request guard,所以你可以在另一个 guard 的FromRequest实现里取全局状态,常见于“读取配置、校验 token、加载缓存句柄”等场景。
两种方式都能用:
方式 A:request.guard::<&State<T>>().await
userocket::State;userocket::request::{self,FromRequest,Request};structMyConfig{user_val:String}structItem<'r>(&'rstr);#[rocket::async_trait]impl<'r>FromRequest<'r>forItem<'r>{typeError=();asyncfnfrom_request(req:&'rRequest<'_>)->request::Outcome<Self,()>{req.guard::<&State<MyConfig>>().await.map(|cfg|Item(&cfg.user_val))}}方式 B:request.rocket().state::<T>()
适合你想“直接查有没有”,并在没有时自定义 forward / error。
这类模式的价值在于:路由函数签名会变得非常干净,很多业务约束被集中到了 guard,路由只处理业务。
4. Request-Local State:请求级缓存,专治“同一请求里重复算多次”
Rocket 的请求局部状态通过request.local_cache(|| ...)实现:
- 闭包在一个请求内最多执行一次
- 之后同类型再取,拿到的是缓存结果
典型用途:生成请求 ID、解析并缓存认证结果、记录请求耗时起点等。
下面是一个“每个请求生成唯一 ID,并在请求内复用”的 guard:
usestd::sync::atomic::{AtomicUsize,Ordering};userocket::request::{self,FromRequest,Request};staticID_COUNTER:AtomicUsize=AtomicUsize::new(0);structRequestId(pubusize);#[rocket::async_trait]impl<'r>FromRequest<'r>for&'rRequestId{typeError=();asyncfnfrom_request(req:&'rRequest<'_>)->request::Outcome<Self,()>{request::Outcome::Success(req.local_cache(||{RequestId(ID_COUNTER.fetch_add(1,Ordering::Relaxed))}))}}#[get("/")]fnid(id:&RequestId)->String{format!("This is request #{}.",id.0)}这个机制解决了三个痛点:
- 把数据绑定到请求本身(而不是全局变量)
- 保证同一请求内只生成一次(避免重复开销与不一致)
- guard 可能在一次请求内被多次触发(转发/多路由匹配/组合 guard),缓存能直接省成本
5. 数据库:rocket_db_pools 的“三步接入法”
Rocket 0.5 推荐用rocket_db_pools(异步、ORM 无关)接入数据库连接池,流程非常固定:
5.1 Cargo.toml 选择驱动 feature
例如用 sqlx + sqlite:
[dependencies.rocket_db_pools] version = "0.2.0" features = ["sqlx_sqlite"]5.2 Rocket.toml 配置数据库
给数据库起个名字,比如sqlite_logs:
[default.databases.sqlite_logs] url = "/path/to/database.sqlite"5.3 派生 Database + attach 初始化 + Connection 取连接
#[macro_use]externcraterocket;userocket_db_pools::{Database,Connection};userocket_db_pools::sqlx::{self,Row};#[derive(Database)]#[database("sqlite_logs")]structLogs(sqlx::SqlitePool);#[get("/<id>")]asyncfnread(mutdb:Connection<Logs>,id:i64)->Option<String>{sqlx::query("SELECT content FROM logs WHERE id = ?").bind(id).fetch_one(&mut**db).await.and_then(|r|Ok(r.try_get(0)?)).ok()}#[launch]fnrocket()->_{rocket::build().attach(Logs::init()).mount("/",routes![read])}你会注意到两点很舒服:
- 数据库连接就是一个 request guard:
Connection<Logs> - 初始化连接池靠
.attach(Logs::init()),生命周期交给 Rocket
5.4 需要 sqlx 的额外能力?自己把 sqlx feature 打开
rocket_db_pools只开最小 feature。你要用 sqlx 的宏、迁移等,就显式依赖 sqlx:
[dependencies.sqlx] version = "0.7" default-features = false features = ["macros", "migrate"] [dependencies.rocket_db_pools] version = "0.2.0" features = ["sqlx_sqlite"]5.5 同步 ORM 怎么办
如果你必须用 Diesel 这类同步 ORM,Rocket 也提供rocket_sync_db_pools。但在 Rocket 0.5 的异步世界里,同步 I/O 的代价更高,优先选异步栈会更省心。
6. 一套能直接带进项目的最佳实践清单
- Managed State 里放什么
- 纯配置(只读):
Config - 可并发共享的句柄:HTTP 客户端、缓存客户端、队列生产者、连接池(通常数据库用 rocket_db_pools,不一定要 manage)
- 计数/统计:Atomic 系列
- 需要可变共享:用
tokio::sync::RwLock/Mutex或parking_lot(注意 async 场景更推荐 tokio 的锁)
- Request-Local State 里放什么
- 鉴权结果(User/Claims)
- 请求追踪 ID、开始时间戳
- 解析后的 header/cookie/token
- 任何“可能被多次 guard 调用但只想算一次”的东西
- guard 设计建议
- 把“能失败/能 forward”的逻辑写在 guard 里,路由只做业务
- 需要区分“未登录 vs 非管理员 vs 其他错误”的,结合 forwarding + rank 做分流,会非常优雅
- 数据库连接用法
- 用
Connection<T>作为路由参数,不要手动全局持有连接 - 大事务/耗时操作更要注意 async 生态,避免阻塞 runtime