缓存
掘金野鸡流,可能有错,看到新的知识点会补充更新
确定是否需要缓存
根据实时性不同将数据分为三级:
1级:对实时性和精确性要求很高,不添加任何缓存,读写操作直接操作数据库。
2级:读多写少的特征,使用 redis进行缓存。
3级:数据量小,频繁读,几乎不修改的特征,使用本地内存进行缓存。
从两个方面判断是否需要使用缓存:
1、CPU占用:若应用需要消耗大量的 cpu去计算且频繁(如正则表达式),就应使用缓存将正则表达式的结果给缓存下来。
2、数据库 IO占用:若数据库连接池比较繁忙,甚至经常报出连接不够的报警,就应该考虑使用缓存。
缓存分类
进程缓存
进程缓存类似于计算机 cpu中使用缓存,减少对内存的直接访问,从而加快访问速度。
已经有 Redis了,为什么还要有 Guava、Caffeine这些进程缓存呢:
1、Redis如果挂了或者使用老版本的 Redis,其会进行全量同步,此时 Redis是不可用的,这个时候我们只能访问数据库,很容易造成雪崩。
2、访问 Redis会有一定的网络 I/O以及序列化反序列化,虽然性能很高但是其终究没有本地方法快,可以将最热的数据存放在本地,以便加快访问速度。
ConcurrentHashMap:适合缓存比较固定不变且数量较小的情况,由于是 jdk自带的类,在各种框架中有大量的使用,如我们可以用来缓存我们反射的 Method,Field等;也可以缓存一些链接,防止其重复建立
LRUMap:如果不想引入第三方包,又想使用淘汰算法淘汰数据,可以使用这个
Ehcache:由于其 jar包很大,较重量级。对于需要持久化和集群的一些功能的,可以选择 Ehcache。
Guava Cache:Guava这个jar包在很多 Java应用程序中都有大量的引入,所以很多时候其实是直接用就好了,并且其本身是轻量级的而且功能较为丰富,在不了解 Caffeine的情况下可以选择 Guava Cache。
Caffeine:命中率高,读写性能上比 Guava Cache好很多,并且其 API和 Guava cache基本一致,甚至会多一点。
总结:若不需要淘汰算法则选择 ConcurrentHashMap,若需要淘汰算法和一些丰富的 API,则推荐使用 Caffeine
分布式缓存
MemCache:吞吐量较大,但是支持的数据结构较少,并且不支持持久化
Redis: 支持丰富的数据结构,读写性能高,但是数据全内存,必须要考虑资源成本,支持持久化
Tair: 支持丰富的数据结构,读写性能较高,部分类型比较慢,理论上容量可以无限扩充
多级缓存
一般我们选择一个进程缓存和一个分布式缓存来搭配做多级缓存,如用 Caffeine做一级缓存,Redis为二级缓存
当应用进程发起请求时:
1、先去 Caffeine中查询,有则直接返回,没有则进行第 2步。
2、再去 Redis中查询,有则返回数据并在 Caffeine中填充此数据,没有则进行第 3步
3、最后去 MySQL中查询,有则返回数据并在 Redis,Caffeine中依次填充此数据
对于 Caffeine缓存更新:如果有数据更新,只能删除更新数据的那台机器上的缓存,其他机器只能通过超时来过期删除缓存
对于 Redis的缓存更新,其他机器立马可见,但是也必须要设置超时时间,过期时间应比 Caffeine的长
可以通过 Redis的 pub/sub来通知其他进程缓存对缓存进行删除。如果 Redis挂了或者订阅机制不靠谱,仍用超时来过期
保证双写时缓存、数据一致性
更新缓存,更新数据库
一般不用,主要原因是要更新缓存,有的缓存并非直接从库中取出使用,而是要经过一系列计算才得出的,此时更新缓存的代价是很高的。且若是有大量的写请求,而很少读请求,此时如果每写一次都更新一下缓存,性能损耗非常大
删除缓存而不是更新缓存,是一个懒加载的思想,只有当它需要被使用时才重新计算
要确认这个缓存到底会不会真正被频繁访问:
一个缓存涉及的表的字段,在 1分钟内就修改了 100次,那么缓存更新 100次;但这个缓存在 1分钟内只被读取了 1次,有大量的冷数据。
若是删除缓存的话,那么在 1分钟内,这个缓存不过就重新计算 1次而已,开销大幅度降低。当要用到缓存时才去算缓存
先删缓存,后更新库
两个并发的读写操作:
1、写操作先进来,把缓存删除了
2、在写操作还未更新数据库时,一个读的请求又进来了,发现没有命中缓存,查库读出老数据
3、写操作更新了数据库
4、读操作把老数据回填回缓存中
先删缓存的好处是:如果对数据库操作失败了,由于先删除的缓存,最多只是造成缓存未命中
先更新库,后删缓存
在没有缓存的情况下,两个并发的读写操作:
1、读操作先进来,发现没有缓存,去数据库中读数据,这个时候因为某种原因卡了,没有及时把数据放入缓存
2、写操作进来了,修改了数据库,删除了缓存
3、读操作恢复,把老数据写进了缓存
删除缓存时,若删除缓存失败,则数据库存的是新数据,而缓存里面的数据还是老数据,导致数据库、缓存不一致
解决方案
延时双删
采用延时双删策略,保证事务提交完以后再进行删除缓存
第一次删除缓存相当于检测下缓存服务是否可用,网络是否有问题,第二次延迟一定时间再次删除缓存,是因为要保证读请求在写请求之前完成
但若采用 MySQL读写分离架构,事物提交后其主从同步会有时间差问题。解决办法是:若是对 Redis进行填充数据的查库操作,就强制将其指向主库进行查询
消息队列
利用消息队列进行删除补偿:
1、请求 A先对数据库进行更新操作
2、删缓存时出错,删除失败
3、此时将 Redis的 key作为消息体发送到消息队列中
4、系统接收到消息队列发送的消息后再次对 Redis进行删除操作
这个方案的缺点是会对业务代码造成大量的侵入,深深的耦合在一起,A要干各种各样的活。如果写操作非常频繁,队列的数据比较多,可能消费会比较慢,导致修改数据库后,隔了一段时间缓存才被删除
最后的优化就是把 MySQL的 binlog利用好,订阅 binlog 服务来对缓存进行操作,对更新操作解耦
缓存坑爹三剑客
缓存穿透
缓存穿透指查询一条数据库和缓存都没有的数据,会一直查询数据库,数据库的访问压力增大,解决方案有以下两种: 1、缓存空对象:代码维护较简单,效果不好 2、布隆过滤器:代码维护复杂,效果很好
缓存空对象
缓存空对象,当请求了缓存和数据库都不存在的数据,第一次请求会跳过缓存直接访问库,且库返回为空,同时也将该空对象进行缓存。当再次访问该空对象时,会直接击中缓存,而不是落入库中
缓存空对象会使缓存中存在很多空对象,浪费内存空间,解决办法是为空对象设置较短的过期时间
布隆过滤器
布隆过滤器是一种基于概率的数据结构,它只能告诉你某个元素一定不在集合内或可能在集合内,特点如下:
1、一个非常大的二进制位数组(只有 0和 1),空间效率和查询效率高,有若干个哈希函数
2、不存在漏报,但可能存在误报,不提供删除方法
3、位数组初始化为 0,不存元素的具体值,将元素哈希后的值对应的数组位置值改为 1
误判率
布隆过滤器的判断准确率和两个因素有关:
1、布隆过滤器大小:越大,误判率就越小,所以说布隆过滤器一般长度都是非常大的。
2、哈希函数的个数:哈希函数的个数越多,误判率就越小
不可删
当删除 z元素之后,对应的下标 10和 13会设置为 0,这样会导致 x和 y元素的下标受影响,导致数据的判断不准确,所以直接不提供删除元素的 api
布隆过滤器的缺点就是要实时维持容器中的数据,对于频繁变化的数据,布隆过滤器要更新其中的数据为最新
缓存击穿
缓存击穿是指一个 key非常热点,大并发集中对这一个点进行访问,当这个 key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大。缓存击穿强调的是并发。
造成缓存击穿的原因有两个:
1、该数据没有人查询过 ,第一次就大并发的访问。(冷门数据)
2、添加了缓存,reids也设置了 key过期的时间,大并发访问时,这个缓存刚好失效(热点数据)
当出现大并发访问时,在查缓存和查数据库的过程中加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接击中缓存,防止缓存击穿
分布式环境中要使用分布式锁,单机的话用普通的锁就够了(synchronized、Lock)
缓存雪崩
缓存雪崩指在某个时间段,缓存集中过期失效,此刻无数的请求直接绕开缓存,直接请求数据库。 造成缓存雪崩的原因有两种:
1、reids宕机 2、同一时间内大量的 key失效
解决方案:
1、搭建高可用的集群,防止单机的 redis宕机
2、设置不同的过期时间,防止大量的 key同一时间内失效