1、三种缓存访问方式
① 旁路缓存:读取数据时先从redis中读取,如果存在直接返回;如果不存在则访问数据库,将数据写入redis,之后返回;写数据时会先将数据写入数据库中,写入完成之后再删除redis的缓存,下次访问加载的就是最新的数据了。
② 读写穿透:类似旁路缓存,但是读取写入操作不是由客户端来进行逻辑判断的,而是由缓存中间件去完成,当然redis是不具备这样的功能的。
③ 异步写入:数据写入时先写到缓存中,之后再异步地写到数据库,这可能会带来缓存一致性问题,对于一致性要求不高的情况下可以使用【热点排行榜、点赞、阅读量】。
2、数据一致性问题
先更新数据库,之后删除缓存,可以很大程度上确保数据一致性【并发情景下】。
而不可忽视的是,更新和删除中间可能出现的失败,如果更新数据库成功,但是删除缓存失败也会造成数据不一致的问题,因此这里可以引入消息队列,将删除缓存的任务发送到消息队列中,由指定的消费者去进行,失败就重试。
如果不想在业务层引入消息队列去解决,可以订阅数据库日志(binlog),接着根据日志的变更去删除对应的缓存信息,这样在业务层就只需要更新数据库,其他的操作可以交给订阅日志的中间件去进行,例如 阿里的canal。
结论:推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做。
3、缓存雪崩
指的是原本发送到缓存的大量请求由于缓存失效/缓存宕机导致无法到达,继而请求全部打到数据库上,由于这两者处理数据的吞吐量是有量级上的区别的,因此数据库很容易因此宕机。导致缓存雪崩的原因一般是缓存同时间大批量过期或者缓存实例宕机。
防范手段:
对于键值不设置统一的过期时间,而是在过期时间中加上随机数(1~3分钟),防止缓存数据同时间大量过期,并且随机数的设置不应该影响业务功能,加上一个短的随机时间即可。对于实例宕机的问题,可以采用主从节点集群部署的方式,主节点挂壁了从节点可以顶上来,不至于长时间的大量请求打到数据库上。
发生了缓存雪崩后降低影响的手段:
对于缓存大规模失效情况,首先考虑服务降级,将非核心的业务对缓存的请求暂时不给予正确响应,而是返回预设值或者空值,对于核心业务正常处理,这样可以尽量减少请求的数量,尽可能避免数据库因此宕机,这种手段类似于电商大促期间对于确认收货/评论功能的暂时关闭。
对于缓存宕机的情况,首先考虑服务限流,比如原来允许通过的流量是1w,其中有9k会在缓存中得到响应,只有1k会到数据库,那么现在发生了缓存雪崩,意味着最大可能1w的请求会到数据库,因此我们可以减小允许通过的请求数,即限制流量在1k,这样就可以保证最大也只有1k的流量会到数据库上。最坏的情况下可以考虑服务熔断,即切断该服务的提供,拒绝服务相关请求,避免影响到其他业务的正常运行。
4、缓存击穿
指的是某一时段对于某个热点数据的大量请求都打到数据库上,从而导致数据库宕机危险。这种情况发生在某个热点数据缓存过期的时候,因此解决方案也简单暴力,直接对热点数据不设置过期时间。
5、缓存穿透
缓存雪崩、缓存穿透是数据库中有对应数据的情况,而缓存穿透发生的情境下,缓存没有该数据、数据库也没有该数据,那就代表着请求无法在访问数据库获取数据之后写入缓存中,意味着缓存形同虚设,所有的请求都会到达数据库,但实际上这些请求都是无效的。
引发缓存穿透的原因:
① 业务层误操作,导致数据库和缓存中的数据被删除掉,而请求还是源源不断地进来;
② 恶意攻击,攻击者伪造不存在的key,进行大量的访问,导致缓存穿透。
解决手段:
① 对于首次访问在数据库、缓存中均不存在的数据,缓存一个空值或者0值到中间件,后续的访问就不会打到数据库上,只是请求了缓存,这时要注意设置值的问题,如果是库存之类的应该设置为0;
② 增加一层布隆过滤器,对值的存在情况进行判断,布隆过滤器可以针对值不存在的情况精确的返回判断结果,因此我们可以先请求布隆过滤器,如果判断数据不存在,那么无需进入后续的处理流程,直接在这里返回空值/默认值即可,避免请求到达数据库;
③ 在业务层进行拦截,对于不合法的参数、不合法的请求等进行拦截,不让其进入后续流程;