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
类替换;
提供的常用方法:(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());
}
提供的常用方法:(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());
}