前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >缓存穿透、击穿、雪崩的成因及解决方案

缓存穿透、击穿、雪崩的成因及解决方案

作者头像
用户7353950
发布2024-05-10 18:27:23
970
发布2024-05-10 18:27:23
举报
文章被收录于专栏:IT技术订阅IT技术订阅
缓存穿透的成因 缓存穿透是指查询的数据在数据库中根本不存在,因此也不会存在于缓存中。正常情况下,第一次请求查不到数据不会有问题,但在高并发场景下,如果大量的这类请求持续不断地发起,每次都会直接穿透缓存去查询数据库,这不仅浪费了数据库资源,而且可能导致数据库因为承载不了如此大的请求量而崩溃。 解决方案 一种常用的解决方案是在查询数据库为空的情况下,也将空值存入缓存,设置一个较短的有效期(比如几分钟),这样可以抵挡住恶意的连续请求,保护数据库。 Java代码示例 以下是一个基于Spring Boot整合Redis的简单示例,展示了如何处理缓存穿透的情况: import org.springframework.cache.annotation.Cacheable; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @Service public class UserService { private final RedisTemplate<String, Object> redisTemplate; public UserService(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } // 使用自定义注解处理缓存穿透 @Cacheable(value = "users", unless = "#result == null") public User getUserById(String id) { // 先尝试从缓存中获取用户 User user = (User) redisTemplate.opsForValue().get(id); // 如果缓存中没有找到用户 if (user == null) { // 去数据库查询 user = queryFromDatabase(id); // 数据库查询结果为空,为了避免缓存穿透,将空值暂时存入缓存 if (user == null) { redisTemplate.opsForValue().set(id, "", 5, TimeUnit.MINUTES); // 设置一个短时效的空值 } else { // 若数据库中有数据,则正常存入缓存 redisTemplate.opsForValue().set(id, user); } } return user; } private User queryFromDatabase(String id) { // 这里模拟数据库查询,实际开发中调用数据库查询的方法 // ... return dbQueryResult; // 假设这是从数据库查询得到的结果 } } 在上述代码中,我们使用了Spring Cache的`@Cacheable`注解,并添加了一个条件`unless = "#result == null"`,这意味着只有当查询结果非空时才会缓存结果。如果数据库查询结果为空,则会将一个临时空值存入缓存,以防止后续的同样请求再次穿透到数据库。注意要根据业务需求合理设定空值缓存的过期时间。

缓存击穿的成因 缓存击穿是指在高并发场景下,某个热点数据的缓存突然失效(如缓存过期),而这时恰好有大量的并发请求来访问这个刚刚失效的key,所有请求都无法从缓存中获取到数据,进而都涌向数据库,导致数据库瞬时压力过大,这就是所谓的“击穿”。尤其是在数据更新并不频繁的情况下,这种集中性的数据库查询压力可能导致数据库响应变慢,甚至宕机。 解决方案 - Java代码示例(使用Redis分布式锁) 下面是一个基于Redis实现分布式锁,用于解决缓存击穿问题的基本Java代码框架: import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import java.util.Collections; @Service public class CacheService { private final StringRedisTemplate redisTemplate; private final RedisScript<Long> luaLockScript; public CacheService(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; luaLockScript = new DefaultRedisScript<>(// 定义Lua脚本,用于获取分布式锁 "if redis.call('exists', KEYS[1]) == 0 then " + "redis.call('hset', KEYS[1], ARGV[1], 1);" + "redis.call('pexpire', KEYS[1], ARGV[2]); " + "return 1; " + "end;" + "return 0;", Long.class); } public Object getDataFromDBWithLock(String cacheKey) { Boolean locked = acquireLock(cacheKey, "uniqueId"); // 尝试获取锁 if (locked) { try { // 如果获取到锁,则尝试从缓存中获取数据 Object data = getDataFromCache(cacheKey); if (data != null) { return data; } // 缓存未命中,从数据库加载数据 data = loadFromDatabase(cacheKey); // 将数据写入缓存 writeToCache(cacheKey, data); return data; } finally { releaseLock(cacheKey, "uniqueId"); // 无论何时,都要确保最后释放锁 } } else { // 没有获取到锁,等待其他线程完成数据库操作后从缓存中读取 return getDataFromCacheAfterWait(cacheKey); } } private Boolean acquireLock(String key, String uniqueId) { // 调用Lua脚本获取分布式锁,这里假设expireTime是你设置的锁超时时间 Long result = redisTemplate.execute(luaLockScript, Collections.singletonList(key), uniqueId, String.valueOf(expireTime)); return result == 1L; } private void releaseLock(String key, String uniqueId) { // 在实际应用中,释放锁可能涉及到更复杂的逻辑,比如判断持有锁的线程ID一致才能释放 // 这里简化处理,仅作示意 redisTemplate.delete(key); } // 其他辅助方法... private Object getDataFromCache(String cacheKey) { ... } private Object loadFromDatabase(String cacheKey) { ... } private void writeToCache(String cacheKey, Object data) { ... } } 上述代码是一个简化的示例,`loadFromDatabase` 和 `writeToCache` 方法需要根据实际的缓存和数据库操作进行实现。通过这种方式,当缓存失效时,只有一个线程能够获得锁并执行数据库查询,其他线程则等待锁释放后从缓存中读取数据,从而避免了数据库的并发压力。

缓存雪崩的成因 缓存雪崩通常是指缓存层出现了大规模的缓存失效,可能是由于以下原因导致的: 1. 大量缓存集中在同一时刻失效,比如设置了一致的过期时间,到期后大量缓存同时失效。 2. 缓存服务整体宕机,导致所有请求无法通过缓存,直接达到后端数据库。 3. 缓存数据的大规模删除,如误操作清空了大量缓存。 解决方案 1. 随机设置过期时间:让缓存的过期时间分散分布,而不是同时失效,可以减少同时失效带来的压力。 2. 缓存预热:在缓存重建前,提前将热门数据加载到缓存中。 3. 设置二级缓存:即使一级缓存失效,还有二级缓存作为备用,减轻数据库压力。 4. 使用互斥锁:当缓存失效时,只允许一个请求去数据库加载数据,其它请求等待锁释放后从缓存获取。 5. 服务熔断与降级:在缓存雪崩发生时,采取熔断措施,避免请求继续涌入数据库,同时提供降级服务。 6. 缓存高可用架构:搭建缓存集群,具备故障转移能力,防止单点故障引起的服务中断。 Java代码示例(伪代码,仅展示关键思路) #### 随机过期时间设置 import org.springframework.cache.annotation.Cacheable; import java.time.Duration; import java.util.concurrent.TimeUnit; @Service public class ProductService { @Cacheable(value = "products", key = "#id", condition = "#result != null", unless = "#result == null", sync = true, cacheManager = "cacheManager") // sync=true 表示同步方式获取缓存,避免并发时多次查询数据库 public Product getProductById(Long id) { Duration randomExpiration = Duration.ofSeconds(randomBetween(minExpireSecs, maxExpireSecs)); // 查询数据库并获取产品数据 Product product = queryProductFromDatabase(id); // 设置缓存时加上随机过期时间 cacheManager.getCache("products").put(id, product, randomExpiration.toMillis(), TimeUnit.MILLISECONDS); return product; } // ... // 内部实现randomBetween函数生成随机的过期时间区间 } 服务熔断与降级(借助Hystrix或其他熔断工具) import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; @Service public class ProductService { @HystrixCommand(fallbackMethod = "getFallbackProductById") @Cacheable("products") public Product getProductById(Long id) { // 正常查询逻辑 return queryProductFromDatabase(id); } // 当数据库压力过大时,提供降级服务 private Product getFallbackProductById(Long id) { log.error("Cache miss and fallback for productId: {}", id); // 提供默认或缓存的备份数据,或直接返回错误信息 return defaultProduct(); } // ... // 内部实现queryProductFromDatabase和defaultProduct方法 } 实际上,Java代码的具体实现会依赖于使用的具体缓存组件(如Redis、Memcached)以及熔断框架(如Hystrix、Resilience4j)。以上代码旨在表达解决缓存雪崩问题的核心思路,而非完整的生产环境下的代码实现。在实际项目中,还需要考虑更多细节,比如配置管理、并发控制、分布式锁的使用等。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-05-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 IT技术订阅 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档