引子
web页面在服务器端进行渲染,需要根据模板和数据库数据经过运算生成和渲染出最终的页面,这些运算和渲染过程会耗费大量时间和资源,如果将这些运算结果缓存起来放在内存中,下次再请求页面就会直接从缓存中直接读取数据,节省了页面渲染计算时间,从而大大提高了页面的相应速度。目前内存价格相对便宜,通过缓存带来性能方面的收益往往比单纯从代码层面优化性能,高效的和经济实惠的多。
什么是缓存
什么是缓存,缓存其实也是一种代理,作为网站内容展示的代理,用户直接访问缓存内容,而缓存内容来源于后端程序计算, 缓存实际为一个中间代理层。
缓存存在基础和场景
缓存的存在基础是内容的相对不变性,在相对的时间、相对的条件内容不发生变化,这样不用每次都动态计算生成内容。内容变化的频率相对不高,应用对内容实时性要求相对不高,是引入缓存的场景基础。读的操作远大于写是实际的表现。缓存不适合高交互性,对内容实时性要求高的场景。
缓存主要是解决变与不变的问题,变什么时候变的问题,也就是缓存的同步问题。
缓存策略
缓存策略就是缓存如何生成和如何过期的规则定义。
缓存策略定义和业务场景有很大关系如内容展示类网站(新闻类)就会更适合缓存的使用场景。缓存也是种艺术,不同的场景会适合不同的缓存策略,有时候需要拿将几种策略在实际应用中进行对比,通过实际观测和相关数据统计(缓存命中率 缓存停留时间等)来决定哪种策略更适合。
下边就具体介绍下集中常用的缓存策略
基于时间的缓存策略
通过设置cache过期时间来同步缓存内容,预定的时间到了缓存就会自动过期,优点是简单,缺点是会有一定的时间数据不同步。
基于cache key的缓存策略
对于动态网站,网站页面是基于后端数据源结合预定义模板动态渲染生成的,后端数据源一般为数据库(关系型数据库 非关系型数据库),后端的数据变化带来页面内容的的变化,当后端数据发生变化时就会生成新的cache_key从而生成新的cache内容,老的cahe_key和对应的内容会驻留在缓存服务中,只有当缓存服务的空间满了才会触发缓存服务的驱逐操作,缓存才会被清除。
基于cache key的缓存策略的关键点就是如何检测到数据的变化,如何构建cache key。页面的数据源是基于数据库的,可以利用数据库的id和update time时间戳(id为数据唯一标识 update time时间戳为变化标识)组成cache key,数据的任何变化都会改变update time这个时间戳字段,从而生成新的key值,并根据key值重新缓存内容。像memcache和redis等缓存服务会有自己的驱逐策略,会按照最近最少使用的策略自动驱逐缓存,缓存驱逐并不是当缓存空间满了以后才执行的,以memcache为例,他会根据缓存内容的大小分成不同的slab,当前slab空间满的时候就会触发驱逐,这就是为什么你会发现缓存还有大量空间的时候就有cache被驱逐。
对于文档行数据库如elasticsearch等,提供了一个版本字段,当文档数据有任何修改,版本字段都会进行改变(版本号递增),可以利用文档的id和版本字段来组成cache key
基于cache key的缓存策略是每次对象变化就重新写入缓存,旧的缓存对象时在slab满的情况下由缓存服务根据最近最少访问策略来驱逐旧的缓存,这种策略很简单将缓存的清理工作交给了缓存服务,缺点就是有场景会将一些正在使用的cache给驱逐了,反而当前缓存的旧版本的对象会长期滞留在缓存服务中从而引发缓存震荡(同一cache频繁换入换出)。
网页模板会随着开发工作的进行而变动,当模板变化时需要重新生成cache。cache key的值的一部分是基于模板内容的degist md5值来计算的,只有当模板内容改变了,才会生成新的key和缓存内容。计算模板内容degist值也是需要耗费时间的,也可以将其缓存起来,将模板的名称和路径当做key来把degist值缓存起。在实际操作过程中,程序重新启动时(新的relase发生)会检测一次模板内容是否改变来决定是否生成新的模板内容degist。
实际应用中会结合数据库数据值和模板digest值来生成cache key和cache content。
可回收的cache key缓存策略
简单理解就是根据cache key在缓存服务中存储对象,如果对象被修改就用新版本的缓存对象替换旧版本的对象,从而做到基于当前key的缓存对象空间循环利用。
他的核心就是基于被缓存对象定义的cache_key和cache_version,
名词解释:
cache_key: 为被缓存对象的唯一标识,如数据库对象的id
cache_vertion:保存对象的版本信息,追踪对象的变化,如关系数据库对象的update_at 时间戳,文档型数据库的vertion字段。
被缓存的类:需要被缓存的对象。
缓存类:对于缓存内容封装
被缓存的类中需要同时定义cache_key方法和cache_vertion方法,构建一个缓存类来包裹被缓存的对象,设置一个字段为vertion用来存被缓存对象的vertion,一个字段为value存储实际的对象,将这个缓存对象按照cache_key存入缓存服务中(memcahe redis),在读取缓存对象时,先将缓存对象按照cache_key从缓存服务中取出,然后对比当前对象的vertion和缓存对象的vertion是否一致,如果一致就会直接读取缓存对象中的value值,如果不一致就是对象的内容发生了变化从而缓存服务中的缓存对象就是过期缓存,需要根据新的vertion和内容重新构建缓存对象,然后根据cache_key存入缓存服务中。因为是同一个cache_key所以就等于根据cache_key把上一个老版本的缓存对象替换为新版本的缓存对象供读取使用,这样做的优点就是是节省缓存空间,提高缓存驻留时间,预防缓存震荡
动态更新cache内容
通过程序来手动控制缓存的更新,如通过数据库变化调用回调(after save)来重新生成缓存。这样做好处是可以实时改变缓存,让缓存变得实时可控,缺点是大大增加了程序的复杂性。
总结
这些策略的实际使用。往往不是单一的,要根据具体情况,如业务场景、应用场景、程序逻辑、技术框架等结合使用,通过实际的工程试验来实现最适合自己的缓存方案。