我们在开发中,经常会用到各种缓存,比如Session、Application、HttpRuntime.Cache、Redis、Memcached、MongoDB、Riak等。而一般项目中使用缓存时,都是比较初级的,大多都是常见的Key-Value方式,通过依赖、时间、同步更新或直接删除方法来管理缓存的过期。当然网上对于缓存的介绍绝大部分都是这方面的,而对于多级缓存、缓存与缓存相互关联、表记录与多缓存关联、后端缓存与前端页面缓存关联、缓存名称动态生成的缓存与其他缓存联动处理、频繁更新的缓存与其他缓存联动问题……等等不同情况下该如果去管理这些缓存的知识点,我在园子里找了半天也没有看到……而最近自己开发的项目中就碰到了缓存管理上的问题,所以发这篇贴子同大家讨论一下有什么更好的解决方案。

 

  随着项目参与人员数量的增加,大家经验的不同和对缓存的认知不一样,而项目为了达到在生产环境上能处理更大的并发和良好的性能,缓存的使用也越来越广泛了。项目在老板、运营部门和项目经理的推动下,新功能、新需求不断的推陈出新,代码与复杂度也几何级的爆增起来,缓存的使用几乎充斥在所有代码的调用当中,由于没有专门写一个处理插件对它进行统一管理,造成缓存管理开始有些混乱。

  比如对运行中的系统在某些情况下执行了清空全部缓存时,偶尔会发生一些小异常(有些数据读取不正常);对同一结果重复使用缓存(A同事创建了一个缓存来存储H任务执行结果的,而B同事不知道A同事创建过,也创建了一个),浪费内存空间;多个缓存依赖同一个缓存值的变动,某些人由于一些需要修改了所依赖缓存的名称以及所影响的一些缓存时,个别非自己编写的缓存没有处理到或忘记处理了;某些缓存存储内容依赖临时表来创建的,缓存名称有规律但不固定,而另外一些缓存的内容是根据这些缓存来计算的,当这些缓存更新时如何能自动同步所依赖它的缓存……

  当然上面这些情况或还有其他的情况单独编写代码来处理肯定可以实现,只需要花多一些时间而已,问题是如果系统很庞大后,充斥差各种交叉关联的缓存时,它们已像蜘蛛网一样,动一发而牵动全身,写这些处理代码一个没考虑好就会影响到其他内容。况且有很多时候更新临时表记录时,有些缓存名称是不固定的(根据某些规则关联到其他表记录或日期等方式生成的缓存名称),代码并不可能智能识别要同步更新那些缓存。所以编写一个强大的自动化缓存处理插件也势在必行了。

  相信博客园团队和其他一些大型的网站都有着自己一套完善的缓存管理办法,由于自己知识广度还不足够,暂时没有在网上发现有一套完善、高效的解决方案可以借鉴,只好自己来想办法解决,所以先抛出一块砖头,看能否引来一堆美玉了,嘿嘿…

 

  下面我们先来了解一下缓存中的一些分类与名词说明

  按名称的命名可分为:

  固定名称:通常以表名、字段名、功能名称、前几项的组合……等按各人的喜好来进行命名,调用时也直接方便

  有固定前缀(单个或多个可变后缀或可变后缀+固定后缀组合等):表名+记录Id、临时表名(表名按一定规则进行变动,比如后缀为年月、关联表的Id等)、功能名称+编码……调用时需要动态传入指定的参数,在不知道参数的情况下无法对该缓存进行操作

 

  按缓存依赖内容可分为:

  依赖指定表:指定表记录增加、修改、删除、更新时,需要同步更新该缓存内容

  依赖指定表中的某些或某条记录:同上

  依赖多个表数据:同上

  依赖指定字段值:默字段值改变时,同步修改(主要用于更新频率比较高的字段,比如页面点击计数等,如果需要用到缓存的,需要独立出来存储,以免更新时执行同步清除功能)

  依赖其他缓存:指定缓存值改动时,需要同步修改所依赖它的其他缓存值(比如依赖某些计算结果或状态值;存储某些临时记录等)

  ……

 

  按影响缓存值的操作可分为:

  数据表记录的添加、修改、删除;其他缓存值的更新变化;某些计算结果的变化等

  对于所依赖的内容变化后,相关缓存就需要同步更新,这样又可分为实时同步和延时同步等方式

  缓存更新策略通过有:实时更新(主要针对记录级别缓存,直接同步更新指定记录;当然也可以整表更新,但这样对程序执行性能有较大的影响)、超时检测(比如缓存依赖其他缓存时,设置一个最后更新时间与获取时间,通过比较两个时间来确定缓存是否过期)、绝对时间过期(为缓存设定过期时间)、动态时间过期(缓存被访问后过期时间顺延)等。

 

  按缓存数据集合大小分:

  单值、单条记录、小型数据集合、中型数据集合、大型数据集合、超大型数据集合

  对于缓存管理,数据集越小则存取与转换速度越快,所以当数据集合过大时,就必须进行分割,将集合尽量分成小块,提升缓存使用性能

 

  按缓存更新频率分:

  固定值(指的是某些配置信息,存储进缓存后它的值就不再变化)、偶尔更新、经常更新、频繁更新

 

  按缓存级别分:

  无分级缓存、二级缓存

 

  其他:

  数据缓存、页面缓存……

 

  一般来说,大部分人使用缓存都是直接key-value,这样种操作简单方便,无需太多的算法去处理。而这样操作对于记录集合比较大的数据(当然不能直接缓存大型或超大型数据)来说,频繁的进行数据存取转换也会消耗不少资源,所以有时需要在这个基础上再加个二级缓存,将NOSQL缓存中读取出来的数据载入IIS缓存中,程序直接编写代码调用,只有相关值更改时再重新加载一次,这样就减少了对大数据转换的性能损耗,当然程序的复杂度就大大提升了很多。

  对于使用二级缓存或依赖其他缓存的缓存来说,经常更新或频繁更新影响是最大的,程序写的不好直接会造成性能几何级的下降(因为每一次更新都需要同步更新相关的所有缓存)。

 

  目前系统使用缓存状况

  目前我的框架使用的是二级缓存,首先是使用Redis缓存存储各种表记录和值,然后对于某些数据量不小,修改不多但使用相对比较频繁的数据,为了减少从Redis缓存中不停的读取出来后进行反序列化操作,会从Redis缓存中读取出来后将它存储到IIS缓存中,当这些数据有更新时会实时同步更新IIS缓存中的数据,这些代码都封装在逻辑层中,统一使用模板生成,方便快捷。

  将业务数据量大的模块进行了分割,每天按不同属性生成N个临时表,第二天凌晨会执行定时任务将对这些临时表进行分析处理,去除无效数据后统一更新到历史表中(历史表按月生成)。业务数据分割后,每个表的记录量都很少,它们都会存储到相应的缓存中给前后端、服务、Socket等接口进行共同调用处理,目前是不同服务器所有功能共用一个缓存服务。各种系统服务会将常用的数据或记录存储到指定的缓存中,减少跨临时表查询操作或全表数据查询操作。有些功能只使用当天要用到的一些最新数据,旧数据不再使用不需要参与查询,也会使用单独的缓存来进行存储,缓存存储按固定前缀+有规则的后缀进行管理。

  前端页面则直接缓存在Redis中。

  ……

 

  缓存处理存在问题

  除了前面所讲的缓存问题外,我们后端更新某些数据时(比如商品资料),就必须清除前端所有页面缓存,全面重新生成(因为很多页面都会展示商品相关信息),由于没有一个综合管理缓存的框架,在更新时就会将一些不必删除的缓存也同步清除了。而在某些时候缓存被很多其他缓存所依赖时,清除该缓存也会清除一些多余的缓存,而不是精确定位。对于动态生成的可变后缀的缓存,在某些时候无法传递后缀参数时,将很难同步更新这些缓存内容。

 

  缓存处理解决思路

  对于出现上面的一些问题,在综合考虑后,想写个独立的缓存处理插件来处理这些问题。主要通过配置来将缓存直接绑定数据表、字段、记录Id、关联缓存、页面缓存等关联内容,在这些缓存更新时同步清除对应的缓存模块,以便其他缓存重启缓存载入程序来加载相应数据到缓存中。在清除时有针对性,而不会跨界清空多余的缓存。不知大家有什么好的建议?

 

  下面是我的一些解决思路:

  1、首先缓存插件必须是一个独立的程序

  2、调用必须通过统一的接口来进行处理

  3、缓存关联必须通过配置来实现绑定

  4、缓存命名必须符合一定的规范

 

  具体实现办法:

  获取缓存:Get Cache => Check null
=> Load =>
Save(保存时会执行存储数据的检查,这里开发时要小心,避免出现死循环)
=> Return Cache
(即取缓存时必须检查指定缓存是否为空,为空时调用Load接口载入数据到缓存——Load函数功能由操作方实现,使用配置+IoC来调用,IoC配置文件和接口文件可以用T4模板直接生成——,然后将数据存储到缓存中,最后返回所要的缓存;当然如果缓存不为空时直接返回缓存)

  存储数据:Save Cache => Save =>
Check Relevance => Delete Relevance Cache
(即存储数据时,首先将数据保存到缓存中,然后读取配置信息检查该缓存与那些缓存关联,如果存在关联关系的缓存,则同步清除这些缓存,以便下次获取这些缓存时能重新加载)

  删除缓存:Delete Cache => Delete
=> Check Relevance => Delete Relevance
Cache(删除时执行递归调用,按正常来说,这种关联应该不会太深)

  设置缓存参数:Set
(修改缓存插件的一些全局配置)

 

  给外部直接调用的只有Get/Save/Delete,需要外部程序实现的接口暂定为Load这一个,里面实现数据加载的代码

  在配置时,缓存依赖必须单向,避免出现死循环(可写程序检查配置)

  要处理好动态后缀缓存的处理,能通过参数控制智能判断缓存的关联。比如名称为tablename_id的缓存,在执行Load时会将id截取出来传递给操作函数,那么载入时就只加载该id的记录;

  对于更新频繁的数据,比如页面点击计数等,如果需要用到缓存的,需要独立出来存取和更新,以免更新时执行同步清除功能

  可以通过Set来开启或关闭Load、Delete
Relevance Cache功能等

  

 

  由于工作时间繁忙,本随笔断断续续写了好长时间,有些想法和思路没有及时记下来都忘了,暂时想到这么多,思路也不是很成熟,不知大家有什么好的建议?这种处理模式是否存在什么问题?欢迎大家出来拍砖

 

 

 版权声明:

  本文由AllEmpty原创并发布于博客园,欢迎转载,未经本人同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。如有问题,可以通过1654937@qq.com
联系我,非常感谢。

 

  发表本编内容,**为了和大家共同学习共同进步,有兴趣的朋友可以加加Q群:327360708
,大家一起探讨。

 

  更多内容,敬请观注博客:http://www.cnblogs.com/EmptyFS/

 

相关文章