DataPersistence - ScutGame/Scut GitHub Wiki

此章节介绍服务端缓存的持久化存储机制

持久化存储概述

持久化存储是将服务器中的缓存Cache转存储到电脑磁盘的过程,服务器关掉重启数据不会丢失掉,它会重新从电脑磁盘中读取数据恢复到服务器缓存Cache中。

Scut缓存持久化

Scut的存储分为:服务器的缓存,Redis缓存数据库,DB数据库(SQL/MySql);

先说说Scut的数据加载步骤

  1. 根据定义实体的EntityTable信息,判断StorageType属性是从Redis数据库还是从DB数据库加载数据;

  2. 如果是Redis,则使用实体名和实体主键(EntityField(True))做为Redis的Key,取出数据;

  3. 如果是DB数据库,则从表名为实体名,查询字段为实体主键的数据;

  4. 如果实体的是ShareEntity的子类,它是加载全部的数据,不再使用实体主键筛选数据;

数据加载到服务器缓存Cache后,需要取出来使用或修改其中的数据,那修改完后,它怎么存储到DB数据库或Redis数据库呢?

接下来看看Scut的数据更新步骤

  1. 从缓存中取出的实体对象,当修改它的属性时会触发Changed事件,会告诉Scut有数据改变了需要持久化数据到DB数据库或Redis数据库中;

  2. 当收到Changed件事通知时,会将此实体对象先放入内存队列(是服务器的内存)中,会有一个子线程负责每间隔100ms处理内存队列,将数据写入到Redis的消息队列中,这时还并没有更新到DB数据库或Redis数据库(为了因Changed事件触发频繁造成拥堵,这里使用内存队列作层缓冲而不是直接写入Redis消息队列);

  3. 接着会有多个线程间隔100ms处理Redis消息队列(每个线程只负责相应的子队列,默认启用2个,可以配置增加),它将实体数据存储到Redis数据库中,如果有开启Db数据库存储会提交给另一个Redis的Sql消息队列;

  4. Sql消息队列负责将Sql语句更新DB数据库,由于DB数据库它写入性能不高,这里采用的延迟时间默认为5分钟来缓冲实体数据的写入量(实体主键相同只会写入一次),默认启用2个线程来处理Sql消息队列;如果要实时写入数据库,调用DataSyncManager.SendSql方法。

  5. 到这时实体数据才持久化成功,如果在此之前业务层有调用Cache的ReLoad或LoadFrom方法,它加载的数据是旧的数据会造成修改的实体数据丢失,因此只有在服务器启动初始化时使用LoadFrom和Reload方法,其它地方谨慎使用;

以上步骤数据更新由DataSyncQueueManager类负责处理。

另外提示

  • Redis数据库中带'__'开头的是Key是Redis消息队列,带'$'开头的是实体存储数据;
  • '__'开头并且有Error结束的Key,表示处理异常的队列,这个队列需要手机处理,判断是否可以直接删除掉;
  • 消息队列产生的异常会记录在Log目录的Exception子目录下,监控消息则在Warn目录;

Redis缓存数据转移

由于随着时间的推移,有些玩家流失,那么这部分的数据会占用Redis的内存,造成Redis占用很大的内存; Scut将这部分流失的玩家数据定义为冷数据,可以将它从Redis中转移到数据库中存储(Temp_EntityHistory表),当这部分玩家又有上来时,会自动从数据库中加载到Redis中,转变化热数据。

  1. 接下来看看如何转移Redis数据:

    我们需要制作一个工具,创建一个新的Console项目,调用Scut类库dll,工具的配置文件与游戏项目的配置相同,根据自己的业务编写相应的脚本。

    调用CacheFactory类的RemoveToDatabase方法,代码如下:

    static void Main()
    {
        var userId = "278903";
        var keys = PersonalCacheStruct.TakeOrLoad<UserHero>(userId).Select(t => t.GetKeyCode()).ToList();
        CacheFactory.RemoveToDatabase(new[]{
            new KeyValuePair<Type, IList<string>>(typeof(UserRole), new[] { userId }),
            new KeyValuePair<Type, IList<string>>(typeof(UserHero), keys),     
        });
    }
    

    示例中将玩家为278903的角色UserRole实体数据与英雄UserHero实体数据转移到数据库的Temp_EntityHistory表中,因为UserHero使用了UserId+HeroId作为复合主键,一个角色下会有多个Hero的实例,要转移时必须将因角色下的所有Hero一起转移, 不能单独转移一个Hero实例。

    以上示例是针对指定的玩家,也可以从数据库查出一定时间未登录的玩家ID,再遍历处理。

  2. Redsi数据转移后需要开启冷数据加载开关

    在默认情况下,考虑加载性能是不会从冷数据表加载数据的, 需要手动开启,在项目配置中增加一段配置,如下:

    <add key="Cache.IsStorageToDb" value="true" />
⚠️ **GitHub.com Fallback** ⚠️