场景:例如我们系统面临大量的查询请求,我们的数据库后端面对如此频繁的IO可能出现性能问题,甚至直接崩溃。就好比CPU和内存之间的关系,CPU处理的太快了,内存速度跟不上,这时最先想到的解决方案就是引入缓存
目标:
加快用户访问速度,提高用户体验
降低后端负载,减少潜在的风险,保证系统平稳
保证数据“尽可能”及时更新
如何保证缓存与数据库的双写一致性要求强一致性(实际很少):
方案:读请求和写请求串行化,串到一个内存队列里去
问题:吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上请求
不要求强一致性:
方案Cache Aside Pattern
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应
更新的时候,先更新数据库,然后再删除缓存
为什么是删除缓存,而不是更新缓存
在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值
这个缓存不一定被频繁访问到
问题1:
先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致
解决思路:
先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中
问题2:
如果采用了上述的先删缓存再更新数据库,一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了
解决思路:
再数据库更新操作还没成功之前,将读请求放入队列(JVM内部队列即可)中等待更新完成,如果请求在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值
缓存可能引入的问题缓存穿透
场景:大量查询一个数据库中没有的key,每次请求都走到了后端数据库,缓存没有起到作用
解决方案:
缓存空对象:查不到的key就在缓存中设置一个空对象由一个特殊的值标识,然后给这个key设置过期时间,一般为5分钟。可能的问题是5分钟内可能数据不一致,可以在新增操作中加入缓存空值的检查
布隆过滤器:查不到的key查询布隆过滤器,返回true证明这个key后端没有值,如果false就查询数据库,有值就更新缓存,没值就更行布隆过滤器(可以利用 Redis 的 Bitmaps 实现布隆过滤器GitHub 类似的方案,这种方法适用于数据命中不高,数据相对固定实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少)
缓存雪崩
场景:缓存直接崩掉了
解决方案:
缓存高可用:Redis Sentinel,Redis Cluster
本地缓存:Ehcache,Guava Cache
请求 DB 限流:限制DB每秒请求 Guava RateLimiter,Sentinel
服务降级:提供默认返回Hystrix、Sentinel
缓存击穿
场景:是指某个极度“热点”数据在某个时间点过期时,恰好在这个时间点对这个 KEY 有大量的并发请求过来
解决方案:
加锁:使用分布式锁(setnx ),保证有且只有一个线程去查询 DB ,并更新到缓存
set key value [ex seconds|px milliseconds] [nx|xx] nx key不存在贼执行,xx key存在则执行
手动过期:缓存上从不设置过期时间,功能上将过期时间存在 KEY 对应的 VALUE 里,如果发现要过期,通过一个后台的异步线程进行缓存的构建,也就是“手动”过期。通过后台的异步线程,保证有且只有一个线程去查询 DB
Redis分布式锁
单节点分布式锁
SET resource_name my_random_value NX PX 30000
Redlock
安全特性:互斥访问,即永远只有一个 client 能拿到锁
免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
容错性:只要大部分 Redis 节点存活就可以正常提供服务
实现
原理
假如起了 5 个 master 节点,分布在不同的机房尽量保证可用性。为了获得锁,client 会进行如下操作:
得到当前的时间,微秒单位