- Notifications
You must be signed in to change notification settings - Fork 559
此章节介绍如何使用服务端的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类替换;
提供的常用方法:(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")]publicclassUserRanking:ShareEntity{publicUserRanking():base(false){}[ProtoMember(1)][EntityField(true)]publicintUserID{get;set;}[ProtoMember(2)][EntityField]publicstringUserName{get;set;}[ProtoMember(3)][EntityField]publicintScore{get;set;}}- 添加操作
使用ShareCacheStruct向缓存池中添加一条UserRanking对象,代码如下:
staticvoidMain(){varuserRanking=newUserRanking(){UserID=1001,UserName="Scut",Score=90};varcache=newShareCacheStruct<UserRanking>();if(cache.Add(userRanking)){//这里只是增加到缓存池成功,同步到Redis存储是异步的,暂未提供成功回调Console.WriteLine("add ok.");}else{Console.WriteLine("add fail.");}}- 修改操作
使用ShareCacheStruct修改缓存池中的数据时,需要先取出,再修改,代码如下:
staticvoidMain(){varcache=newShareCacheStruct<UserRanking>();varuserRanking=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删除缓存池中的数据,代码如下:
staticvoidMain(){varcache=newShareCacheStruct<UserRanking>();varuserRanking=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)(结果排序),代码如下:
staticvoidMain(){varcache=newShareCacheStruct<UserRanking>();UserRankinguserRanking=cache.FindKey(1001);userRanking=cache.Find(t=>t.UserID==1001);cache.Foreach((key,t)=>{if(t.UserID==1001){userRanking=t;returnfalse;//找到则退出}returntrue;});//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)=>{returny.UserID-x.UserID;});}- 查找主键相同的多种实体
staticvoidMain(){intkey=1001;Data1t1;Data2t2;ShareCacheStruct.Get(key,outt1,outt2);//If entity is null throw exception.}- 以主键查找实体,不存在增加
staticvoidMain(){intkey=1001;Data1t1=ShareCacheStruct.GetOrAdd(key,newLazy<Data1>(()=>newData1()));}- 从数据库恢复数据到Redis
staticvoidMain(){intuserId=123;varcache=newShareCacheStruct<UserRanking>();varfilter=newDbDataFilter();filter.Condition="Userid = @UserId";filter.Parameters.Add("UserId",userId);cache.TryRecoverFromDb(filter,userId.ToString());}提供的常用方法:(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")]publicclassUserPackage:BaseEntity{publicUserPackage():base(false){//必需要先为Item初始化Items=newCacheList<ItemData>();}[ProtoMember(1)][EntityField(true)]publicintUserID{get;set;}//此处用CacheList对象是因为它是线程安全的, 如果用数组或List则不是线程安装[ProtoMember(2)][EntityField(true,ColumnDbType.LongText)]publicCacheList<ItemData>Items{get;set;}protectedoverrideintGetIdentityId(){//指定分类的字体,以玩家的ID分类对象return(int)UserId;}}//CacheList的项必需继承EntityChangeEvent,对它修改时才会同步到Redis存储[ProtoContract]publicclassItemData:EntityChangeEvent{[ProtoMember(1)]publiclongItemID{get;set;}[ProtoMember(2)]publicshortItemLv{get;set;}[ProtoMember(3)]publicintNum{get;set;}}- 添加操作
使用PersonalCacheStruct向缓存池中添加一条UserRanking对象,代码如下:
staticvoidMain(){varuserPackage=newUserPackage(){UserID=1001};varcache=newPersonalCacheStruct<UserPackage>();userPackage.Item.Add(newItemData(){ItemID=cache.GetNextNo(),ItemLv=1,Num=1});if(cache.Add(userPackage)){//这里只是增加到缓存池成功,同步到Redis存储是异步的,暂未提供成功回调Console.WriteLine("add ok.");}else{Console.WriteLine("add fail.");}}- 修改操作
使用PersonalCacheStruct修改缓存池中的数据时,需要先取出,再修改,代码如下:
staticvoidMain(){varcache=newPersonalCacheStruct<UserPackage>();varuserPackage=cache.FindKey(1001);if(userPackage!=null){//修改多个属性值,只提交一次同步userPackage.ModifyLocked(()=>{//注意UserId是主键,不能去修改它userPackage.Items.Add(newItemData(){ItemID=cache.GetNextNo(),ItemLv=2,Num=1});userPackage.Items.Add(newItemData(){ItemID=cache.GetNextNo(),ItemLv=3,Num=1});});Console.WriteLine("modify ok.");}else{Console.WriteLine("Not found entity.");}}- 删除操作
使用PersonalCacheStruct删除缓存池中的数据,代码如下:
staticvoidMain(){varcache=newPersonalCacheStruct<UserPackage>();varuserPackage=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)(结果排序),代码如下:
staticvoidMain(){varcache=newPersonalCacheStruct<UserPackage>();UserPaceageuserPackage=cache.FindKey("1001");if(cache.TryFindKey("1002",outuserPackage)==LoadingStatus.Success){if(userPackage==null){//当Redis存储中加载成功,且为空时,才增加数据,存在时则更新它userPackage=newUserPackage(){UserID=1002};cache.AddOrUpdate(userPackage);}}userPackage=cache.TakeSubKey(1002);userPackage=cache.Find("1001", t=>t.UserID==1001);varlist=newList<UserPackage>();cache.Foreach((personalId,key,t)=>{if(personalId=="1001"){list.Add(t)}returntrue;});list=cache.FindGlobal(t=>t.UserID==1001);}- 查找主键相同的多种实体
staticvoidMain(){stringuserId="1380001";Data1t1;Data2t2;PersonalCacheStruct.Get(userId,outt1,outt2);//If entity is null throw exception.}如果有多个主键,只能取一种
staticvoidMain(){stringuserId="1380001";intkey=1001;Data1t1=PersonalCacheStruct.Get(userId,false,userId,key);//If entity is null throw exception.}- 以主键查找实体,不存在增加
staticvoidMain(){stringuserId="1380001";intkey=1001;Data1t1=PersonalCacheStruct.GetOrAdd(userId,newLazy<Data1>(()=>newData1()),key);}- 从数据库恢复数据到Redis
staticvoidMain(){intuserId=123;varcache=newPersonalCacheStruct<UserFriend>();varfilter=newDbDataFilter();filter.Condition="Userid = @UserId";filter.Parameters.Add("UserId",userId);cache.TryRecoverFromDb(filter,userId.ToString());}