Cache - ScutGame/Scut GitHub Wiki

此章节介绍如何使用服务端的Cache缓存

目的与用途

面对开发人员使用数据库在操作数据时,一般会使用查询、删除、更新等Sql语句操作数据库;使用开发时间长又不免方便,对于游戏需要频繁读取和更新数据的需求,性能上又满意;那有没有更好的方案呢?这里给大家介绍一种NoSql的Redis内存数据库,它提供高读写的IO性能,可以满足我们的需求。

在Redis内存数据库基础上,为了更好的使用和性能,在服务器端增加一层缓存访问层(CacheStruct),有区分ShareCacheStruct和PersonalCacheStruct两种方式访问缓存数据(下面会介绍),对于常用的数据在第一次使用后,它会存留在内存中,一定时间内(24h)未使用则会自动移除,再次使用时会再次加载;在数据的增加、修改和删除操作上,使用CacheStruct对象来增加、修改和删除数据,它会监听数据的变动,采用异步的方式自动更新到Redis内存数据库中(更新间隔100ms);不需要关心与数据库或Redis内存数据库的交互。

游戏数值类型的配置数据,变动比较小,不需要在程序里对它修改,是只读类型的;此类数据可以使用SQL或MySql数据库存储,CacheStruct可以针对此类数据也可以缓存,提高读取性能;有修改时,可以重载数据。

注意:Redis内存数据库使用

  • 为了读取性能,它的数据也是在内存中的,不能随意关闭Redis内存数据库,需要等待它自动备份或手动备份后,才可关闭,否则数据会丢失;
  • 打开Redis.conf配置文件,搜索“save”配置项可查看它数据的同步频率配置;
  • “dbfilename”配置项是它的存储磁盘文件,用于备份或数据恢复, 需要自己手动备份;

缓存设计

它提供数据加载,数据重载,数据卸载,缓存查找,缓存修改删除等功能。

缓存存储结构

存储方式使用静态的字典模型,数据以结构化的实体对象Entity放入字典模型。

缓存结构:

缓存池(
    缓存容器(
        缓存项[ 
            实体对象 |
            字典对象 |
            队列对象 |
        ]
    )
)

接下来介绍各个结构

1 缓存池(CachePool)结构如下:(实体类名:表示Entity的完整类名)


字典主键   |  字典值项 
----------------------
实体类名   |  缓存容器
----------------------

2 缓存容器(CacheContainer)结构也是一个字典模型,有Entity、Dictionary和Queue几种类型存储。

实体主键:表示Entity的属性有带“EntityField(true)"标记的主键值。

  • Entity类型存储:它用于公有的ShareEntity实体模型存储,使用ShareCacheStruct缓存访问
    字典主键   |  字典值项 
    ----------------------
    实体主键   |  缓存项
    ----------------------
  • Dictionary类型存储:它用于私有的BaseEntity实体模型存储,PersonalId表示数据的所属者ID是Entity的GetIdentity方法返回值,可以使用UserID作为所属者ID,使用PersonalCacheStruct缓存访问
    字典主键   |  字典值项 
    ----------------------
    PersonalId |  缓存项
    ----------------------
  • Queue类型存储:它用于存储一组Entity对象列表,缓存访问需要继承BaseCacheStruct基类扩展
    字典主键   |  字典值项 
    ----------------------
    队列主键   |  缓存项
    ----------------------

3 缓存项(CacheItemSet)

  • 如果是Entity类型存储:存储Entity对象的值

  • 如果是Dictionary类型存储:值也是一个字典模型,如下

    字典主键   |  字典值项 
    ----------------------
    实体主键   |  Entity   
    ----------------------
  • 如果是Queue类型存储:值是一个队列模型,如下
    Queue项
    ----------------------
    Entity1
    Entity2
    ...
    EntityN
    ----------------------

缓存结构特点:

  • Entity结构:一般常用于Key/Value字典取值,查找速度快,加载数据时只能全部一起加载,过期时则全部一起移除;
  • Dictionary结构:针对一类或一组数据的存储方式,结构是字典的字典,查找同一类的数据速度快,加载数据时可以只加载同一类的数据标识ID的数据,过期时只会移除这一类数据,非常适用于存储玩家的数据,以玩家的ID归类;
  • Queue结构:用于存储一组数据,查找速度比较慢;

缓存更新机制

缓存使用Redis内存数据库为主要存储,监听对实体数据的变动操作放入消息队列等待同步处理,再由线程间隔100ms处理保存到到Redis内存数据库。

当然也支持可选配置的方式存储在SQL或MySql数据库,由于数据库的写入性能差,增加一个同步等待消息队列,间隔5分钟同步一次,如果当中有相同的实体多次操作,同步时只会被执行一次,可以减少更新的频率提供更高的性能。

缓存访问层

操作使用缓存的类它不存储缓存数据,只提供操作方法,缓存数据存储在缓存池CachePool对象中,操作缓存分为以下几种:

  • ShareCacheStruct:访问实体模型使用ShareEntity定义的实体对象;
  • PersonalCacheStruct:访问实体模型使用BaseEntity定义的实体对象;

提示:历史版本使用的操作类

  • ConfigCacheSet:早期版本使用访问ShareEntity实体操作类,可以使用此ShareCacheStruct类替换;
  • GameDataCacheSet:早期版本使用访问BaseEntity实体操作类,可以使用此PersonalCacheStruct类替换;

ShareCacheStruct缓存访问类

提供的常用方法:(Entity表示ShareEntity类型的对象)

  • bool Add(Entity):增加到缓存中,且自动同步Redis存储,如果存在不增加并返回false;

  • bool AddOrUpdate(Entity):增加到缓存,存在则更新,且自动同步Redis存储;

  • bool Delete(Entity):从缓存中删除实体,并同步到Redis;

  • bool RemoveCache(Entity):从缓存中删除实体, 但不同步;

  • void Foreach(Func<string, T, bool>):在缓存遍历数据,LoadingStatus为空时不触发数据加载,参数是Lamda表达式,第一个参数是主键,第二个参数是实体值,最后是返回值,如果返回false则中断遍历;

  • Entity FindKey(Keys):LoadingStatus为空时触发数据加载,通过实体的主键进行查找,以字典Key的方式取值,时间复杂度(1);

  • Entity Find(Predicate<T>):LoadingStatus为空时触发数据加载,通过Lamda表达式条件匹配查找,取满足条件的第一个,时间复杂度(n);

  • List<Entity> FindAll(Predicate<T>, bool):LoadingStatus为空时触发数据加载,获取所有或通过Lamda表达式条件匹配的数据,参数ture表示以实体主键排序后的结果列表;

  • long GetNextNo():获取Redis存储的自增编号,类似数据库中的自增编号,存储在Redis中以“EntityPrimaryKey_”开头的Key;

  • ReLoad():将缓存中的数据更新回Redis,并重新加载数据到缓存中;

  • UnLoad():只将一类实体的缓存清空,Redis存储的数据并不会被清空;

  • bool TryRecoverFromDb(DbDataFilter):尝试从数据库中加载数据更新回Redis存储中,用于Redis数据丢失时处理;

  • bool IsEmpty属性:判断一类的缓存是否为空;

  • LoadingStatus LoadingStatus属性:判断缓存数据加载的状态;

小提示:Lamda表达

  • 它是一个匿名的委托方法,需要匹配方法的参数类型、个数和返回值;参数名字可以随意取,等待被调用;
  • Net自带的有 Action(不带返回值)、Func(带返回值)、Predicate等委托类,可以直接使用,不需要使用Delegate再定义;
  • 例中定义一个参数为int类型且返回bool类型
  • void Main(Func<int, bool> fun){ fun(10); }
  • fun委托方法被调用:Main( p=>{ return true; }),Main参数同 bool method(int p){ return true;}

使用示例:

使用缓存操作时,需要先定义ShareEntity的子类,例定义一个排行榜的实体对象,如下:(了解详细Entity定义)

//定义UserRanking实体
[Serializable, ProtoContract]
[EntityTable(CacheType.Entity, "ConnData")]
public class UserRanking : ShareEntity
{
    public UserRanking () : base(false)
    {
    }

    [ProtoMember(1)]
    [EntityField(true)]
    public int UserID { get; set; }

    [ProtoMember(2)]
    [EntityField]
    public string UserName{ get; set; }
    
    [ProtoMember(3)]
    [EntityField]
    public int Score{ get; set; }
}
  • 添加操作

使用ShareCacheStruct向缓存池中添加一条UserRanking对象,代码如下:

static void Main()
{
    var userRanking = new UserRanking() { UserID = 1001, UserName = "Scut", Score = 90 };
    var cache = new ShareCacheStruct<UserRanking>();
    if (cache.Add(userRanking))
    {
        //这里只是增加到缓存池成功,同步到Redis存储是异步的,暂未提供成功回调
        Console.WriteLine("add ok.");
    }
    else 
    {
        Console.WriteLine("add fail.");    
    }

}
  • 修改操作

使用ShareCacheStruct修改缓存池中的数据时,需要先取出,再修改,代码如下:

static void Main()
{
    var cache = new ShareCacheStruct<UserRanking>();
    var userRanking = cache.FindKey(1001);
    
    if (userRanking != null)
    {
        //修改多个属性值,只提交一次同步
        userRanking.ModifyLocked(() =>
        {
            //注意UserId是主键,不能去修改它
            userRanking.UserName = "Cocos";
            userRanking.Score = 95;
        });

        Console.WriteLine("modify ok.");
    }
    else 
    {
        Console.WriteLine("Not found entity.");    
    }

}
  • 删除操作

使用ShareCacheStruct删除缓存池中的数据,代码如下:

static void Main()
{
    var cache = new ShareCacheStruct<UserRanking>();
    var userRanking = cache.FindKey(1001);
    
    if (userRanking != null)
    {
        //删除缓存并同时删除Redis中存储的数据
        cache.Delete(userRanking);
        Console.WriteLine("delete ok.");
    }
    else 
    {
        Console.WriteLine("Not found entity.");    
    }

}
  • 查找操作

使用ShareCacheStruct查找缓存池中的数据,效率从高到低 FindKey > Find = Foreach > FindAll(false)(结果不排序) > FindAll(True)(结果排序),代码如下:

static void Main()
{
    var cache = new ShareCacheStruct<UserRanking>();
    UserRanking userRanking = cache.FindKey(1001);
    
    userRanking = cache.Find(t=>t.UserID == 1001);
    
    cache.Foreach((key, t) =>
    {
        if (t.UserID == 1001)
        {
            userRanking = t;
            return false; //找到则退出
        }
        return true;
    });
    
    //1.筛选结果不要求顺序
    List<UserRanking> list1 = cache.FindAll(false);
    //取有字母S开头的
    list1 = cache.FindAll(t=>t.UserName.StartsWith("S"), false); 
        
    //2.筛选结果要求以主键升顺
    List<UserRanking> list2 = cache.FindAll();
    //取有字母S开头的
    list2 = cache.FindAll(t=>t.UserName.StartsWith("S"));
    
    //3.自定以UserId降序
    List<UserRanking> list3 = cache.FindAll(false);
    list3.QuickSort((x, y) =>
    {
        return y.UserID - x.UserID;
    });
}
  • 查找主键相同的多种实体
static void Main()
{
    int key = 1001;
    Data1 t1;
    Data2 t2;
    ShareCacheStruct.Get(key, out t1, out t2);//If entity is null throw exception.
}
  • 以主键查找实体,不存在增加
static void Main()
{
    int key = 1001;
    Data1 t1= ShareCacheStruct.GetOrAdd(key, new Lazy<Data1>(() => new Data1()));
}
  • 从数据库恢复数据到Redis
static void Main()
{
    int userId = 123;
    var cache = new ShareCacheStruct<UserRanking>();
    var filter = new DbDataFilter();
    filter.Condition = "Userid = @UserId";
    filter.Parameters.Add("UserId", userId);
    cache.TryRecoverFromDb(filter, userId.ToString());
}

PersonalCacheStruct缓存访问类

提供的常用方法:(Entity表示BaseEntity类型的对象)

  • bool Add(Entity):增加到缓存中,且自动同步Redis存储,如果存在不增加并返回false;

  • bool AddOrUpdate(Entity):增加到缓存,存在则更新,且自动同步Redis存储;

  • bool Delete(Entity):从缓存中删除实体,并同步到Redis;

  • bool RemoveCache(Entity):从缓存中删除实体, 但不同步;

  • void Foreach(Func<string, string, T, bool>):在缓存中遍历所有数据,不触发数据加载,参数是Lamda表达式,第一个参数分类所属ID(PersonalId),第二个是实体主键,第三个参数是实体值,最后是返回值,如果返回false则中断遍历;

  • Entity TakeSubKey(Keys):不触发数据加载,从所有缓存中查找,取出指定实体主键的对象,时间复杂度(n);

  • List FindGlobal(Predicate<T>):不触发数据加载,从所有缓存中查找,取出满足条件的所有数据,时间复杂度(n);

  • LoadingStatus TryFindKey(PersonalId, out entity, Keys):触发数据加载,通过实体的主键进行查找,以字典Key的方式取值,时间复杂度(1),如果LoadingStatus为空时触发加载,并将加载状态返回;

  • Entity FindKey(PersonalId, Keys):同TryFindKey;

  • LoadingStatus TryFind(PersonalId, Predicate<T>, out entity):触发数据加载,先通过PersonalId取出分类的所有数据,对字典取值时间复杂度(1);取的结果再通过Lamda表达式条件匹配查找,取满足条件的第一个,时间复杂度(n);

  • Entity Find(PersonalId, Predicate<T>):同TryFind

  • LoadingStatus TryFindAll(PersonalId, Predicate<T>, bool, out List): 触发数据加载,获取PersonalID的数据后再通过Lamda表达式条件匹配的数据,参数ture表示以实体主键排序后的结果列表;

  • List<Entity> FindAll(PersonalId, Predicate<T>, bool):同TryFindAll

  • long GetNextNo():获取Redis存储的自增编号,类似数据库中的自增编号,存储在Redis中以“EntityPrimaryKey_”开头的Key;

  • ReLoad(PersonalId):将PersonalId的缓存中数据更新回Redis,并重新加载数据到缓存中;

  • UnLoad():只将一类实体的缓存清空,Redis存储的数据并不会被清空;

  • bool TryRecoverFromDb(DbDataFilter, PersonalId):尝试从数据库中加载PersonalId的数据更新回Redis存储中,用于Redis数据丢失时处理;

  • void LoadFrom(Predicate<T> match): 从Redis库中加载匹配math条件的数据,速度比较慢;

  • bool IsEmpty属性:判断一类的缓存是否为空;

使用示例:

使用缓存操作时,需要先定义BaseEntity的子类,例定义一个背包的实体对象,如下:(了解详细Entity定义)

//定义UserPackage实体
[Serializable, ProtoContract]
[EntityTable(CacheType.Dictionary, "ConnData")]
public class UserPackage : BaseEntity
{
    public UserPackage () : base(false)
    {
        //必需要先为Item初始化
        Items = new CacheList<ItemData>();
    }

    [ProtoMember(1)]
    [EntityField(true)]
    public int UserID { get; set; }
    
    //此处用CacheList对象是因为它是线程安全的, 如果用数组或List则不是线程安装
    [ProtoMember(2)]
    [EntityField(true, ColumnDbType.LongText)]
    public CacheList<ItemData> Items { get; set; }
    
    protected override int GetIdentityId()
    {
        //指定分类的字体,以玩家的ID分类对象
        return (int)UserId;
    }
}

//CacheList的项必需继承EntityChangeEvent,对它修改时才会同步到Redis存储
[ProtoContract]
public class ItemData : EntityChangeEvent
{
    [ProtoMember(1)]
    public long ItemID { get; set; }
    
    [ProtoMember(2)]
    public short ItemLv { get; set; }
    
    [ProtoMember(3)]
    public int Num { get; set; }
}
    
  • 添加操作

使用PersonalCacheStruct向缓存池中添加一条UserRanking对象,代码如下:

static void Main()
{
    var userPackage = new UserPackage() { UserID = 1001 };
    var cache = new PersonalCacheStruct<UserPackage>();
    userPackage.Item.Add(new ItemData(){ ItemID = cache.GetNextNo(), ItemLv = 1, Num = 1});
    
    if (cache.Add(userPackage))
    {
        //这里只是增加到缓存池成功,同步到Redis存储是异步的,暂未提供成功回调
        Console.WriteLine("add ok.");
    }
    else 
    {
        Console.WriteLine("add fail.");    
    }

}
  • 修改操作

使用PersonalCacheStruct修改缓存池中的数据时,需要先取出,再修改,代码如下:

static void Main()
{
    var cache = new PersonalCacheStruct<UserPackage>();
    var userPackage = cache.FindKey(1001);
    
    if (userPackage != null)
    {
        //修改多个属性值,只提交一次同步
        userPackage.ModifyLocked(() =>
        {
            //注意UserId是主键,不能去修改它
            userPackage.Items.Add(new ItemData(){ItemID=cache.GetNextNo(), ItemLv=2, Num=1});
            userPackage.Items.Add(new ItemData(){ItemID=cache.GetNextNo(), ItemLv=3, Num=1});
        });

        Console.WriteLine("modify ok.");
    }
    else 
    {
        Console.WriteLine("Not found entity.");    
    }

}
  • 删除操作

使用PersonalCacheStruct删除缓存池中的数据,代码如下:

static void Main()
{
    var cache = new PersonalCacheStruct<UserPackage>();
    var userPackage = cache.FindKey(1001);
    
    if (userPackage != null)
    {
        //删除缓存并同时删除Redis中存储的数据
        cache.Delete(userPackage);
        Console.WriteLine("delete ok.");
    }
    else 
    {
        Console.WriteLine("Not found entity.");    
    }

}
  • 查找操作

使PersonalCacheStruct查找缓存池中的数据,效率从高到低 FindKey > Find = Foreach > FindAll(false)(结果不排序) > FindAll(True)(结果排序),代码如下:

static void Main()
{
    var cache = new PersonalCacheStruct<UserPackage>();
    UserPaceage userPackage = cache.FindKey("1001");
    
    if(cache.TryFindKey("1002", out userPackage) == LoadingStatus.Success)
    {
        if(userPackage == null)
        {
            //当Redis存储中加载成功,且为空时,才增加数据,存在时则更新它
            userPackage = new UserPackage() { UserID = 1002 };
            cache.AddOrUpdate(userPackage);
        }
        
    }
    
    userPackage = cache.TakeSubKey(1002);
    
    userPackage = cache.Find("1001", t=>t.UserID == 1001);
    
    var list = new List<UserPackage>();
    cache.Foreach((personalId, key, t) =>
    {
        if (personalId == "1001")
        {
            list.Add(t)
        }
        return true;
    });
    
    list = cache.FindGlobal(t=>t.UserID == 1001);
}
  • 查找主键相同的多种实体
static void Main()
{
    string userId = "1380001";
    Data1 t1;
    Data2 t2;
    PersonalCacheStruct.Get(userId, out t1, out t2);//If entity is null throw exception.
}

如果有多个主键,只能取一种

static void Main()
{
    string userId = "1380001";
    int key = 1001;
    Data1 t1 = PersonalCacheStruct.Get(userId, false, userId, key);//If entity is null throw exception.
}
  • 以主键查找实体,不存在增加
static void Main()
{
    string userId = "1380001";
    int key = 1001;
    Data1 t1 = PersonalCacheStruct.GetOrAdd(userId, new Lazy<Data1>(() => new Data1()), key);
}
  • 从数据库恢复数据到Redis
static void Main()
{
    int userId = 123;
    var cache = new PersonalCacheStruct<UserFriend>();
    var filter = new DbDataFilter();
    filter.Condition = "Userid = @UserId";
    filter.Parameters.Add("UserId", userId);
    cache.TryRecoverFromDb(filter, userId.ToString());
}
⚠️ **GitHub.com Fallback** ⚠️