The All-in-One Note (24)

场景:例如我们系统面临大量的查询请求,我们的数据库后端面对如此频繁的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 会进行如下操作:

得到当前的时间,微秒单位

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpyysp.html