- 外文名
- cache
- 类 别
- 高速缓存、磁盘缓存
- 原 理
- 程序局部性原理
- 中文名
- 缓存
- 学 科
- 计算机
- 目 的
- 提高数据存取速度
-
2022-05-08 10:30:24
前言
缓存的使用在各种项目中非常普遍,nginx作为一款高效的代理服务器,也提供了强大的缓存机制,试想在一些电商网站中,那些静态的html,js,css文件登数量非常庞大的情况下,加载页面的时候,如果没有缓存的话,页面将会非常慢,在这种情况下,就可以考虑使用nginx提供的缓存功能的配置;
Nginx的web缓存
- Nginx从0.7.48版提供缓存功能。
- Nginx是基于Proxy Store来实现的,其原理是把URL及相关组合当做Key,在使用MD5算法对Key进行哈希,得到硬盘上对应的哈希目录路径,从而将缓存内容保存在该目录中。
- 它可以支持任意URL连接,同时也支持404/301/302这样的非200状态码。Nginx即可以支持对指定URL或者状态码设置过期时间,也可以使用purge命令来手动清除指定URL的缓存。
Nginx缓存设置相关指令
Nginx的web缓存服务主要是使用ngx_http_proxy_module模块相关指令集来完成,接下来把常用的指令做一下总结:
proxy_cache_path
该指定用于设置缓存文件的存放路径
语法格式
proxy_cache_path path [levels=number]
keys_zone=zone_name:zone_size [inactive=time]
[max_size=size];更多相关内容 -
缓存穿透,缓存击穿,缓存雪崩详解及解决办法
2021-12-30 10:38:07大部分情况下,加缓存的目的是:为了减轻数据库的压力,提升系统的性能,如果在项目中引入了缓存,可能会给我们带来的下面这三大问题。看看你中招了没? 1.缓存穿透 1.1 我们是如何用缓存的? 一般情况下,如果有...大部分情况下,加缓存的目的是:为了减轻数据库的压力,提升系统的性能,如果在项目中引入了缓存,可能会给我们带来的下面这三大问题。看看你中招了没?
1.缓存穿透
1.1 我们是如何用缓存的?
一般情况下,如果有用户请求过来,先查缓存,如果缓存中存在数据,则直接返回。如果缓存中不存在,则再查数据库,如果数据库中存在,则将数据放入缓存,然后返回。如果数据库中也不存在,则直接返回失败。
流程图如下:
上面的这张图小伙们肯定再熟悉不过了,因为大部分缓存都是这样用的。
1.2 什么是缓存穿透?
但如果出现以下这两种特殊情况,比如:
-
用户请求的id在缓存中不存在。
-
恶意用户伪造不存在的id发起请求。
这样的用户请求导致的结果是:每次从缓存中都查不到数据,而需要查询数据库,同时数据库中也没有查到该数据,也没法放入缓存。也就是说,每次这个用户请求过来的时候,都要查询一次数据库。
图中标红的箭头表示每次走的路线。
很显然,缓存根本没起作用,好像被穿透了一样,每次都会去访问数据库。
这就是我们所说的:
缓存穿透问题
。如果此时穿透了缓存,而直接数据库的请求数量非常多,数据库可能因为扛不住压力而挂掉。呜呜呜。
那么问题来了,如何解决这个问题呢?
1.3 校验参数
我们可以对用户id做检验。
比如你的合法id是15xxxxxx,以15开头的。如果用户传入了16开头的id,比如:16232323,则参数校验失败,直接把相关请求拦截掉。这样可以过滤掉一部分恶意伪造的用户id。
1.4 布隆过滤器
如果数据比较少,我们可以把数据库中的数据,全部放到内存的一个map中。
这样能够非常快速的识别,数据在缓存中是否存在。如果存在,则让其访问缓存。如果不存在,则直接拒绝该请求。
但如果数据量太多了,有数千万或者上亿的数据,全都放到内存中,很显然会占用太多的内存空间。
那么,有没有办法减少内存空间呢?
答:这就需要使用
布隆过滤器
了。布隆过滤器底层使用bit数组存储数据,该数组中的元素默认值是0。
布隆过滤器第一次初始化的时候,会把数据库中所有已存在的key,经过一些列的hash算法(比如:三次hash算法)计算,每个key都会计算出多个位置,然后把这些位置上的元素值设置成1。
之后,有用户key请求过来的时候,再用相同的hash算法计算位置。
-
如果多个位置中的元素值都是1,则说明该key在数据库中已存在。这时允许继续往后面操作。
-
如果有1个以上的位置上的元素值是0,则说明该key在数据库中不存在。这时可以拒绝该请求,而直接返回。
使用布隆过滤器确实可以解决缓存穿透问题,但同时也带来了两个问题:
-
存在误判的情况。
-
存在数据更新问题。
先看看为什么会存在误判呢?
上面我已经说过,初始化数据时,针对每个key都是通过多次hash算法,计算出一些位置,然后把这些位置上的元素值设置成1。
但我们都知道hash算法是会出现hash冲突的,也就是说不同的key,可能会计算出相同的位置。
上图中的下标为2的位置就出现了hash冲突,key1和key2计算出了一个相同的位置。
如果有几千万或者上亿的数据,布隆过滤器中的hash冲突会非常明显。
如果某个用户key,经过多次hash计算出的位置,其元素值,恰好都被其他的key初始化成了1。此时,就出现了误判,原本这个key在数据库中是不存在的,但布隆过滤器确认为存在。
如果布隆过滤器判断出某个key存在,可能出现误判。如果判断某个key不存在,则它在数据库中一定不存在。
通常情况下,布隆过滤器的误判率还是比较少的。即使有少部分误判的请求,直接访问了数据库,但如果访问量并不大,对数据库影响也不大。
此外,如果想减少误判率,可以适当增加hash函数,图中用的3次hash,可以增加到5次。
其实,布隆过滤器最致命的问题是:如果数据库中的数据更新了,需要同步更新布隆过滤器。但它跟数据库是两个数据源,就可能存在数据不一致的情况。
比如:数据库中新增了一个用户,该用户数据需要实时同步到布隆过滤。但由于网络异常,同步失败了。
这时刚好该用户请求过来了,由于布隆过滤器没有该key的数据,所以直接拒绝了该请求。但这个是正常的用户,也被
拦截
了。很显然,如果出现了这种正常用户被拦截了情况,有些业务是无法容忍的。所以,布隆过滤器要看实际业务场景再决定是否使用,它帮我们解决了缓存穿透问题,但同时了带来了新的问题。
1.5 缓存空值
上面使用布隆过滤器,虽说可以过滤掉很多不存在的用户id请求。但它除了增加系统的复杂度之外,会带来两个问题:
-
布隆过滤器存在误杀的情况,可能会把少部分正常用户的请求也过滤了。
-
如果用户信息有变化,需要实时同步到布隆过滤器,不然会有问题。
所以,通常情况下,我们很少用布隆过滤器解决缓存穿透问题。其实,还有另外一种更简单的方案,即:
缓存空值
。当某个用户id在缓存中查不到,在数据库中也查不到时,也需要将该用户id缓存起来,只不过值是空的。这样后面的请求,再拿相同的用户id发起请求时,就能从缓存中获取空数据,直接返回了,而无需再去查一次数据库。
优化之后的流程图如下:
关键点是不管从数据库有没有查到数据,都将结果放入缓存中,只是如果没有查到数据,缓存中的值是空的罢了。
2. 缓存击穿问题
2.1 什么是缓存击穿?
有时候,我们在访问热点数据时。比如:我们在某个商城购买某个热门商品。
为了保证访问速度,通常情况下,商城系统会把商品信息放到缓存中。但如果某个时刻,该商品到了过期时间失效了。
此时,如果有大量的用户请求同一个商品,但该商品在缓存中失效了,一下子这些用户请求都直接怼到数据库,可能会造成瞬间数据库压力过大,而直接挂掉。
流程图如下:
那么,如何解决这个问题呢?
2.2 加锁
数据库压力过大的根源是,因为同一时刻太多的请求访问了数据库。
如果我们能够限制,同一时刻只有一个请求才能访问某个productId的数据库商品信息,不就能解决问题了?
答:没错,我们可以用
加锁
的方式,实现上面的功能。伪代码如下:
try { String result = jedis.set(productId, requestId, "NX", "PX", expireTime); if ("OK".equals(result)) { return queryProductFromDbById(productId); } } finally{ unlock(productId,requestId); } return null;
在访问数据库时加锁,防止多个相同productId的请求同时访问数据库。
然后,还需要一段代码,把从数据库中查询到的结果,又重新放入缓存中。办法挺多的,在这里我就不展开了。
2.3 自动续期
出现缓存击穿问题是由于key过期了导致的。那么,我们换一种思路,在key快要过期之前,就自动给它续期,不就OK了?
答:没错,我们可以用job给指定key自动续期。
比如说,我们有个分类功能,设置的缓存过期时间是30分钟。但有个job每隔20分钟执行一次,自动更新缓存,重新设置过期时间为30分钟。
这样就能保证,分类缓存不会失效。
此外,在很多请求第三方平台接口时,我们往往需要先调用一个获取token的接口,然后用这个token作为参数,请求真正的业务接口。一般获取到的token是有有效期的,比如24小时之后失效。
如果我们每次请求对方的业务接口,都要先调用一次获取token接口,显然比较麻烦,而且性能不太好。
这时候,我们可以把第一次获取到的token缓存起来,请求对方业务接口时从缓存中获取token。
同时,有一个job每隔一段时间,比如每隔12个小时请求一次获取token接口,不停刷新token,重新设置token的过期时间。
2.4 缓存不失效
此外,对于很多热门key,其实是可以不用设置过期时间,让其永久有效的。
比如参与秒杀活动的热门商品,由于这类商品id并不多,在缓存中我们可以不设置过期时间。
在秒杀活动开始前,我们先用一个程序提前从数据库中查询出商品的数据,然后同步到缓存中,提前做
预热
。等秒杀活动结束一段时间之后,我们再
手动删除
这些无用的缓存即可。3. 缓存雪崩问题
3.1 什么是缓存雪崩?
前面已经聊过缓存击穿问题了。
而缓存雪崩是缓存击穿的升级版,缓存击穿说的是某一个热门key失效了,而缓存雪崩说的是有多个热门key同时失效。看起来,如果发生缓存雪崩,问题更严重。
缓存雪崩目前有两种:
-
有大量的热门缓存,同时失效。会导致大量的请求,访问数据库。而数据库很有可能因为扛不住压力,而直接挂掉。
-
缓存服务器down机了,可能是机器硬件问题,或者机房网络问题。总之,造成了整个缓存的不可用。
归根结底都是有大量的请求,透过缓存,而直接访问数据库了。
那么,要如何解决这个问题呢?
3.2 过期时间加随机数
为了解决缓存雪崩问题,我们首先要尽量避免缓存同时失效的情况发生。
这就要求我们不要设置相同的过期时间。
可以在设置的过期时间基础上,再加个1~60秒的随机数。
实际过期时间 = 过期时间 + 1~60秒的随机数
这样即使在高并发的情况下,多个请求同时设置过期时间,由于有随机数的存在,也不会出现太多相同的过期key。
3.3 高可用
针对缓存服务器down机的情况,在前期做系统设计时,可以做一些高可用架构。
比如:如果使用了redis,可以使用哨兵模式,或者集群模式,避免出现单节点故障导致整个redis服务不可用的情况。
使用哨兵模式之后,当某个master服务下线时,自动将该master下的某个slave服务升级为master服务,替代已下线的master服务继续处理请求。
3.4 服务降级
如果做了高可用架构,redis服务还是挂了,该怎么办呢?
这时候,就需要做服务降级了。
我们需要配置一些默认的兜底数据。
程序中有个全局开关,比如有10个请求在最近一分钟内,从redis中获取数据失败,则全局开关打开。后面的新请求,就直接从配置中心中获取默认的数据。
当然,还需要有个job,每隔一定时间去从redis中获取数据,如果在最近一分钟内可以获取到两次数据(这个参数可以自己定),则把全局开关关闭。后面来的请求,又可以正常从redis中获取数据了。
需要特别说一句,该方案并非所有的场景都适用,需要根据实际业务场景决定。
-
-
什么是缓存雪崩、击穿、穿透?
2022-03-13 17:55:28什么是缓存雪崩、击穿、穿透? 用户的数据一般都是存储于数据库,数据库的数据是落在磁盘上的,磁盘的读写速度可以说是计算机里最慢的硬件了。 当用户的请求,都访问数据库的话,请求数量一上来,数据库很容易就奔溃...什么是缓存雪崩、击穿、穿透?
用户的数据一般都是存储于数据库,数据库的数据是落在磁盘上的,磁盘的读写速度可以说是计算机里最慢的硬件了。
当用户的请求,都访问数据库的话,请求数量一上来,数据库很容易就奔溃的了,所以为了避免用户直接访问数据库,会用 Redis 作为缓存层。
因为 Redis 是内存数据库,我们可以将数据库的数据缓存在 Redis 里,相当于数据缓存在内存,内存的读写速度比硬盘快好几个数量级,这样大大提高了系统性能。
引入了缓存层,就会有缓存异常的三个问题,分别是缓存雪崩、缓存击穿、缓存穿透。
这三个问题也是面试中很常考察的问题,我们不光要清楚地知道它们是怎么发生,还需要知道如何解决它们。
话不多说,发车!
缓存雪崩
通常我们为了保证缓存中的数据与数据库中的数据一致性,会给 Redis 里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。
那么,当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
可以看到,发生缓存雪崩有两个原因:
- 大量数据同时过期;
- Redis 故障宕机;
不同的诱因,应对的策略也会不同。
大量数据同时过期
针对大量数据同时过期而引发的缓存雪崩问题,常见的应对方法有下面这几种:
- 均匀设置过期时间;
- 互斥锁;
- 双 key 策略;
- 后台更新缓存;
1. 均匀设置过期时间
如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。
2. 互斥锁
当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。
3. 双 key 策略
我们对缓存数据可以使用两个 key,一个是主 key,会设置过期时间,一个是备 key,不会设置过期,它们只是 key 不一样,但是 value 值是一样的,相当于给缓存数据做了个副本。
当业务线程访问不到「主 key 」的缓存数据时,就直接返回「备 key 」的缓存数据,然后在更新缓存的时候,同时更新「主 key 」和「备 key 」的数据。
4. 后台更新缓存
业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
事实上,缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”,而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内,业务线程读取缓存失败就返回空值,业务的视角就以为是数据丢失了。
解决上面的问题的方式有两种。
第一种方式,后台线程不仅负责定时更新缓存,而且也负责频繁地检测缓存是否有效,检测到缓存失效了,原因可能是系统紧张而被淘汰的,于是就要马上从数据库读取数据,并更新到缓存。
这种方式的检测时间间隔不能太长,太长也导致用户获取的数据是一个空值而不是真正的数据,所以检测的间隔最好是毫秒级的,但是总归是有个间隔时间,用户体验一般。
第二种方式,在业务线程发现缓存数据失效后(缓存数据被淘汰),通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。这种方式相比第一种方式缓存的更新会更及时,用户体验也比较好。
在业务刚上线的时候,我们最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情。
Redis 故障宕机
针对 Redis 故障宕机而引发的缓存雪崩问题,常见的应对方法有下面这几种:
- 服务熔断或请求限流机制;
- 构建 Redis 缓存高可靠集群;
1. 服务熔断或请求限流机制
因为 Redis 故障宕机而导致缓存雪崩问题时,我们可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,不用再继续访问数据库,从而降低对数据库的访问压力,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。
服务熔断机制是保护数据库的正常允许,但是暂停了业务应用访问缓存服系统,全部业务都无法正常工作
为了减少对业务的影响,我们可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务,等到 Redis 恢复正常并把缓存预热完后,再解除请求限流的机制。
2. 构建 Redis 缓存高可靠集群
服务熔断或请求限流机制是缓存雪崩发生后的应对方案,我们最好通过主从节点的方式构建 Redis 缓存高可靠集群。
如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。
缓存击穿
我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。
应对缓存击穿可以采取前面说到两种方案:
- 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
缓存穿透
当发生缓存雪崩或击穿时,数据库中还是保存了应用要访问的数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透就不一样了。
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
缓存穿透的发生一般有这两种情况:
- 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
- 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
应对缓存穿透的方案,常见的方案有三种。
- 第一种方案,非法请求的限制;
- 第二种方案,缓存空值或者默认值;
- 第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;
第一种方案,非法请求的限制
当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
第二种方案,缓存空值或者默认值
当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。
我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。
即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
那问题来了,布隆过滤器是如何工作的呢?接下来,我介绍下。
布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
布隆过滤器会通过 3 个操作完成标记:
- 第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值;
- 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。
- 第三步,将每个哈希值在位图数组的对应位置的值设置为 1;
举个例子,假设有一个位图数组长度为 8,哈希函数 3 个的布隆过滤器。
在数据库写入数据 x 后,把数据 x 标记在布隆过滤器时,数据 x 会被 3 个哈希函数分别计算出 3 个哈希值,然后在对这 3 个哈希值对 8 取模,假设取模的结果为 1、4、6,然后把位图数组的第 1、4、6 位置的值设置为 1。当应用要查询数据 x 是否数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中。
布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。
所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据。
总结
缓存异常会面临的三个问题:缓存雪崩、击穿和穿透。
其中,缓存雪崩和缓存击穿主要原因是数据不在缓存中,而导致大量请求访问了数据库,数据库压力骤增,容易引发一系列连锁反应,导致系统奔溃。不过,一旦数据被重新加载回缓存,应用又可以从缓存快速读取数据,不再继续访问数据库,数据库的压力也会瞬间降下来。因此,缓存雪崩和缓存击穿应对的方案比较类似。
而缓存穿透主要原因是数据既不在缓存也不在数据库中。因此,缓存穿透与缓存雪崩、击穿应对的方案不太一样。
我这里整理了表格,你可以从下面这张表格很好的知道缓存雪崩、击穿和穿透的区别以及应对方案。
参考资料:
1.《极客时间:Redis核心技术与实战》
-
https://github.com/doocs/advanced-java/blob/main/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md
-
https://medium.com/@mena.meseha/3-major-problems-and-solutions-in-the-cache-world-155ecae41d4f
-
Redis——缓存穿透、缓存击穿、缓存雪崩、分布式锁
2021-07-26 13:10:001.缓存穿透 1.1 什么是缓存穿透? 1.2 缓存穿透的解决方案 2.缓存击穿 2.1 什么是缓存击穿? 2.2 缓存击穿的解决方啊 3.缓存雪崩 3.1 什么是缓存雪崩? 3.2 缓存雪崩的解决方案 4.分布式锁 4.1 使用setnx ...文章目录:
1.缓存穿透
1.1 什么是缓存穿透?
当我们访问某个key时,这个key对应的数据在数据源并不存在,每次针对这个key的请求从缓存中都获取不到,那么这些请求都会压到数据源(DB),从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
当我们访问某个key的时候,此时会去redis缓存中查找,但是此时redis缓存中并不存在这个key,那么它就会去数据库中查找。而我们每次请求都是在查找这个key → redis缓存中没有 → DB中查找,反反复复就使得应用服务器压力变大、redis命中率降低,同时过多的非正常url请求也会造成数据库崩溃。
1.2 缓存穿透的解决方案
- 对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。
- 设置可访问的名单(白名单):使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
- 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
- 进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。
2.缓存击穿
2.1 什么是缓存击穿?
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
当我们查询某个key对应的数据时,它是存在于redis中的,但是它过期了,也就是说无法从redis中获取这个数据了,那么此时会去数据库中查找(这是单一请求的时候)。如果这个key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。也就是说:大量并发请求都是如此,redis中缓存数据过期了,这些请求全部转到数据库中查找,这就会造成后端数据库崩溃。
2.2 缓存击穿的解决方啊
(1)预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的存活时长,避免在高并发期间这些数据过期。
(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长。
(3)使用锁:
- 就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db。
- 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key。
- 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;
- 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。
3.缓存雪崩
3.1 什么是缓存雪崩?
缓存雪崩与缓存击穿的区别在于:缓存雪崩是针对很多key而言的缓存,而缓存击穿则是针对某一个key。
正常情况下,访问应该是下图这样:👇👇👇
缓存雪崩发生是说:当我们访问多个key对应的数据时,这些key在redis中都过期(无法获取)了,那么这些大量的高并发请求就会转到后端DB中去查找访问,进而造成了数据库的崩溃现象。
缓存失效时的雪崩效应对底层系统的冲击非常可怕!
3.2 缓存雪崩的解决方案
- 构建多级缓存架构:nginx缓存 + redis缓存 +其他缓存(ehcache等)。
- 使用锁或队列:用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况。
- 设置过期标志更新缓存:记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
- 将缓存失效时间分散开:比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
4.分布式锁
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
分布式锁主流的实现方案:
1. 基于数据库实现分布式锁
2. 基于缓存(Redis等)
3. 基于Zookeeper
每一种分布式锁解决方案都有各自的优缺点:
1. 性能:redis最高
2. 可靠性:zookeeper最高
这里,我们就基于redis实现分布式锁。
4.1 使用setnx + del实现分布式锁的添加与释放
对某个key进行上锁:setnx
对某个key进行解锁:del
下面的截图案例:首先redis中没有users这个key,我们使用 setnx 对这个key进行加锁操作,值为10;之后我们尝试再次setnx,可以看到返回值是0,说明此时不能再修改users的值,也就是说之前操作对users加了锁,其他人就无法操作users,只能等待释放users的锁,也就是del命令。这里del users释放了锁之后,就可以再次对users加锁操作了。
这里对应的Java代码就如下图所示。
@GetMapping("/testLock") public void testLock(){ //1获取锁,setne Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111"); //2获取锁成功、查询num的值 if(lock){ Object value = redisTemplate.opsForValue().get("num"); //2.1判断num为空return if(StringUtils.isEmpty(value)){ return; } //2.2有值就转成成int int num = Integer.parseInt(value+""); //2.3把redis的num加1 redisTemplate.opsForValue().set("num", ++num); //2.4释放锁,del redisTemplate.delete("lock"); }else{ //3获取锁失败、每隔0.1秒再获取 try { Thread.sleep(100); testLock(); } catch (InterruptedException e) { e.printStackTrace(); } } }
4.2 分布式锁优化之设置锁的过期时间
上面的案例中,我们的确给users加了锁,加锁之后,其他客户端也无法再操作users。
但是这里应该会出现一个问题:setnx刚好获取到锁,此时当前获取到锁的这个服务器的业务逻辑出现了异常(造成了redis崩溃),这就会导致users的锁无法释放,而其他服务器此时仍然无法操作users。
解决方案:对需要上锁的key设置过期时间,到了过期时间,无论如何自动释放锁。
1. 首先想到通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
2. 在set时指定过期时间(推荐),在上锁的同时,设置锁的过期时间。(setnx + setex)
这里我们使用 set users 10 nx ex 10 来对users上锁,上锁的同时设置它的过期时间为10s。
//1获取锁,setne Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111", 10, TimeUnit.SECONDS);
4.3 分布式锁优化之UUID防误删
上面设置锁的过期时间案例中,仍然存在一些问题。
场景:如果业务逻辑的执行时间是10s。执行流程如下
- a的业务逻辑没执行完,此时已经执行了5秒,5秒后锁被自动释放。但是此时a服务器出现了卡顿情况(业务逻辑并未执行完),导致a服务器自动释放了锁。
- 在a释放锁之后,b和c服务器将会争夺锁,假设此时b获取到锁,执行它的业务逻辑,5秒后锁被自动释放。
- 在b执行的这5秒业务逻辑期间,a服务器又恢复正常了,那么它此时肯定要继续执行自己的业务逻辑,但是a发现此时锁被b占有了,而b正在执行业务逻辑,那么此时a就会手动释放b上的锁(因为a已经自动释放过一次锁了,无法自动,就只能手动释放)。
最终等于没锁的情况。
面对这样的场景问题,解决方案是:为锁设置一个唯一值(例如:uuid),释放锁之前获取这个值,判断要释放的锁是否是自己的锁。
@GetMapping("/testLock") public void testLock(){ String uuid = UUID.randomUUID().toString(); //1获取锁,setne Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.SECONDS); //2获取锁成功、查询num的值 if(lock){ Object value = redisTemplate.opsForValue().get("num"); //2.1判断num为空return if(StringUtils.isEmpty(value)){ return; } //2.2有值就转成成int int num = Integer.parseInt(value+""); //2.3把redis的num加1 redisTemplate.opsForValue().set("num", ++num); //2.4释放锁,del //判断比较uuid值是否一样 String lockUuid = (String)redisTemplate.opsForValue().get("lock"); if(lockUuid.equals(uuid)) { redisTemplate.delete("lock"); } }else{ //3获取锁失败、每隔0.1秒再获取 try { Thread.sleep(100); testLock(); } catch (InterruptedException e) { e.printStackTrace(); } }
4.4 分布式锁优化之LUA脚本保证删除的原子性
经过了上面的案例:UUID防误删,它仍然存在一些问题。看如下描述:
此时有a、b两个操作,a先对users上了锁,执行它的业务逻辑,执行完毕之后,a执行释放锁操作,在这个期间发生了问题:首先比较uuid是否一样,比较完一样证明这是自己的锁,而正要释放删除锁的时候(此时还未删除),锁到达了过期时间、自动释放了。。。此时b就获取到了锁,执行b的业务逻辑。因为之前a的删除锁操作并未做完,所以此时a仍然会执行锁的删除操作,那么自然而然就把b上的锁给删除了。(疑问:a为何能删除b的锁呢?不是已经添加了uuid吗? 这里注意:a在执行删除锁操作时,已经进行了uuid比较,比较结果是一样证明是自己的锁;那么b此时获取的这个锁就是a的锁,当然可以删!!!)
造成这种情况的原因就是:无法保证删除锁操作的原子性。
解决方案:👇👇👇
@GetMapping("testLockLua") public void testLockLua() { //1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中 String uuid = UUID.randomUUID().toString(); //2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除! String skuId = "25"; // 访问skuId 为25号的商品 100008348542 String locKey = "lock:" + skuId; // 锁住的是每个商品的数据 // 3 获取锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS); // 第一种: lock 与过期时间中间不写任何的代码。 // redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间 // 如果true if (lock) { // 执行的业务逻辑开始 // 获取缓存中的num 数据 Object value = redisTemplate.opsForValue().get("num"); // 如果是空直接返回 if (StringUtils.isEmpty(value)) { return; } // 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在! int num = Integer.parseInt(value + ""); // 使num 每次+1 放入缓存 redisTemplate.opsForValue().set("num", String.valueOf(++num)); /*使用lua脚本来锁*/ // 定义lua 脚本 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; // 使用redis执行lua执行 DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(script); // 设置一下返回值类型 为Long // 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型, // 那么返回字符串与0 会有发生错误。 redisScript.setResultType(Long.class); // 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。 redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid); } else { // 其他线程等待 try { // 睡眠 Thread.sleep(1000); // 睡醒了之后,调用方法。 testLockLua(); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
Mybatis 中的一级缓存与二级缓存
2022-01-19 23:57:32Mybatis 中的一级缓存与二级缓存 -
缓存系列:缓存雪崩的解决思路
2022-03-24 22:51:18缓存,已经是一个老生常谈的技术了,在高并发读的情况下对于读服务来说可谓是抗流量的银弹。 今天我们就来谈谈:缓存雪崩。 -
Caffeine缓存
2020-11-08 12:59:29在本文中,我们来看看 Caffeine — 一个高性能的 Java 缓存库。 缓存和 Map 之间的一个根本区别在于缓存可以回收存储的 item。 回收策略为在指定时间删除哪些对象。此策略直接影响缓存的命中率 — 缓存库的一个重要... -
微服务20_多级缓存01:JVM进程缓存
2022-04-06 11:52:18微服务20_多级缓存01:JVM进程缓存前言:什么是多级缓存?1、传统缓存的问题:2、多级缓存方案一、JVM进程缓存(Tomcat内部编写进程缓存)1、导入商品案例1.安装mysql2.导入Demo3. 商品查询页面 来调用controller的... -
分布式数据:缓存技术
2021-12-26 19:31:05分布式数据:缓存技术前言什么是分布式缓存?Redis 分布缓存原理Memcached 分布式缓存原理对比分析知识扩展:除了分布式存储中的缓存,还有计算机体系结构和网络中的缓存, 它们又分别是什么呢?总结 前言 分布式... -
Spring分布式缓存
2021-06-06 18:11:31什么是分布式缓存 在实际开发场景中,往往单机应用无法满足当前的需求,需要对项目进行分布式部署,由此每个项目中的缓存都是属于自己独立服务的,并不能共享,其次当某个服务更新了缓存,其他服务并不知道,当用户... -
Redis: 缓存过期、缓存雪崩、缓存穿透、缓存击穿(热点)、缓存并发(热点)、多级缓存、布隆过滤器
2019-08-18 16:34:241.缓存过期 缓存过期:在使用缓存时,可以通过TTL(Time To Live)设置失效时间,当TTL为0时,缓存失效。 为什么要设置缓存的过期时间呢? 一、为了节省内存 例如,在缓存中存放了近3年的10亿条博文数据,但是经常被... -
如何使用 Redis 缓存?
2022-04-20 21:39:05对于 Redis 来讲,作为缓存使用,是我们在业务中经常使用的,这里总结下,Redis 作为缓存在业务中的使用。 旁路缓存 Cache Aside(旁路缓存)策略以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读... -
27 Redis 缓存污染问题
2021-12-11 17:12:5527 Redis 缓存污染问题前言一、如何解决缓存污染问题二、LRU 缓存策略三、LFU 缓存策略的优化总结 前言 应用 Redis 缓存时,如果能缓存会被反复访问的数据,那就能加速业务应用的访问。 如果发生了缓存污染,缓存... -
23 Redis 的旁路缓存
2021-12-10 20:53:0023 Redis 的旁路缓存前言一、缓存的特征二、Redis 缓存处理请求的两种情况三、Redis 作为旁路缓存的使用操作四、缓存的类型总结 前言 Redis 提供了高性能的数据存取功能,广泛应用在缓存场景中,既能有效地提升... -
chrome缓存清除扩展程序,通过js清除chrome缓存的api演示
2014-09-01 15:39:30chrome缓存清除扩展程序,通过js清除chrome缓存的api演示 -
redis系列之——缓存穿透、缓存击穿、缓存雪崩
2020-06-26 12:43:35系统龟速运行,你以为加一个缓存就没事了?图样图森破!今天就说说redis作为缓存遇到的常见问题:缓存穿透、缓存击穿、缓存雪崩。 解决高并发问题的其中一项措施是使用缓存,而通常的技术选型就是redis。 用户访问... -
HTTP系列之:HTTP缓存
2021-09-01 22:10:56为了提高网站的访问速度和效率,我们需要设计各种各样的缓存,通过缓存可以避免不必要的额外数据传输和请求,从而提升网站的请求速度。对于HTTP协议来说,本身就自带有HTTP缓存。 今天我们就深入探讨一下HTTP中的... -
Activiti流程定义缓存源码分析4-缓存使用redis
2020-02-04 21:15:52摘要:activiti缓存使用redis与flowable使用redis存储缓存道理一样,本文以activiti使用redis作为缓存介质为例进行说明。关于其他工作流引擎使用reids缓存流程定义也可以参考本文。 上面我们详细分析了关于... -
Android 缓存机制
2021-07-31 14:03:15一、Android 中的缓存策略 一般来说,缓存策略主要包含缓存的添加、获取和删除这三类操作。如何添加和获取缓存这个比较好理解,那么为什么还要删除缓存呢?这是因为不管是内存缓存还是硬盘缓存,它们的缓存大小都是... -
浏览器缓存机制详解
2022-01-25 10:12:25一、为什么需要缓存 在前端开发中,我们主要追求的是性能和用户体验。对于一个网站查看性能最简单的 方式就是打开网站的速度。而一个好的缓存策略可以大大提升网站的性能。使得已经 下载后的资源被重复利用。减少... -
缓存系列:缓存穿透的解决思路
2022-03-13 23:03:01上次我们讨论了在分布式系统下的缓存架构体系,从浏览器缓存到客户端缓存,再到CDN缓存,再到反向代理缓存,再到本地缓存,再到分布式缓存。整个链路中有非常多的缓存。 如果面试官问你说:给你一个机会,你有什么... -
前端缓存 (http缓存 与 本地缓存)
2022-04-10 13:16:06前端缓存主要是分为http缓存...在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求,缓存不命中时,都会向服务器发送请求来获取资源。 在实际的缓存机制中,强缓存策略和协商缓存策略 -
浏览器缓存详解
2021-01-12 18:28:22搞懂前端缓存 总的来说: 如果开启了Service Worker首先会从Service Worker中拿 如果新开一个以前打开过的页面缓存会从Disk Cache中拿(前提是命中强缓存) 刷新当前页面时浏览器会根据当前运行环境内存来决定是从 ... -
本地缓存:为什么要用本地缓存?用它会有什么问题?
2022-04-04 11:13:00随着不断的发展,这一架构也产生了改进,在一些场景下可能单纯使用Redis类的远程缓存已经不够了,还需要进一步配合本地缓存使用,例如Guava cache或Caffeine,从而再次提升程序的响应速度与服务性能。于是,就产生了... -
nginx缓存以及清除缓存
2021-04-18 10:57:01缓存 缓存的基本思想是利用客户端访问的时间局限性,将客户端访问过的内容做一个副本,在一定时间内存放到本地,当改数据下次被访问时,不必连接到后端服务器反复去查询数据,而是由本地保存的副本响应数据。 ... -
关于RecyclerView的缓存
2022-02-26 16:00:03RecyclerView有4级缓存如下: 层级 缓存变量 缓存名 容量 数据结构 缓存用途 1 mChangeScrap 可见缓存 x(无限制) ArrayList 快速知道哪个可见的View被移除还是被添加 2 ... -
前端基础-浏览器缓存/HTTP缓存机制(面试常考)
2021-03-28 15:07:55浏览器缓存/HTTP缓存机制(面试常考) ...缓存过程分析 缓存规则 1.强制缓存 1.1Expires 1.2 Cache-Control 2. 缓存存储 ...强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存 -
SpringBoot整合Spring Cache,简化分布式缓存开发
2021-07-16 13:44:04这篇博文,我们介绍,SpringCache,以及SpringCache是如何来统一不同的缓存技术以高效便捷的方式接入到项目中,最后,深入讲解SpringCache是如何解决缓存击穿,缓存穿透,缓存雪崩的,还有哪些不足。 -
Redis 缓存穿透、缓存击穿、缓存雪崩
2020-10-19 09:55:53缓存穿透是指当用户在查询一条数据的时候,而此时数据库和缓存却没有关于这条数据的任何记录,而这条数据在缓存中没找到就会向数据库请求获取数据。用户拿不到数据时,就会一直发请求,查询数据库,这样会对数据库的... -
Spring三级缓存详解
2022-02-16 14:36:31Spring三级缓存是为了解决对象间的循环依赖问题。