记一次redis缓存击穿问题

本文探讨了高并发场景下Redis缓存击穿问题,通过分析业务场景和代码实现,提出了使用互斥锁synchronized避免缓存击穿的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:

首先先介绍下项目环境:SpringBoot+Redis+JPA等。之所以写这篇文章是为了总结下线上的一个击穿问题,以便于对redis有更为深刻的认识和理解。

这里我就不使用项目中的代码了,使用自己的测试代码来说明主要问题。

一、业务场景:

使用redis的理由是在用户访问量很大的时候,如果一次次去从数据库中读取数据,无疑会增加数据库的负担(重要的数据当然还是要从库中读),但对于诸如用户浏览记录或者xxx列表之类的数据,数据量是巨大的,如果从库中读显然不可行,所以考虑存到redis中,大致的流程是这样,注意:此图和实际业务无关只是为了说明问题

好了接下来来看下代码,由于使用的是springboot因此在整合redis的时候使用了redisTemplete

嗯,看上去没有什么问题,来测试一下,为了模拟环境,我们使用25条线程执行10万条请求,来看是否像我们的预想结果即:如果是第一次查询的话,默认去查数据库,由于之后将信息放到了缓存,因此第二次应该查缓存。

为了验证次问题我将之前在redis中存的数据全部移除,然后我们来执行一下测试程序:

结果:

我们再来看一下redis客户端中有没有缓存到数据,可以看到缓存了数据,说明逻辑是对的。

但是,我们可以看到结果并没有向我们想象的那样第一次查数据库,而是有部分数据都是从数据库中读取的,后面才开始查缓存的,如果并发再大点的话,查数据库的数量会更多,那么我们该如何避免这种情况呢?

二、如何避免缓存击穿

我们先来分析一下问题出在哪里,10w条查询请求进来,由于是第一次查询所以这个时候redis中是不存在缓存数据的,这个时候因为是并发执行,可能有1000条请求进来,发现redis中没有,于是就去读数据库查了出来,然后将数据设置到了redis中,1001条以后的请求进来后发现redis中已经有了缓存,所以就不去读取数据库而是直接去redis中读取缓存,因此就会出现上面的那种结果。

如何避免呢?我们很容易就想到使用互斥锁,即synchronized它可以保证在某一时刻只有一个对象拥有锁,也就是说当第一条请求进入的时候它拥有了一把锁,那么此时请求流程是怎样的呢?看图~

和之前相同的是所有的请求都可以进入到判断是否为空的逻辑,当判断redis为空后去数据库中查的时候,采用synchronized来锁定当前的对象,也就是用户1的请求,这个时候其他的请求只能等,那么用户1接着就会去数据库中读取,然后把读取到的信息放入到redis中去,当它执行完这一系列流程之后,请求2才开始执行,这时它发现redis中已经有了数据,所以就不去数据库中读取,直接从redis中拿出来显示,所以此时就可以避免缓存击穿的问题,先来看下代码:

可以看到this也就是当前对象被锁定,其他对象就无法继续进入执行,接着我们来清除redis中原有的缓存来测试一下结果:

此时的redis中是没有缓存数据的,接着我们来执行程序:

可以看到此刻虽然redis中没有数据,但是只有第一次从数据库中查询,之后的请求都是从缓存中读取出来,我们再来看下redis缓存中:

备注:当然你也可以将synchronized作用在方法上,这样也可以保证结果的正确性,但是需要注意的是这样的话性能会大大降低,因此当你调用方法的时候其他请求会进行等待,此时方法是阻塞的,而且效率也是低下的,而作用在方法块上的效率显然要比方法上好的多。

### Redis缓存击穿、穿透、雪崩及限流的概念 #### 缓存击穿 (Cache Breakdown) 当某个热点键(即访问频率极高的键)突然失效时,如果此时大量并发请求同时到达并尝试获取该键对应的值,则会直接打到数据库上,造成数据库压力骤增甚至崩溃的现象称为缓存击穿。 为了防止这种情况发生,通常采用以下几种方法来应对: - **加锁机制**:对于高并发场景下的热点key操作,可以在读取缓存前加上分布式锁。只有获得锁的服务实例才能去查询DB并将结果写回缓存[^3]。 - **设置合理的过期时间**:避免所有缓存项在同一时刻集体到期,可以通过随机化每个缓存条目的TTL(Time To Live),使得它们不会集中在一个时间段内全部过期[^4]。 - **逻辑过期方案**:除了物理删除外还可以给缓存录附加一个额外的时间戳字段表示其实际有效期;即使超过了这个期限也不会立即清除而是标为已过期状态等待下一次更新后再真正移除。 ```python import time from redis import StrictRedis def get_with_logical_expiry(redis_client, key): value = redis_client.get(key) if not value: return None data, expiry_time_str = value.split('|') current_time = int(time.time()) expiry_time = int(expiry_time_str) if current_time >= expiry_time: # 已经过期,返回None让调用者重新加载数据 return None return data ``` --- #### 缓存穿透 (Cache Penetration) 指的是恶意攻击者故意构造不存在的数据ID进行查询,由于这些ID对应的数据根本就不存在于数据库中,所以每次都会导致穿透整个缓存层直抵底层存储系统,从而浪费资源并可能引发性能瓶颈。 针对这一现象的有效措施有: - **缓存空对象**:对于确实不存在的数据,在第一次查库确认之后将其置入缓存一段时间,这样后续相同的请求就可以直接命中缓存而无需再次访问数据库了。 - **布隆过滤器(Bloom Filter)**:这是一种空间效率非常高的概率型数据结构,能够快速判断某元素是否属于集合之一员。虽然存在一定误判率但是非常适合用来做前置拦截以减少不必要的缓存查找次数[^1]。 ```python class BloomFilter(object): def __init__(self, size=500000, hash_num=7): self.bit_array = bitarray(size) self.bit_array.setall(0) self.hash_functions = [] for i in range(hash_num): seed = random.randint(0, MAX_SEED) self.hash_functions.append(mmh3.Hash(seed)) def add(self, item): for f in self.hash_functions: pos = f(item) % len(self.bit_array) self.bit_array[pos] = True def test(self, item): for f in self.hash_functions: pos = f(item) % len(self.bit_array) if not self.bit_array[pos]: return False return True ``` --- #### 缓存雪崩 (Cache Avalanche) 指因为某些原因导致大量的缓存在同一时间内几乎同时失效,进而引起瞬间爆发式的流量冲击后端服务的情况。为了避免此类事件的发生,建议采取如下预防手段: - **分片处理**:将不同类型的业务按照一定规则划分成多个独立的小模块各自维护自己的私有缓存区域,以此降低全局性灾难发生的可能性[^2]。 - **引入熔断保护机制**:一旦检测到异常高峰负载则自动启动紧急预案——如暂停部分非核心功能直至恢复正常运作为止。 --- #### 流量控制(Limiting Flow Control) 通过配置网关层面的限流组件(例如Nginx或Spring Cloud Gateway中的Rate Limit插件),可以有效地限制单位时间内允许通过的最大请求数目,从而减轻上游服务器的压力并提高系统的整体稳定性。 ```yaml spring: cloud: gateway: routes: - id: rate_limit_route uri: lb://example-service predicates: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 ```
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值