目次
弁言
1. 缓存穿透
1.1 什么是缓存穿透?
示例:
1.2 缓存穿透的缘故因由
1.3 缓存穿透的办理方案
1.3.1 缓存空对象
1.3.2 布隆过滤器(Bloom Filter)
1.3.3 参数校验
2. 缓存击穿
2.1 什么是缓存击穿?
示例:
2.2 缓存击穿的缘故因由
2.3 缓存击穿的办理方案
2.3.1 互斥锁(Mutex Lock)
2.3.2 永不逾期 + 配景更新
2.3.3 缓存预热
3. 缓存雪崩
3.1 什么是缓存雪崩?
示例:
3.2 缓存雪崩的缘故因由
3.3 缓存雪崩的办理方案
3.3.1 设置随机逾期时间
3.3.2 多级缓存
3.3.3 限流与降级
4. 缓存穿透、缓存击穿与缓存雪崩的区别
5. 最佳实践
6. 总结
弁言
在使用 Redis 作为缓存体系时,缓存穿透、缓存击穿和缓存雪崩是三个常见的标题。它们不光会影响体系的性能,还可能导致数据库压力过大以致体系瓦解。本文将深入探究这三种标题的界说、缘故因由、办理方案以及最佳实践,并通过 Java 代码示例 帮助读者全面明白并有用应对这些标题。
1. 缓存穿透
1.1 什么是缓存穿透?
缓存穿透是指查询一个 不存在的数据,导致哀求直接穿透缓存层,直接访问数据库。由于数据库中也不存在该数据,因此每次哀求都会绕过缓存,直接访问数据库,从而导致数据库压力过大。
示例:
- 用户哀求一个不存在的商品 ID,缓存中没有该数据,哀求直接打到数据库。
- 恶意攻击者故意哀求大量不存在的数据,导致数据库压力激增。
1.2 缓存穿透的缘故因由
- 恶意攻击:攻击者故意哀求大量不存在的数据。
- 业务逻辑标题:业务代码未对哀求参数举行校验,导致非法哀求直接访问数据库。
1.3 缓存穿透的办理方案
1.3.1 缓存空对象
当查询数据库发现数据不存在时,将空效果(如 null)缓存到 Redis 中,并设置一个较短的逾期时间。如许,后续雷同的哀求可以直接从缓存中获取空效果,制止直接访问数据库。
- import redis.clients.jedis.Jedis;
- public class CachePenetration {
- private Jedis redis;
- private Database db;
- public CachePenetration(Jedis redis, Database db) {
- this.redis = redis;
- this.db = db;
- }
- public String getData(String key) {
- // 从缓存中获取数据
- String data = redis.get(key);
- if (data != null) {
- return "NULL".equals(data) ? null : data; // 返回空结果
- }
- // 从数据库中查询数据
- data = db.query(key);
- if (data == null) {
- redis.setex(key, 300, "NULL"); // 缓存空对象,过期时间 300 秒
- return null;
- }
- redis.setex(key, 3600, data); // 缓存真实数据,过期时间 1 小时
- return data;
- }
- }
复制代码 1.3.2 布隆过滤器(Bloom Filter)
布隆过滤器是一种概率型数据布局,用于快速判定一个元素是否存在于聚会合。它可以有用过滤掉不存在的数据哀求,制止缓存穿透。
- 优点:内存占用少,查询服从高。
- 缺点:存在肯定的误判率(False Positive),但可以通过调解参数低沉误判率。
- import com.google.common.hash.BloomFilter;
- import com.google.common.hash.Funnels;
- import redis.clients.jedis.Jedis;
- public class CachePenetration {
- private Jedis redis;
- private Database db;
- private BloomFilter<String> bloomFilter;
- public CachePenetration(Jedis redis, Database db) {
- this.redis = redis;
- this.db = db;
- this.bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 1000000, 0.001);
- }
- public String getData(String key) {
- // 使用布隆过滤器判断 key 是否存在
- if (!bloomFilter.mightContain(key)) {
- return null; // 如果 key 不在布隆过滤器中,直接返回
- }
- // 从缓存中获取数据
- String data = redis.get(key);
- if (data != null) {
- return data;
- }
- // 从数据库中查询数据
- data = db.query(key);
- if (data == null) {
- redis.setex(key, 300, "NULL"); // 缓存空对象
- return null;
- }
- redis.setex(key, 3600, data); // 缓存真实数据
- return data;
- }
- }
复制代码 1.3.3 参数校验
在业务逻辑中对哀求参数举行校验,过滤掉非法哀求。比方,查抄商品 ID 是否为正整数,大概是否符合某种格式。
- public class CachePenetration {
- private Jedis redis;
- private Database db;
- public CachePenetration(Jedis redis, Database db) {
- this.redis = redis;
- this.db = db;
- }
- private boolean validateKey(String key) {
- try {
- int id = Integer.parseInt(key);
- return id > 0; // 检查 key 是否为正整数
- } catch (NumberFormatException e) {
- return false;
- }
- }
- public String getData(String key) {
- if (!validateKey(key)) {
- return null; // 非法请求直接返回
- }
- // 其他逻辑...
- return null;
- }
- }
复制代码 2. 缓存击穿
2.1 什么是缓存击穿?
缓存击穿是指 某个热门数据在缓存中逾期,同时有大量并发哀求访问该数据,导致所有哀求直接访问数据库,从而导致数据库压力激增。
示例:
- 某个热门商品的缓存逾期,同时有大量用户哀求该商品,导致数据库压力激增。
2.2 缓存击穿的缘故因由
- 热门数据逾期:某个热门数据的缓存逾期。
- 高并发哀求:大量并发哀求同时访问该热门数据。
2.3 缓存击穿的办理方案
2.3.1 互斥锁(Mutex Lock)
在缓存失效时,使用互斥锁确保只有一个线程去加载数据,其他线程等待加载完成后再从缓存中获取数据。
- import redis.clients.jedis.Jedis;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
- public class CacheBreakdown {
- private Jedis redis;
- private Database db;
- private Lock lock = new ReentrantLock();
- public CacheBreakdown(Jedis redis, Database db) {
- this.redis = redis;
- this.db = db;
- }
- public String getData(String key) {
- // 从缓存中获取数据
- String data = redis.get(key);
- if (data != null) {
- return data;
- }
- // 尝试获取锁
- if (lock.tryLock()) {
- try {
- // 从数据库中查询数据
- data = db.query(key);
- if (data != null) {
- redis.setex(key, 3600, data); // 更新缓存
- }
- } finally {
- lock.unlock(); // 释放锁
- }
- return data;
- } else {
- try {
- Thread.sleep(100); // 等待其他线程加载数据
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- return getData(key); // 重试
- }
- }
- }
复制代码 2.3.2 永不逾期 + 配景更新
对于热门数据,可以设置缓存永不逾期,并通事配景使命定期更新缓存。
- import redis.clients.jedis.Jedis;
- import java.util.concurrent.Executors;
- import java.util.concurrent.ScheduledExecutorService;
- import java.util.concurrent.TimeUnit;
- public class CacheBreakdown {
- private Jedis redis;
- private Database db;
- public CacheBreakdown(Jedis redis, Database db) {
- this.redis = redis;
- this.db = db;
- // 启动后台任务定期更新缓存
- ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
- scheduler.scheduleAtFixedRate(this::updateCache, 0, 1, TimeUnit.HOURS);
- }
- public String getData(String key) {
- // 从缓存中获取数据
- String data = redis.get(key);
- if (data != null) {
- return data;
- }
- // 从数据库中查询数据
- data = db.query(key);
- if (data != null) {
- redis.set(key, data); // 缓存永不过期
- }
- return data;
- }
- private void updateCache() {
- String hotData = db.queryHotData();
- redis.set("hot_data", hotData); // 更新缓存
- }
- }
复制代码 2.3.3 缓存预热
在体系启动或低峰期,提前加载热门数据到缓存中,制止缓存击穿。
- import redis.clients.jedis.Jedis;
- public class CacheBreakdown {
- private Jedis redis;
- private Database db;
- public CacheBreakdown(Jedis redis, Database db) {
- this.redis = redis;
- this.db = db;
- preheatCache();
- }
- private void preheatCache() {
- String hotData = db.queryHotData();
- redis.set("hot_data", hotData); // 缓存预热
- }
- }
复制代码 3. 缓存雪崩
3.1 什么是缓存雪崩?
缓存雪崩是指 大量缓存数据在同一时间失效,导致大量哀求直接访问数据库,从而导致数据库压力激增以致瓦解。
示例:
- 缓存中的数据设置了雷同的逾期时间,导致大量数据在同一时间失效。
- Redis 实例宕机,导致所有缓存失效。
3.2 缓存雪崩的缘故因由
- 缓存会合失效:缓存中的数据设置了雷同的逾期时间。
- Redis 实例宕机:Redis 服务不可用,导致所有缓存失效。
- 热门数据失效:某些热门数据的缓存失效,导致大量哀求直接访问数据库。
3.3 缓存雪崩的办理方案
3.3.1 设置随机逾期时间
为缓存数据设置随机的逾期时间,制止大量缓存数据在同一时间失效。
- import redis.clients.jedis.Jedis;
- import java.util.Random;
- public class CacheAvalanche {
- private Jedis redis;
- private Database db;
- private Random random = new Random();
- public CacheAvalanche(Jedis redis, Database db) {
- this.redis = redis;
- this.db = db;
- }
- public void setCache(String key, String value) {
- int expireTime = 3600 + random.nextInt(600); // 过期时间在 1 小时到 1 小时 10 分钟之间
- redis.setex(key, expireTime, value);
- }
- }
复制代码 3.3.2 多级缓存
使用多级缓存架构(如本地缓存 + Redis 缓存),即使 Redis 缓存失效,本地缓存仍旧可以提供服务。
- import redis.clients.jedis.Jedis;
- import java.util.Map;
- import java.util.concurrent.ConcurrentHashMap;
- public class CacheAvalanche {
- private Jedis redis;
- private Database db;
- private Map<String, String> localCache = new ConcurrentHashMap<>();
- public CacheAvalanche(Jedis redis, Database db) {
- this.redis = redis;
- this.db = db;
- }
- public String getData(String key) {
- // 先从本地缓存获取
- String data = localCache.get(key);
- if (data != null) {
- return data;
- }
- // 再从 Redis 缓存获取
- data = redis.get(key);
- if (data != null) {
- localCache.put(key, data); // 更新本地缓存
- return data;
- }
- // 最后从数据库获取
- data = db.query(key);
- if (data != null) {
- redis.setex(key, 3600, data); // 更新 Redis 缓存
- localCache.put(key, data); // 更新本地缓存
- }
- return data;
- }
- }
复制代码 3.3.3 限流与降级
在缓存雪崩发生时,通过限流和降级机制保护数据库。比方,使用限流工具(如 Redis 的 INCR 下令)限制哀求速率,大概返回默认值或错误页面。
- import redis.clients.jedis.Jedis;
- public class CacheAvalanche {
- private Jedis redis;
- private Database db;
- public CacheAvalanche(Jedis redis, Database db) {
- this.redis = redis;
- this.db = db;
- }
- public String getData(String key) {
- // 限流:每秒最多处理 100 个请求
- if (redis.incr("request_rate") > 100) {
- return "Too many requests, please try again later.";
- }
- // 其他逻辑...
- return null;
- }
- }
复制代码 4. 缓存穿透、缓存击穿与缓存雪崩的区别
特性缓存穿透缓存击穿缓存雪崩界说查询不存在的数据,导致哀求直接访问数据库热门数据缓存失效,导致大量哀求直接访问数据库大量缓存数据在同一时间失效,导致哀求直接访问数据库缘故因由恶意攻击或业务逻辑标题热门数据逾期或高并发哀求缓存会合失效或 Redis 实例宕机影响数据库压力过大数据库压力激增数据库压力激增以致瓦解办理方案缓存空对象、布隆过滤器、参数校验互斥锁、永不逾期 + 配景更新、缓存预热设置随机逾期时间、多级缓存、限流与降级 5. 最佳实践
- 公道设置缓存逾期时间:制止缓存会合失效。
- 使用布隆过滤器:有用防止缓存穿透。
- 多级缓存架构:进步体系的容错本领。
- 限流与降级机制:保护数据库不被压垮。
- 监控
与报警:及时监控 缓存掷中率和数据库负载,及时发现并办理标题。
6. 总结
缓存穿透、缓存击穿和缓存雪崩是 Redis 使用过程中常见的标题,它们会导致数据库压力过大以致体系瓦解。通过公道的筹划和优化,可以有用制止这些标题:
- 缓存穿透:通过缓存空对象、布隆过滤器和参数校验来办理。
- 缓存击穿:通过互斥锁、永不逾期 + 配景更新和缓存预热来办理。
- 缓存雪崩:通过设置随机逾期时间、多级缓存和限流降级来办理。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |