`
答案在风中
  • 浏览: 64117 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

缓存与分页

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

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

    下面是我使用IL动态生成的一个Demo反编译后的代码(这里针对了同时启用本地缓存和分页缓存的情况,还支持分页缓存无本地缓存、仅进行memcache缓存,这里就不加赘述了),可读性不高不想看直接pass吧。
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代码中常常需要调用函数,这就需要使用到反射(第一次生成动态代码时的反射对整体的性能影响还是可以接受的)。泛型函数的反射还是稍稍有些绕的:
//目标函数public static bool FillResult<T>(...)
MethodInfo fillResult= typeof(PageFormatUtil).GetMethod("FillResult");
fillResult=fillResult.MakeGenericMethod(info.ReturnParameter.ParameterType.GetGenericArguments()[0]);//info.ReturnParameter.ParameterType.GetGenericArguments()获取函数返回结果中泛型参数的信息,MakeGenericMethod之后才是真正完成了泛型函数的反射

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


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

欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
1
0
分享到:
评论
10 楼 答案在风中 2013-09-18  
youjianbo_han_87 写道
你这种按照key规则来进行缓存分页有太大的局限性了。换个业务需求和场景,你就得换个实现。

有段时间没登自己的blog了,之前使用的cachekey规则是:自定义keyname+被代理的函数入参。要说这个设计没有漏洞肯定是不对的,使用字符串拼接必然存在两个不同的业务函数拼接出了相同的cachekey的可能性,所以这个“自定义keyname”的使用就需要谨慎了。
例如:
[CacheMethod("Test1")]
public int test(int i){..}
Test1为自定义keyname,经过代理后,缓存key应该是“Test1|函数入参i的值”
[CacheMethod("Test2")]
public string test(int i){..}
上面两个业务函数分别使用了两个不同的keyname,从一定程度上避免了因为缓存key重复导致缓存的读写异常(int和string类型无法兼容)。
再看一个例子:
[CacheMethod("Test1")]
public int test(int i){..}
[CacheMethod("Test1")]
public int test2(int i){..}
如果定义了这样两个业务函数,希望复用缓存结果。这种情况不能说完全没有意义,不过定义两个“签名”相同(这里仅指returntype、paramtertypes)甚至业务数据都能复用的函数的确是一个不好的设计。
说到这,其实有很重要一点,对keyname的约束应该在编译期或者尽早进行检查,避免可能的缓存混乱的问题。
这里的实现并不是最优的设计,近期我正在使用java来重写这给个cache代理,有什么好的idea欢迎分享~
9 楼 youjianbo_han_87 2013-09-09  
你这种按照key规则来进行缓存分页有太大的局限性了。换个业务需求和场景,你就得换个实现。
8 楼 答案在风中 2012-11-07  
KA兔 写道
可以在后台起一个进程,从数据库中读出数据后直接分好页,然后再按每页的信息存入数据库,这样前端直接取某页的缓存即可

谢谢关注~
7 楼 答案在风中 2012-11-07  
wingsrao 写道
支持楼主继续学习分享。

谢谢支持
6 楼 答案在风中 2012-11-07  
KA兔 写道
KA兔 写道
可以在后台起一个进程,从数据库中读出数据后直接分好页,然后再按每页的信息存入数据库,这样前端直接取某页的缓存即可

然后再按每页的信息存入缓存,比如事先从数据20页的内容,再将这20页的数据分别存入缓存

不知道我理解的有没有偏差呀。“比如事先从数据20页的内容”这样是指数据分块存储吗?这个优化是有的。如果仅仅是为了存放要缓存的信息来创建一张表,在缓存到期的时候,进程启动更新数据(这个过程伴随着查询插入)可能会造成缓存击穿。另外如果已经使用了分页缓存为何还要再建立一张表来做物理存储呢?呵呵,不知道理解的意思对不对哦。
5 楼 答案在风中 2012-11-07  
zhongmin2012 写道
怎么处理缓存中的脏数据呢?

这也是我头疼的东西,没想到什么在不污染原有接口的前提下带入过滤操作。。。
4 楼 zhongmin2012 2012-11-07  
怎么处理缓存中的脏数据呢?
3 楼 KA兔 2012-11-07  
KA兔 写道
可以在后台起一个进程,从数据库中读出数据后直接分好页,然后再按每页的信息存入数据库,这样前端直接取某页的缓存即可

然后再按每页的信息存入缓存,比如事先从数据20页的内容,再将这20页的数据分别存入缓存
2 楼 KA兔 2012-11-07  
可以在后台起一个进程,从数据库中读出数据后直接分好页,然后再按每页的信息存入数据库,这样前端直接取某页的缓存即可
1 楼 wingsrao 2012-11-07  
支持楼主继续学习分享。

相关推荐

Global site tag (gtag.js) - Google Analytics