缓存研究

1. 缓存分类

1)Client Cache
在大型网站中,往往会考虑使用Client Cache,如京东网站,通过抓包可以看出它就设置了Cache: Cache-Control:max-age=120,其它的大型网站,如淘宝也都设置了Cache的值。它们都是通过Http 协议头中的字段来控制完成:Cache-Control,Expires,Last-Modified。

这样做的做的目的是减少数量传输,如果缓存有效的情况下,就不需要再次传输内容了。
2)Local JVM Cache
本地缓存是相对分布式缓存而言的,它是将Cache的内容存储在JVM中,如Tomcat中存储session。常用的框架有:Ehcache, ConcurrentHashMap, Guava Cache等。本地缓存的局限是受限于本地内存空间的大小。

它的特点是应用开发和cache在同一进程内部,请求缓存非常快速,缺点是多个应用开发程序无法直接共享缓存。
3)Distributed Cache
分布式缓存最大的特点是不受限单个缓存节点的限制,可以扩展缓存的容量。目前在生产环境上主要使用Memcache,Redis以及基于它们而自己研发的分布式缓存框架。阿里使用Tair分布式缓存框架,京东使用了JimDB分布式缓存框架。也有其它的一些开源分布式缓存框架,但大部分还没有真正应用于大规模的生产环境上。

目前,国内使用分布式缓存,基本上是在使用Redis(中国电信等公司),少数大型互联网公司是基于Redis重新开发一个框架或者自己完全开发一套分布式缓存框架(阿里的开源框架Tair)。

2. 缓存关注问题及解决策略

一般而言,我们主要关注缓存以下的问题。
1) 缓存大小:内存空间大小有限,缓存不可能无限大;
2) 淘汰算法:由1)中可知,有些缓存内容需被覆盖,需要一种淘汰机制来保证哪些被覆盖。一般用LRU算法来实现;
3) 过期:缓存的内容什么时候过期。如Tomcat会开一个线程来监控session的过期。

用一句话来概括, 即多久更新,如何更新

解决策略
在这里只讨论淘汰的策略。一般而言有两种策略:一是定时,到了一定的时间间隔,缓存就失效。它的优点是简单,缺点是灵活性不够;另一个是如果有更新操作时,需要判断是否引进缓存失效,它的优点是灵活,但是处理逻辑相对复杂。

在分布式缓存环境中,面临三个比较大的难题。
1) 数据一致性。为了保证系统的高可用性,往往会有多个缓存副本,这样多个缓存副本可能就不一致了。还有为了尽量提高缓存命中率,缓存也是分层:全局缓存,二级缓存。他们是存在继承关系,需要考虑继承关系的缓存之间的一致性问题。另外还有缓存与数据库的一致性问题。
2) 缓存雪崩。当缓存系统重启或者所有缓存在同一时刻失效(比如某些系统为了提高速度,会在系统启动是统一将大部分数据刷到缓存中,此时如果设置缓存时间都是24小时,那24小时过后,那就悲剧)时,应用系统由于扛不住压力而直接挂掉。
3) 缓存穿透。查询一个必然不存在的数据,查询一个必然不存在的key,每次都会访问DB,如果有人恶意破坏,那么很可能直接对DB造成影响。

解决策略
缓存雪崩和缓存穿透是从性能出发点考虑的,而数据一致性是数据的实时性和真实时,在这里讨论数据一致性的解决方法。而数据一致性又分为三种不同的情况,一般继承关系的缓存用得还不太多,暂时不去分析它。剩下的就是多个副本缓存的一致性和缓存与数据库的一致性问题了。

副本缓存的一致性问题可以由分布式缓存框架来解决,如Redis集群在牺牲性能的情况下可以保证数据的一致性,这个需要在性能和数据一致性之间进行权衡。

剩下讨论缓存与数据库的一致性问题。主要的策略如下:
1) Cache Miss Reload。数据在数据库中进行CUD操作时,将缓存设置成失效;
2) Update Then SetCache。在缓存的数据更新的同时也触发程序更新缓存;
3) Compensation Mechanism。有些时候缓存的更新不一定能够成功,也有可能会有脏数据进入缓存,如果要确保数据‘绝对’一致性,我们可以采取适当的补偿机制,如定时从数据库的值更新到缓存,或者在更新缓存失败时,插入失败日志,定时重新执行缓存更新等;
4) Reload(Rebuilt)All。有些查询对象的写远大于读,那么如果一定要缓存它的话,那可能就要以牺牲一部分的数据实时性为代价了,我们一般采用定时程序 Reload或Rebuilt所有的缓存。

3.缓存适用场景

缓存并不是一个系统中必须的,特别是写操作特别频繁时。我们希望缓存那些不经常变化且难得获取的数据。

1)主要缓存那些比较难得获取的数据,如查询数据库的记录、xml解析、远程获取IO数据等;
2)主要缓存那些以读为主的数据,数据不会变化得太频繁。

4.最基本的缓存设计思路

它的思想比较简单,一个请求过来,首先要缓存中查找,如果没有找到,再到数据库中去查询,然后将数据库中的结果存储在缓存,这样在后面如果再次查询想再的请求,就不必去数据库中查询了,直接到Cache中去取就行了。
这样设计思路适用的场景是数据变化不频繁,如果缓存的数据变化太变了,每次还是要从数据库中取最新的数据,这样系统的性能反而下降了,因为每次要到缓存中去查找,尽管查询Key-Value很快,但它毕竟还是要花时间的。

如果缓存中的数据有变化,可以参考 数据一致性中的解决策略来做。

这个缓存设计还存在几个常见的问题,下面分别讨论这些常见的问题并提出相应的解决策略。
1) DB穿透问题分析
第一次缓存中没有数据,必定要穿透到DB层拿数据并更新缓存。如果并发量特别大的情况下,在这一秒,系统就撑不住了。
还有一个是有人恶意查询不存在的Key。

解决方案
假设数据量不太大,我们可以考虑在系统初始化的时候缓存数据。如果数据量比较多,初始化缓存会给后台带来一定的压力。

2) 并发访问同一个缓存失效问题
有时候如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,这也
可能造成DB压力过大。

解决方案
如果多个请求访问同一个缓存,如果Key不存在,就加锁,去获取DB的数据并入缓存,然后解锁,其它的进程发现有锁就等待,发现解锁后就返回缓存里的数据。

3) 缓存失效问题(缓存雪崩)
在高并发情况下,应用中存在了大量的缓存,一般我们都会给这些缓存设置一个超时时
间(TTL),比如设置为一个小时。缓存建立好过了一个小时,这些缓存都会失效,于是这时候应用就必须重新建立各种各样的缓存。上边加锁只能解决一个缓存的重复建立问题,缓存雪崩则是各种不同的缓存重新建立。

解决方案
不让所有的缓存失效时间都设置成相同的值,可以按照随机值来生成,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。


5.实时数据更新缓存的设计思路

一般而言,缓存策略是不支持实时更新的,如果一定要实现这样的功能,可以考虑异步更新的方式来实现。例如,统计不同页面的浏览量(这样的情景常在电商中出现,如商品的浏览量),那么可以这样来实现:请求过来将缓存中的数据进行修改(修改缓存中的数据比操作数据库还是要快的),然后异步更新到数据库中。

这种模式中,也要注意一个问题,就是第一次访问的问题,在缓存中肯定是不存在的,与第一种模式是相似的,解决方法可以提前加载数据,这可以解决一部分的问题。现在假设数据量非常大,不可能提前加载数据。又存在高并发的情况下,如何解决这个问题呢?
其实处理方式与加锁的方式是一样的,在开始多并发的情况下,只有一个请求去后台查询DB,其它的请求等待,这样只会有一个请求去后台查询DB,返回结果后加入缓存中,同时解锁,这样其它的请求马上可以返回结果。

你可能感兴趣的:(java,缓存,缓存,缓存设计)