You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust中如何序列化带有多个tokio RwLock保护字段的结构体?

Rust 异步锁序列化场景的规范解决方案

Rust 生态中处理这类异步锁序列化问题没有通用银弹,但有几个工业界普遍采用的落地规范,核心原则是永远不要在 serde 的同步序列化流程中处理异步锁的获取逻辑,所有锁操作必须提前放到异步上下文中完成。


方案1:拆分内存态与传输态结构体(最推荐)

这是绝大多数业务场景的首选方案,从架构层面完全规避了序列化时访问锁的问题:

  1. 定义专门用于网络传输的无锁快照结构体,仅用来做序列化/反序列化
  2. 内存中使用的带锁结构体单独实现快照生成方法,统一在异步上下文批量获取所有读锁,生成无锁快照后再做序列化

代码示例:

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:预生成快照缓存(适合高频率发送场景)

如果数据发送频率远高于修改频率,或者每次发送批量拿锁的开销无法接受,可以在内存结构体中维护一份最新的快照缓存:

  1. 引入arc-swap库的ArcSwap类型存储快照,读写均无锁
  2. 每次修改完锁内数据后,同步更新缓存的快照
  3. 需要发送数据时直接加载缓存快照即可,完全不需要获取任何锁

代码示例:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 16:24:02