Rust游戏服务端开发随笔【数据缓存方案】

789 阅读6分钟

简介

在游戏业务中,玩家的数据读写都非常的频繁,所以不能在每次玩家操作的时候直接从数据库中或者一些缓存中间件中读取数据,那样响应会很慢,所以常见的做法是在玩家登录时一次性将玩家数据读取到内存中,以后所有的读取、更新操作都基于内存数据进行,然后定时将脏数据写入数据库。

本篇文章会介绍基于Mongodb的数据缓存方案,如何将脏数据写入数据库。

追踪脏数据

假设我们有一个玩家的数据结构Player

pub struct Player {
    pub id: i64,
    pub account: String,
    pub role: Role,
    pub avatar: RoleAvatar,
    pub items: HashMap<i32, Item>,
}

按照最简单的思路,我们只需要在登录的时候,将玩家数据从数据库中加载到内存,然后启动一个定时器,定期把这个结构写入到数据库就行了。但是这样会造成大量的数据写入数据库(现实情况是整个数据结构会非常的大),会对数据库造成压力,并且不是所有数据都在变化,一个更优的方案是我们定期写入那些有变化的数据。

例如玩家数据中的role变化了,那么我们只需要生成一条更新role的数据库操作指令,更新role的数据就好了,又例如玩家的某个items变化了,例如items.1001,那么,我们只会针对items.1001进行数据更新操作。

基于以上思路,我们自然而然的想到,我们需要一个额外的结构来记录上一次进行标脏时,这个数据是什么状态,和这一次标脏操作进行对比,如果它们的状态不一样,那么这个数据就变化了,需要生成写数据库操作。这里就涉及到一个问题,如何标脏?标脏的粒度如何控制?因为我们在更改玩家数据的时候,可能只是改了某个复杂数据结构的某一个字段,例如player.a.b.c.e,理想情况下,我们只需要生成一条类似以下操作的更新指令就行了。

db.player.updateOne({_id:123456}, {$set: {"a.b.c.e": 1}})

但是这样我们需要检查的数据结构就会变得非常多,标脏的逻辑也会更加的复杂,因此我们可以在这个上面做一些取舍,例如标脏的时候,只看Player下面的字段就好了,例如某一次标脏操作,我们只检查role这个字段有没有变化,而不去进一步检查是role里面的哪个具体的字段变化了,我们生成写操作的时候,只要更新role这个字段就好了。这样我们既减小了写数据库的压力,又不会使得标脏的逻辑变得太复杂。

判断数据变化

对于判断某个字段是否发生变化,我们可以计算这个数据的哈希值,通过两次哈希值的对比,可以判断这个数据是否在这个期间内发生过变化,当然,考虑到哈希冲突的可能性,我们可以在多少次无变化后,给这个数据进行一次更加复杂的哈希,例如将这个数据序列化成二进制之后对二进制进行哈希(较慢),这样来保证数据追踪的准确性。

根据以上思路,我们需要一个trait,它能够返回这个struct里面的所有字段,并能够对这些字段进行哈希操作:

pub trait Entity {
    fn id(&self) -> anyhow::Result<Bson>;

    fn for_each_field<F>(&self, visitor: F) -> anyhow::Result<()>
        where
            Self: Sized,
            F: FnMut(FieldRef) -> anyhow::Result<()>;
}

pub enum FieldRef<'a> {
    Value(&'static str, &'a dyn EntityField),
    Map(&'static str, Vec<(&'a dyn EntityField, &'a dyn EntityField)>),
}

pub trait EntityField {
    fn hash(&self) -> u64;

    fn bin_hash(&self) -> anyhow::Result<u64>;

    fn bson(&self) -> anyhow::Result<Bson>;
}
  • id 数据主键,对数据进行更新时会用到
  • for_each_field 提供对Entity中一级字段的访问操作
  • 对于一级的Map字段,我们需要访问里面所有的kv,不对整个Map进行哈希

对EntityField做覆盖实现

由于EntityField里面的逻辑都是一样的,所以我们针对实现了Hash+Encode+Serialize的数据结构,自动实现EntityFieldtrait,bson是当判断整个数据要进行更新的时候,将整个数据转换成Bson结构的方法。

impl<T> EntityField for T where T: Hash + Encode + Serialize {
    fn hash(&self) -> u64 {
        let mut hasher = ahash::AHasher::default();
        self.hash(&mut hasher);
        hasher.finish()
    }

    fn bin_hash(&self) -> anyhow::Result<u64> {
        let bytes = bincode::encode_to_vec(self, bincode::config::standard())?;
        let mut hasher = ahash::AHasher::default();
        Hash::hash(&bytes, &mut hasher);
        Ok(hasher.finish())
    }

    fn bson(&self) -> anyhow::Result<Bson> {
        Ok(to_bson(self).context(type_name::<T>())?)
    }
}

对HashMap和HashSet做哈希实现

标准库中并没有对HashMapHashSet实现Hashtrat,(BTreeMapBTreeSet实现了Hash)因为这两个数据结构迭代kv的顺序是不确定的,所以可能会出现kv相同,但是哈希结果不同的情况。

由于我们的一级数据结构里面可能会包含HashMap或者HashSet这类结构,我们又要求数据结构实现哈希,所以我们需要特殊的HashMapHashSet,我们只通过哈希的结果来判断是否有数据变化,所以即使数据没变化,但是两次的哈希结果不同,也是没关系的,也就生成一次无效的写操作而已。

我们定义一个New Type,直接包裹标准库的实现,然后实现Hash就好了。

pub struct HashMap<K, V>(std::collections::HashMap<K, V, ahash::RandomState>);

impl<K, V> Hash for HashMap<K, V> where K: Hash, V: Hash {
    fn hash<H: Hasher>(&self, state: &mut H) {
        for (k, v) in self {
            k.hash(state);
            v.hash(state);
        }
    }
}

当然,其它的诸如Debug Deref PartialEq Serialize Encode等的实现这里就省略了,代码比较多。

编写Tracer

前面说到,我们需要一些额外的数据结构来记录Entity的数据变化,我们姑且就叫它Tracer好了,Tracer除了包裹真正的数据结构之外,还负责进行数据标脏操作。

#[derive(Default)]
pub struct Tracer<E>
where
    E: Entity + Default,
{
    pub entity: E,
    normal_fields: HashMap<&'static str, TraceValue>,
    map_fields: HashMap<&'static str, HashMap<String, TraceValue>>,
}

#[derive(Debug, Clone, Default)]
pub(crate) struct TraceValue {
    pub(crate) hash_same_times: usize,
    pub(crate) serialize_hash_threshold: usize,
    pub(crate) hash: u64,
    pub(crate) serialize_hash: u64,
}
  • entity 需要进行追踪的数据
  • normal_fields 一级普通字段,对于一级普通字段是Map类型的,我们的标脏逻辑不同,需要单独记录
  • map_fields 一级Map字段,与普通字段的区别是普通字段只有数据更新操作,而Map里面的数据可能会有删除操作,需要在两次标脏中判断哪些数据是新增的、变化的,哪些数据是被删除的
  • hash_same_times 哈希结果相同的次数
  • serialize_hash_threshold 多少次哈希结果相同时进行序列化哈希
  • hash 上一次的哈希结果
  • serialize_hash 上一次的序列化哈希结果

对字段进行哈希计算

直接上代码,代码比较简单,这里就不解释了。

pub(crate) fn hash(&mut self, entity_name: &str, field_name: &str, field: &dyn EntityField) -> anyhow::Result<bool> {
    let hash_changed = self.calculate_hash(entity_name, field_name, field);
    let serialize_hash_changed = self.calculate_serialize_hash(entity_name, field_name, field, false)?;
    Ok(hash_changed || serialize_hash_changed)
}

fn calculate_hash(&mut self, entity_name: &str, field_name: &str, field: &dyn EntityField) -> bool {
    let hash = field.hash();
    if hash != self.hash {
        self.hash = hash;
        self.hash_same_times = 0;
        debug!("{}::{} hash changed", entity_name, field_name);
        true
    } else {
        self.hash_same_times += 1;
        debug!("{}::{} hash not change", entity_name, field_name);
        false
    }
}

pub(crate) fn calculate_serialize_hash(
    &mut self,
    entity_name: &str,
    field_name: &str,
    field: &dyn EntityField,
    force: bool,
) -> anyhow::Result<bool> {
    if self.hash_same_times >= self.serialize_hash_threshold || force {
        self.hash_same_times = 0;
        let serialize_hash = field.bin_hash()?;
        if serialize_hash != self.serialize_hash {
            self.serialize_hash = serialize_hash;
            debug!("{}::{} serialize hash changed", entity_name, field_name);
            Ok(true)
        } else {
            debug!("{}::{} serialize hash not change", entity_name, field_name);
            Ok(false)
        }
    } else {
        Ok(false)
    }
}

pub(crate) fn clean(&mut self, entity_name: &str, field_name: &str, field: &dyn EntityField) -> anyhow::Result<()> {
    self.calculate_hash(entity_name, field_name, field);
    self.calculate_serialize_hash(entity_name, field_name, field, true)?;
    Ok(())
}

对数据进行标脏

new

在进行初始化的时候,我们根据字段的名字,初始化TraceValue,如果init_as_cleantrue的话(通常是从数据库中加载数据后进行初始化),那我们计算一次哈希,当作初始的哈希值。

pub fn new(entity: E, init_as_clean: bool) -> anyhow::Result<Self> {
    let mut tracer = Self {
        entity,
        normal_fields: Default::default(),
        map_fields: Default::default(),
    };
    tracer.entity.for_each_field(|field_ref| {
        match field_ref {
            FieldRef::Value(name, field) => {
                let mut trace_value = TraceValue::default();
                if init_as_clean {
                    trace_value.clean(type_name::<E>(), name, field)?;
                }
                tracer.normal_fields.insert(name, trace_value);
            }
            FieldRef::Map(name, fields) => {
                let mut kvs = HashMap::with_capacity(fields.len());
                for (key, value) in fields {
                    let key = key.bson()?.to_string();
                    let mut trace_value = TraceValue::default();
                    if init_as_clean {
                        trace_value.clean(type_name::<E>(), name, value)?;
                    }
                    kvs.insert(key, trace_value);
                }
                tracer.map_fields.insert(name, kvs);
            }
        }
        Ok(())
    })?;
    Ok(tracer)
}

trace

这个函数就是每隔一定周期进行调用的函数,它会返回一个数据库操作列表,里面就是这次数据变化需要进行数据库更新的操作。

pub fn trace(&mut self) -> anyhow::Result<Vec<Operation>> {
    let mut operations = vec![];
    let Self {
        entity,
        normal_fields,
        map_fields,
    } = self;
    entity.for_each_field(|field_ref| {
        match field_ref {
            FieldRef::Value(name, field) => {
                let trace_value = normal_fields.get_mut(name).unwrap();
                if trace_value.hash(type_name::<E>(), name, field)? {
                    let operation = Self::set(entity.id()?, name.to_string(), field.bson()?)?;
                    operations.push(operation);
                }
            }
            FieldRef::Map(name, fields) => {
                operations.extend(Self::trace_map(
                    entity,
                    name,
                    map_fields.get_mut(name).unwrap(),
                    fields,
                )?);
            }
        }
        Ok(())
    })?;
    Ok(operations)
}

trace_map

对一级Map字段进行标脏的逻辑要复杂一些,首先需要找出新增的、有数据变化的kv生成$set操作,然后找出相对于上次标脏已经不存在的kv,生成$unset操作。

fn trace_map(
    entity: &E,
    name: &str,
    trace_kvs: &mut HashMap<String, TraceValue>,
    kvs: Vec<(&dyn EntityField, &dyn EntityField)>,
) -> anyhow::Result<Vec<Operation>> {
    let mut operations = vec![];
    let mut current_keys = HashSet::with_capacity(kvs.len());
    for (key, value) in kvs.into_iter() {
        let key = key.bson()?.to_string();
        match trace_kvs.entry(key.clone()) {
            Entry::Occupied(mut entry) => {
                let trace_value = entry.get_mut();
                if trace_value.hash(type_name::<E>(), name, value)? {
                    let path = format!("{}.{}", name, entry.key());
                    let operation = Self::set(entity.id()?, path, value.bson()?)?;
                    operations.push(operation);
                }
            }
            Entry::Vacant(entry) => {
                let mut trace_value = TraceValue::default();
                trace_value.clean(type_name::<E>(), name, value)?;
                let path = format!("{}.{}", name, entry.key());
                entry.insert(trace_value);
                let operation = Self::set(entity.id()?, path, value.bson()?)?;
                operations.push(operation);
            }
        }
        current_keys.insert(key);
    }
    for (key, _) in &*trace_kvs {
        if current_keys.contains(key).not() {
            let path = format!("{}.{}", name, key);
            let operation = Self::unset(entity.id()?, path)?;
            operations.push(operation);
        }
    }
    trace_kvs.retain(|key, _| current_keys.contains(key));
    Ok(operations)
}

set unset

fn set(id: Bson, path: String, value: Bson) -> anyhow::Result<Operation> {
    let query = doc! {
        "_id": id,
    };
    let document = doc! {
        "$set": {
            path: value,
        }
    };
    Ok(Operation::set(query, document))
}

fn unset(id: Bson, path: String) -> anyhow::Result<Operation> {
    let query = doc! {
        "_id": id,
    };
    let document = doc! {
        "$unset": {
            path: ""
        }
    };
    Ok(Operation::unset(query, document))
}

编写MongodbOperator

数据更新的IO操作我们单独放到一个Actor中进行操作,不阻塞游戏主逻辑,Mongodb提供了Rust的异步驱动,直接用官方驱动就好了。

#[derive(Debug, Clone)]
pub enum Operation {
    Set {
        query: Document,
        document: Document,
    },
    Unset {
        query: Document,
        document: Document,
    },
    Delete {
        query: Document,
    },
}

编写Actor

这里我们需要数据结构去记录接收到的所有更新操作,当数据成功写入到库中时,再进行删除操作,否则按一定时间进行重试。在merge函数中,我们会对同一path的操作进行合并,我们总是使用最新的操作覆盖可能还未执行的操作,例如对于同一path的操作,当$set还未执行或者执行失败再重试中,新的操作为$unset,那我们直接用$unset覆盖$set即可。

#[derive(Debug)]
pub struct MongodbOperator {
    collection: Collection<Document>,
    operations_by_path: HashMap<String, UpdateOperation>,
    retry: bool,
}

impl MongodbOperator {
    pub fn new(collection: Collection<Document>) -> Self {
        Self {
            collection,
            operations_by_path: HashMap::new(),
            delete_operations: vec![],
            retry: false,
        }
    }

    fn merge(&mut self, operations: Vec<Operation>) {
        for operation in operations {
            if operation.is_update() {
                let key = operation.path();
                let (update_query, document) = operation.into_update().unwrap();
                self.operations_by_path.insert(
                    key,
                    UpdateOperation {
                        query: update_query,
                        document,
                    },
                );
            } else {
                let delete_query = operation.into_delete().unwrap();
                self.delete_operations.push(delete_query);
            }
        }
    }
}

定义BatchExecuteOperations

我们将每次trace产出的Vec<Operation>通过消息发送给Actor进行写库操作。

#[derive(Debug, EmptyCodec)]
pub struct BatchExecuteOperations {
    pub operations: Vec<Operation>,
}

#[async_trait]
impl Message for BatchExecuteOperations {
    type A = MongodbOperator;

    async fn handle(self: Box<Self>, context: &mut ActorContext, actor: &mut Self::A) -> anyhow::Result<()> {
        actor.merge(self.operations);
        context.myself().cast_ns(ExecOperation);
        Ok(())
    }
}

定义ExecOperation

这里就是具体的数据库更新逻辑了。

#[derive(Debug, EmptyCodec)]
pub(crate) struct ExecOperation;

#[async_trait]
impl Message for ExecOperation {
    type A = MongodbOperator;

    async fn handle(
        self: Box<Self>,
        context: &mut ActorContext,
        actor: &mut Self::A,
    ) -> anyhow::Result<()> {
        let mut exec_failed = false;
        let mut update_failed = HashMap::new();
        for (key, operation) in std::mem::take(&mut actor.operations_by_path) {
            let UpdateOperation { query, document } = &operation;
            let mut opts = UpdateOptions::default();
            opts.upsert = Some(true);
            match actor
                .collection
                .update_one(query.clone(), document.clone(), opts)
                .await
            {
                Ok(update_result) => {
                    debug!(
                        "{} update {} {} with result {:?}",
                        actor.collection.name(),
                        query,
                        document,
                        update_result
                    );
                }
                Err(error) => {
                    error!(
                        "{} write operation {} {} error: {}",
                        actor.collection.name(),
                        query,
                        document,
                        error
                    );
                    exec_failed = true;
                    update_failed.insert(key, operation);
                }
            };
        }
        actor.operations_by_path = update_failed;
        if exec_failed {
            actor.retry = true;
            context.myself().cast_ns(ExecOperation);
        } else {
            actor.retry = false;
        }
        Ok(())
    }
}

HashMap 非String类型key处理

Bson只允许用String做key,所以对于非String类型的HashMap key,需要在序列化和反序列化时做额外的处理,在这里我们使用serde_with这个库进行额外的处理,例如文章开头的:

pub items: HashMap<i32, Item>

需要用宏注解:

#[serde_as(as = "HashMap<DisplayFromStr, _>")]
pub items: HashMap<i32, Item>

这样在序列化时,key会自动转换成String类型,反序列化时,自动从String类型转换为i32类型。

这里同时又牵扯出另外一个问题,由于我们的HashMapNew Type,serde_with只对标准库的HashMap做了实现,所以我们要自己再实现一遍序列化和反序列化的接口,只是一层包装,里面还是调用标准库的实现就好了。

impl<K, KU, V, VU> SerializeAs<HashMap<K, V>> for HashMap<KU, VU> where
    KU: SerializeAs<K>,
    VU: SerializeAs<V>,
{
    fn serialize_as<S>(source: &HashMap<K, V>, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
        serializer.collect_map(source.iter().map(|(k, v)| (SerializeAsWrap::<K, KU>::new(k), SerializeAsWrap::<V, VU>::new(v))))
    }
}

impl<'de, K, V, KU, VU> DeserializeAs<'de, HashMap<K, V>> for HashMap<KU, VU> where
    KU: DeserializeAs<'de, K>,
    VU: DeserializeAs<'de, V>,
    K: Eq + Hash,
{
    fn deserialize_as<D>(deserializer: D) -> Result<HashMap<K, V>, D::Error> where D: Deserializer<'de> {
        struct MapVisitor<K, V, KU, VU>(PhantomData<(K, V, KU, VU)>);

        impl<'de, K, V, KU, VU> serde::de::Visitor<'de> for MapVisitor<K, V, KU, VU>
            where
                KU: DeserializeAs<'de, K>,
                VU: DeserializeAs<'de, V>,
                K: Eq + Hash,
        {
            type Value = HashMap<K, V>;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a map")
            }

            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
                where
                    A: serde::de::MapAccess<'de>,
            {
                let mut values = std::collections::HashMap::with_capacity_and_hasher(map.size_hint().unwrap_or(0), ahash::RandomState::default());
                while let Some((key, value)) = map.next_entry()? {
                    values.insert(DeserializeAsWrap::<K, KU>::from(key).into_inner(), DeserializeAsWrap::<V, VU>::from(value).into_inner());
                }
                Ok(HashMap(values))
            }
        }
        let visitor = MapVisitor::<K, V, KU, VU>(PhantomData);
        deserializer.deserialize_map(visitor)
    }
}

dyn类型的数据结构处理

当我们的数据结构中有dyn类型的时候,需要做一些额外的处理,Rust Mongodb Driver采用serde对数据进行序列化和反序列化操作,serde要求序列化对象是Sized的,所以我们不能直接对Box<dyn XX>类型的数据进行序列化、反序列化操作,因为我们不知道其具体的类型。这里我们会使用到另外一个库,typetag,通过这个库,我们就可以序列化任意的dyn类型的数据。其原理是将dyn类型转换成枚举(Rust的枚举真的很强大),这样我们在序列化和反序列化时就可以知道其具体的类型了。

对于Box<dyn XX>类型的数据,我们还需要为其做EntityField的特殊实现:

impl EntityField for Box<dyn XX> {
    fn hash(&self) -> u64 {
        self.deref().hash()
    }

    fn bin_hash(&self) -> anyhow::Result<u64> {
        self.deref().bin_hash()
    }

    fn bson(&self) -> anyhow::Result<Bson> {
        to_bson(self).context(type_name::<Self>())
    }
}

主要是bson这块,我们需要直接对Box<dyn XX>to_bson操作,而不不是对里面的具体类型做to_bson操作,只有这样我们才能调用到typetag实现的序列化接口,而写入具体的类型信息。

最终形式

来看下经过上述处理后我们的数据结构的最终形式以及trait实现:

#[serde_with::serde_as]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Player {
    #[serde(rename = "_id")]
    pub id: i64,
    pub account: String,
    pub role: Role,
    pub avatar: RoleAvatar,
    #[serde_as(as = "HashMap<serde_with::DisplayFromStr, _>")]
    pub items: HashMap<i32, Item>,
}

impl shared::trace::Entity for Player {
    fn id(&self) -> anyhow::Result<mongodb::bson::Bson> {
        shared::trace::entity_field::EntityField::bson(&self.id)
    }
    fn for_each_field<F>(&self, mut visitor: F) -> anyhow::Result<()>
    where
        Self: Sized,
        F: FnMut(shared::trace::FieldRef) -> anyhow::Result<()>,
    {
        visitor(shared::trace::FieldRef::Value("account", &self.account))?;
        visitor(shared::trace::FieldRef::Value("role", &self.role))?;
        visitor(shared::trace::FieldRef::Value("avatar", &self.avatar))?;
        let mut fields = Vec::with_capacity(self.items.len());
        for (k, v) in self.items.iter() {
            fields
                .push((
                    k as &dyn shared::trace::entity_field::EntityField,
                    v as &dyn shared::trace::entity_field::EntityField,
                ));
        }
        visitor(shared::trace::FieldRef::Map("items", fields))?;
        Ok(())
    }
}

结语

对此,对数据的标脏以及写库的核心逻辑就完成了。这个方案还有优化的地方,例如通过过程宏的方式自动为数据结构自动实现Entity,可以很大程度上减少我们的工作量,通过宏进行实现也不容易出错。

#[macros::root_entity]
pub struct Player {
    #[id]
    pub id: i64,
    pub account: String,
    pub role: Role,
    pub avatar: RoleAvatar,
    #[map]
    pub items: HashMap<i32, Item>,
}