缓存与分页

最近再做服务端缓存性能的优化,上一篇《分布式缓存下缓存优化设计方案》http://gkqcz-126-com.iteye.com/admin/blogs/1705695只是粗略的谈了谈对于设计的思路,最近在原设计上又做了一些优化,仅当抛砖引玉(主要参考资料详见上一篇文章)。

上一篇文章里我提到了两种优化方案,一种是使用本地缓存、另一种是分级缓存。这里谈一谈原设计的缺陷,分级缓存中我提出来通过确定两个不同size的缓存块来缓存两种级别的数据,这里带来一些问题:size的大小难以确定、为了避免边界问题大缓存数据包含了小缓存数据这就带来了缓存数据的冗余(这背离了我们设计的初衷)。针对这些问题我们又在原有基础上结合了应用场景的特殊性修改分级缓存为分页缓存(因为对数据列表的访问往往都是伴随分页需求的),将数据库中原始数据中较常使用部分按照固定大小的页进行缓存,服务端根据客户端分页的数据请求到相应的缓存页内查找数据进行填充。采用分页缓存一方面解决了缓存数据冗余的问题,也不用关注分级的边界,虽然相比分级缓存,分页的内容要零散一些,但是总体上而言灵活性要更高。这里谈谈为什么采用固定大小页进行缓存而不是按照客户端分页请求来缓存结果?如果服务端根据客户端分页请求进行缓存这种耦合关系会导致缓存命中率的下、降性能降低,特别是多类型客户端就更糟糕了。按照固定大小页进行缓存类似与MVC模式中将处理逻辑与显示逻辑解耦的思想,服务端的缓存不要依赖客户端,一方面提高了缓存命中率同时也为缓存清理提供了遍历。

下面是我使用IL动态生成的一个Demo反编译后的代码(这里针对了同时启用本地缓存和分页缓存的情况,还支持分页缓存无本地缓存、仅进行memcache缓存,这里就不加赘述了),可读性不高不想看直接pass吧。 C#代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public override ListObject<DemoEntity> GetList(int num5, int num6, int num1, int num4)
{
ListObject<DemoEntity> local;
int num = num1;
int num2 = num4;
int num3 = ((num1 - 1) * num4) % 100;
num1 = (((num1 - 1) * num4) / 100) + 1;//计算缓存页对应的页码和页大小
num4 = 100;
if (num1 > 10)//不在缓存页内直接进行数据库查询
{
return base.GetList(num5, num6, num, num2);
}
ListObject<DemoEntity> obj3 = new ListObject<DemoEntity>();
do
{
string str = string.Concat(new object[] { "DemoCachekeyName", "|", num5, "|", num6, "|", num1, "|", num4 });//根据客户端分页请求计算出对应的cachekey
local = CacheManager.get_Instance().GetLocal(str) as ListObject<DemoEntity>;//LocalCache的访问
if (local == null)
{
DateTime time;
local = CacheManager.get_Instance().Get(str) as ListObject<DemoEntity>;//访问memcache
if (local == null)
{
local = base.GetList(num5, num6, num1, num4);
if (local != null)
{
time = DateTime.Now.AddSeconds(3600.0);
CacheManager.get_Instance().Set(str, local, time);
}
}
if (local != null)
{
time = DateTime.Now.AddSeconds(100.0);
CacheManager.get_Instance().SetLocal(str, local, time);
}
}
num1++;
}
while (((num1 <= 10) && (local != null)) && PageFormatUtil.FillResult<DemoEntity>(obj3, num2, num3, local));//填充结果集
if (obj3.totalCount == 0)
{
return null;
}
return obj3;
}

(下次再完善这里的IL代码的流程图,一直想在缓存结果中再织入进一些过滤操作思前想后没想到如何在不污染原有接口的前提下实现,正在努力中…)

PS:关于IL代码编写:IL代码因为是一种中间代码可读性不是很高,所以进行IL编码其实还是有一点难度的(学习IL编码可以参看《IL Emit学习之旅》一问)。我简单谈谈我在编写IL代码中遇到的一些小问题和自己总结的一些技巧。 1.先编写c#代码的demo,再参照其IL指令,先完成代码框架,在进一步编码。在IL编码前可以先写一个目标生成的动态代码,再通过参照其IL代码进编码,先用IL写出的主体逻辑(即if else、for、while等),再进一步完善。这样逐步编码查错和编码效率都相对高一点。 2.什么时候用“_S”,IL代码中为了缩减指令长度对于某些同一操作提供了两种指令实现,比如无条件跳转有Br、Br_S,有时候使用Br_S跳转目标地址会被截断导致程序出错。我个人觉得可以先在可能出现这类情况的地方使用不带“_S”的指令,待动态代码生成后查看其IL代码,再对指令进行优化。 关于泛型函数的反射: IL代码中常常需要调用函数,这就需要使用到反射(第一次生成动态代码时的反射对整体的性能影响还是可以接受的)。泛型函数的反射还是稍稍有些绕的: C#代码

1
2
3
//目标函数public static bool FillResult<T>(...)
MethodInfo fillResult= typeof(PageFormatUtil).GetMethod("FillResult");
fillResult=fillResult.MakeGenericMethod(info.ReturnParameter.ParameterType.GetGenericArguments()[0]);//info.ReturnParameter.ParameterType.GetGenericArguments()获取函数返回结果中泛型参数的信息,MakeGenericMethod之后才是真正完成了泛型函数的反射

C#代码

1
2
3
//public class ListObject<T>{...}
reflectType=typeof(ListObject<>).MakeGenericType(info.ReturnParameter.ParameterType.GetGenericArguments()[0]);
reflectConstruct=reflectType.GetConstructor(new Type[]{});

End: 作为一个刚工作的大学生,自身存在着很多不足,所以博客中有不足之处希望大家指出。自己写博客分享是一方面,另一方面更多的是想勉励自己多学习多积累。因为很大程度上我也只是一个初学者,分享博客更多的能站在初学者的角度写,另一方面博客中不足和错误也能给像我一样的年轻的程序员一些借鉴。 睡了,大家晚安~

欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。