在以前的文章中你应该知道的缓存进化史介绍了爱奇艺的缓存架构和缓存的进化历史。俗话说得好,工欲善其事,必先利其器,有了好的工具确定得知道如何用好这些工具,本篇将介绍如何利用好缓存。java
在使用缓存以前,须要确认你的项目是否真的须要缓存。使用缓存会引入的必定的技术复杂度,后文也将会一一介绍这些复杂度。通常来讲从两个方面来个是否须要使用缓存:git
若是并无上述两个问题,那么你没必要为了增长缓存而缓存。github
缓存又分进程内缓存和分布式缓存两种。不少人包括笔者在开始选缓存框架的时候都感到了困惑:网上的缓存太多了,你们都吹嘘本身很牛逼,我该怎么选择呢?正则表达式
首先看看几个比较经常使用的缓存的比较,具体原理能够参考你应该知道的缓存进化史: 比较项 | ConcurrentHashMap | LRUMap | Ehcache | Guava Cache | Caffeine ---|--- |---|--- |--- |--- 读写性能 | 很好,分段锁|通常,全局加锁|好|好,须要作淘汰操做|很好 淘汰算法 | 无|LRU,通常|支持多种淘汰算法,LRU,LFU,FIFO|LRU,通常|W-TinyLFU, 很好 功能丰富程度 |功能比较简单 | 功能比较单一| 功能很丰富| 功能很丰富,支持刷新和虚引用等|功能和Guava Cache相似 工具大小 | jdk自带类,很小|基于LinkedHashMap,较小|很大,最新版本1.4MB|是Guava工具类中的一个小部分,较小|通常,最新版本644KB 是否持久化 |否 |否|是|否|否 是否支持集群 |否 |否|是|否|否算法
总结一下:若是不须要淘汰算法则选择ConcurrentHashMap,若是须要淘汰算法和一些丰富的API,这里推荐选择Caffeine。sql
这里选取三个比较出名的分布式缓存来做为比较,MemCache(没有实战使用过),Redis(在美团又叫Squirrel),Tair(在美团又叫Cellar)。不一样的分布式缓存功能特性和实现原理方面有很大的差别,所以他们所适应的场景也有所不一样。数据库
比较项 | MemCache | Squirrel/Redis | Cellar/Tair |
---|---|---|---|
数据结构 | 只支持简单的Key-Value结构 | String,Hash, List, Set, Sorted Set | String,HashMap, List,Set |
持久化 | 不支持 | 支持 | 支持 |
容量大小 | 数据纯内存,数据存储不宜过多 | 数据全内存,资源成本考量不宜超过100GB | 能够配置全内存或内存+磁盘引擎,数据容量可无限扩充 |
读写性能 | 很高 | 很高(RT0.5ms左右) | String类型比较高(RT1ms左右),复杂类型比较慢(RT5ms左右) |
总结:若是服务对延迟比较敏感,Map/Set数据也比较多的话,比较适合Redis。若是服务须要放入缓存量的数据很大,对延迟又不是特别敏感的话,那就能够选择Tair。在美团的不少应用中对Tair都有应用,在笔者的项目中使用其存放咱们生成的支付token,支付码,用来替代数据库存储。大部分的状况下二者均可以选择,互为替代。数组
不少人一想到缓存立刻脑子里面就会出现下面的图:缓存
Redis用来存储热点数据,Redis中没有的数据则直接去数据库访问。网络
在以前介绍本地缓存的时候,不少人都问我,我已经有Redis了,我干吗还须要了解Guava,Caffeine这些进程缓存呢。我基本统一回复下面两个答案:
因此若是仅仅是使用Redis,能知足咱们大部分需求,可是当须要追求更高的性能以及更高的可用性的时候,那就不得不了解多级缓存。
对于进程内缓存,其原本受限于内存的大小的限制,以及进程缓存更新后其余缓存没法得知,因此通常来讲进程缓存适用于:
俗话说得好,世界上没有什么是一个缓存解决不了的事,若是有,那就两个。
通常来讲咱们选择一个进程缓存和一个分布式缓存来搭配作多级缓存,通常来讲引入两个也足够了,若是使用三个,四个的话,技术维护成本会很高,反而有可能会得不偿失,以下图所示:
利用Caffeine作一级缓存,Redis做为二级缓存。
对于Caffeine的缓存,若是有数据更新,只能删除更新数据的那台机器上的缓存,其余机器只能经过超时来过时缓存,超时设定能够有两种策略:
对于Redis的缓存更新,其余机器立马可见,可是也必需要设置超时时间,其时间比Caffeine的过时长。
为了解决进程内缓存的问题,设计进一步优化:
经过Redis的pub/sub,能够通知其余进程缓存对此缓存进行删除。若是Redis挂了或者订阅机制不靠谱,依靠超时设定,依然能够作兜底处理。
通常来讲缓存的更新有两种状况:
对于一个更新操做简单来讲,就是先去各级缓存进行删除,而后更新数据库。这个操做有一个比较大的问题,在对缓存删除完以后,有一个读请求,这个时候因为缓存被删除因此直接会读库,读操做的数据是老的而且会被加载进入缓存当中,后续读请求所有访问的老数据。
对缓存的操做不论成功失败都不能阻塞咱们对数据库的操做,那么不少时候删除缓存能够用异步的操做,可是先删除缓存不能很好的适用于这个场景。
先删除缓存也有一个好处是,若是对数据库操做失败了,那么因为先删除的缓存,最多只是形成Cache Miss。
若是咱们使用更新数据库,再删除缓存就能避免上面的问题。可是一样的引入了新的问题,试想一下有一个数据此时是没有缓存的,因此查询请求会直接落库,更新操做在查询请求以后,可是更新操做删除数据库操做在查询完以后回填缓存以前,就会致使咱们缓存中和数据库出现缓存不一致。
为何咱们这种状况有问题,不少公司包括Facebook还会选择呢?由于要触发这个条件比较苛刻。
对比上面4.1的问题来讲这种问题的几率很低,何况咱们有超时机制保底因此基本能知足咱们的需求。若是真的须要追求完美,可使用二阶段提交,可是其成本和收益通常来讲不成正比。
固然还有个问题是若是咱们删除失败了,缓存的数据就会和数据库的数据不一致,那么咱们就只能靠过时超时来进行兜底。对此咱们能够进行优化,若是删除失败的话 咱们不能影响主流程那么咱们能够将其放入队列后续进行异步删除。
你们一听到缓存有哪些注意事项,确定首先想到的是缓存穿透,缓存击穿,缓存雪崩这三个挖坑的小能手,这里简单介绍一下他们具体是什么以及应对的方法。
缓存穿透是指查询的数据在数据库是没有的,那么在缓存中天然也没有,因此,在缓存中查不到就会去数据库取查询,这样的请求一多,那么咱们的数据库的压力天然会增大。
为了不这个问题,能够采起下面两个手段:
2. 制定一些规则过滤一些不可能存在的数据,小数据用BitMap,大数据能够用布隆过滤器,好比你的订单ID 明显是在一个范围1-1000,若是不是1-1000以内的数据那其实能够直接给过滤掉。
对于某些key设置了过时时间,可是其是热点数据,若是某个key失效,可能大量的请求打过来,缓存未命中,而后去数据库访问,此时数据库访问量会急剧增长。
为了不这个问题,咱们能够采起下面的两个手段:
缓存雪崩是指缓存不可用或者大量缓存因为超时时间相同在同一时间段失效,大量请求直接访问数据库,数据库压力过大致使系统雪崩。
为了不这个问题,咱们采起下面的手段:
缓存污染通常出如今咱们使用本地缓存中,能够想象,在本地缓存中若是你得到了缓存,可是你接下来修改了这个数据,可是这个数据并无更新在数据库,这样就形成了缓存污染:
上面的代码就形成了缓存污染,经过id获取Customer,可是需求须要修改Customer的名字,因此开发人员直接在取出来的对象中直接修改,这个Customer对象就会被污染,其余线程取出这个数据就是错误的数据。
要想避免这个问题须要开发人员从编码上注意,而且代码必须通过严格的review,以及全方位的回归测试,才能从必定程度上解决这个问题。
序列化是不少人都不注意的一个问题,不少人忽略了序列化的问题,上线以后立刻报出一下奇怪的错误异常,形成了没必要要的损失,最后一排查都是序列化的问题。列举几个序列化常见的问题:
//jdk 7 class A{ int a; int b; } //jdk 8 class A{ int b; int a; }
序列化的问题必须获得重视,解决的办法有以下几点:
对于大量使用本地缓存的应用,因为涉及到缓存淘汰,那么GC问题一定是常事。若是出现GC较多,STW时间较长,那么一定会影响服务可用性。这一块给出下面几点建议:
不少人对于缓存的监控也比较忽略,基本上线以后若是不报错而后就默认他就生效了。可是存在这个问题,不少人因为经验不足,有可能设置了不恰当的过时时间,或者不恰当的缓存大小致使缓存命中率不高,让缓存就成为了代码中的一个装饰品。因此对于缓存各类指标的监控,也比较重要,经过其不一样的指标数据,咱们能够对缓存的参数进行优化,从而让缓存达到最优化:
上面的代码中用来记录get操做的,经过Cat记录了获取缓存成功,缓存不存在,缓存过时,缓存失败(获取缓存时若是抛出异常,则叫失败),经过这些指标,咱们就能统计出命中率,咱们调整过时时间和大小的时候就能够参考这些指标进行优化。
一个好的剑客没有一把好剑怎么行呢?若是要使用好缓存,一个好的框架也必不可少。在最开始使用的时候你们使用缓存都用一些util,把缓存的逻辑写在业务逻辑中:
上面的代码把缓存的逻辑耦合在业务逻辑当中,若是咱们要增长成多级缓存那就须要修改咱们的业务逻辑,不符合开闭原则,因此引入一个好的框架是不错的选择。
推荐你们使用JetCache这款开源框架,其实现了Java缓存规范JSR107而且支持自动刷新等高级功能。笔者参考JetCache结合Spring Cache, 监控框架Cat以及美团的熔断限流框架Rhino实现了一套自有的缓存框架,让操做缓存,打点监控,熔断降级,业务人员无需关心。上面的代码能够优化成:
对于一些监控数据也能轻松从大盘上看到:
想要真正的使用好一个缓存,必需要掌握不少的知识,并非看几个Redis原理分析,就能把Redis缓存用得炉火纯青。对于不一样场景,缓存有各自不一样的用法,一样的不一样的缓存也有本身的调优策略,进程内缓存你须要关注的是他的淘汰算法和GC调优,以及要避免缓存污染等。分布式缓存你须要关注的是他的高可用,若是其不可用了如何进行降级,以及一些序列化的问题。一个好的框架也是必不可少的,对其若是使用得当再加上上面介绍的经验,相信能让你很好的驾驭住这头野马——缓存。
最后这篇文章被我收录于JGrowing,一个全面,优秀,由社区一块儿共建的Java学习路线,若是您想参与开源项目的维护,能够一块儿共建,github地址为:https://github.com/javagrowing/JGrowing 麻烦给个小星星哟。
若是你以为这篇文章对你有文章,能够关注个人技术公众号,关注以后便可领取,你的关注和转发是对我最大的支持,O(∩_∩)O