Rust中如何序列化带有多个tokio RwLock保护字段的结构体?
Rust 异步锁序列化场景的规范解决方案
Rust 生态中处理这类异步锁序列化问题没有通用银弹,但有几个工业界普遍采用的落地规范,核心原则是永远不要在 serde 的同步序列化流程中处理异步锁的获取逻辑,所有锁操作必须提前放到异步上下文中完成。
方案1:拆分内存态与传输态结构体(最推荐)
这是绝大多数业务场景的首选方案,从架构层面完全规避了序列化时访问锁的问题:
- 定义专门用于网络传输的无锁快照结构体,仅用来做序列化/反序列化
- 内存中使用的带锁结构体单独实现快照生成方法,统一在异步上下文批量获取所有读锁,生成无锁快照后再做序列化
代码示例:
use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::RwLock; // 传输态:无锁,仅用于序列化/反序列化 #[derive(Serialize, Deserialize, Clone)] struct PlayerSnapshot { pub id: u64, pub meta: Metadata, pub stats: Metadata, // 其余字段均为无锁类型 } // 内存态:带异步锁,用于业务逻辑读写 struct Player { pub id: u64, pub meta: Arc<RwLock<Metadata>>, pub stats: Arc<RwLock<Metadata>>, // 其余带锁字段 } impl Player { // 统一在异步上下文批量获取锁生成快照 pub async fn snapshot(&self) -> PlayerSnapshot { // 固定锁获取顺序避免死锁,tokio RwLock读锁可重入 let (meta, stats, f1, f2, f3) = tokio::join!( self.meta.read(), self.stats.read(), // 其余字段读锁获取 ); PlayerSnapshot { id: self.id, meta: meta.clone(), stats: stats.clone(), // 其余字段赋值 } } }
该方案优势:逻辑清晰、一致性可控、锁持有时间仅为字段克隆的极短耗时,性能开销可控;仅需额外维护一套结构体,适合绝大多数业务场景。
方案2:预生成快照缓存(适合高频率发送场景)
如果数据发送频率远高于修改频率,或者每次发送批量拿锁的开销无法接受,可以在内存结构体中维护一份最新的快照缓存:
- 引入
arc-swap库的ArcSwap类型存储快照,读写均无锁 - 每次修改完锁内数据后,同步更新缓存的快照
- 需要发送数据时直接加载缓存快照即可,完全不需要获取任何锁
代码示例:
use arc_swap::ArcSwap; struct Player { pub id: u64, pub meta: Arc<RwLock<Metadata>>, pub stats: Arc<RwLock<Metadata>>, // 缓存最新快照 snapshot: ArcSwap<Arc<PlayerSnapshot>>, } // 修改meta的示例逻辑 impl Player { pub async fn update_meta(&self, new_meta: Metadata) { let mut meta_lock = self.meta.write().await; *meta_lock = new_meta; // 写完锁后同步更新快照缓存 let new_snapshot = Arc::new(self.snapshot().await); self.snapshot.store(new_snapshot); } // 发送时直接拿缓存,零锁开销 pub fn get_cached_snapshot(&self) -> Arc<PlayerSnapshot> { self.snapshot.load().clone() } }
该方案优势:发送时零锁开销,延迟极低;缺点是会额外占用内存,修改数据时多了一份更新快照的开销,适合读多写少的场景。
方案3:手动实现Serialize(仅适合特殊场景,不推荐)
如果确实不想拆分结构体,可以为Player手动实现Serialize trait,但注意不要在serialize方法内处理锁:需要先在异步上下文中拿到所有字段的读锁,将数据存到临时变量中,再传入serde进行序列化。
该方案不推荐的原因:容易不小心在同步方法里调用异步锁操作导致runtime阻塞,嵌套锁场景下维护成本极高。
通用注意事项
- 所有耗时超过1ms的同步序列化/反序列化操作,必须放到
tokio::spawn_blocking中执行,避免阻塞异步runtime的worker线程 - 批量获取读锁时要固定锁的获取顺序,避免出现死锁
- 如果对数据一致性要求不高,可以使用
try_read()尝试获取锁,拿不到就用上次的快照,进一步降低延迟
内容的提问来源于stack exchange,提问作者Bonanov
相关产品推荐
相关产品推荐

