NewLife.XCode是一个有10多年历史的开源数据中间件,支持nfx/netcore,由新生命团队(2002~2019)开发完成并维护至今,以下简称XCode。
整个系列教程会大量结合示例代码和运行日志来进行深入分析,蕴含多年开发经验于其中,代表作有百亿级大数据实时计算项目。
开源地址:https://github.com/NewLifeX/X (求star, 765+)
全表缓存
在实际项目开发中,经常遇到有一些表数据很少(1000行以内),不会频繁修改(平均每行几个小时才会修改一次),例如配置表、分类表等。
这样的表,往往可以接受三五秒甚至更长的延迟,正是最适合使用缓存的地方。
实体缓存:一次性加载全表数据进入内存,供上层多维度查询!
来看一个例程:
根据查询日志可以看到,虽然执行了1000万次查询,实际上只有一行select日志输出,也就是只查了一次数据库,其它9,999,999次从缓存中查找。
实体缓存的查询速度只取决于 CPU主频,在这台AMD古董机上也可以轻松得到百万级速度。
最后一行是实体缓存统计日志,10秒输出第一次,然后每10分钟输出一次,统计了缓存数、请求数、命中率。
如上,7亿多次查询,命中率在99%以上。
使用实体缓存
实体缓存本质上就是一个实体列表 IList<TEntity>,可通过 Meta.Cache 快速访问。
该列表位于 Meta.Cache.Entities,在 Meta.Cache 上提供了 Find/FindAll 方法。
因为是 IList<TEntity>,所以适用所有Linq方法,如上面例子可以改为:Meta.Cache.Entities.FirstOrDefault(e => e.Name.EqualIgnoreCase(name))
XCode在生成实体类扩展查询代码时,默认都会带有实体缓存用法,当表行数小于1000时,走实体缓存:
if (Meta.Count < 1000) return Meta.Cache.Find(e => e.Name.EqualIgnoreCase(name));
如果不想使用实体缓存,注释这一行即可。除此之外,XCode内部任何地方不会主动使用实体缓存。
使用缓存的阈值
1000是一个大量实践得到的值:
小于1000时,内存搜索远胜于数据库,毕竟数据库还有网络开销和序列化为实体对象的开销;
大于10000时,内存搜索就不如数据库了;
1000到10000之间,内存搜索速度逐步下降,可根据场景决定阈值大小,例如数据极少修改且又需要进行范围搜索时甚至可以设为大于10000;
扩展属性优化
在前面《扩展属性》中提到过,XCode不支持多表关联,而是建议拆分为多次单表查询。查询简单化以后,就可以更容易的实现缓存优化。
还是学生班级的例子,为了在学生列表页展示班级名称,而学生表student只有班级编号classid字段,当时的做法是建立Class扩展属性,借助Class.FindByID查询。
因为班级数量不会特别多,更是极少修改,因此我们可以在Class.FindByID内部使用实体缓存,把所有班级都缓存起来。
至此,学生班级的多表关联查询,借助扩展属性和列表缓存,成功转化成为学生表单表查询,班级名称的匹配几乎毫无压力!
过期策略
所有缓存都必须有过期策略。实体缓存的过期策略有以下:
- 初始化。首次访问缓存时,加锁阻塞所有访问线程,直到加载完全表数据。
- 定时过期。缓存过期后,开异步线程更新并同时返回旧数据,确保应用层性能。设置文件的 EntityCacheExpire, 默认10秒
- 添删改过期。对实体类的添删改操作完成后,都会直接修改实体缓存对应项,而不会清空整个列表。
显然,首次加载以后,将来访问的永远是定时更新的缓存数据,应用层可以得到非常好的性能!
由于实体缓存的添删改过期跟实体操作绑定在一起,因此,越过实体类直接DAL执行更新操作,或者其它服务器修改数据,此时无法影响实体缓存,导致数据更新不及时。
早期版本XCode实体缓存默认过期时间60秒,随着数据库性能提升,默认值修改为10秒,可根据实际场景设置。