Redis 缓存穿透雪崩和击穿

在使用 Redis 缓存时,常见的问题包括缓存穿透、缓存雪崩和缓存击穿,以下分别从问题表现、可能原因及解决方法进行阐述:

1. 缓存穿透

  • 问题表现:客户端请求的数据在缓存和数据库中都不存在,每次请求都会穿透缓存直接查询数据库,导致数据库压力增大,甚至可能引发数据库宕机。
  • 可能原因
    • 恶意攻击,如黑客故意请求不存在的数据,频繁访问数据库。
    • 业务数据存在异常,某些数据在正常情况下不会存在于数据库中,但被误请求。
  • 解决方法
    • 布隆过滤器(Bloom Filter):在缓存之前使用布隆过滤器,布隆过滤器可以快速判断某个数据是否存在。如果布隆过滤器判断数据不存在,就直接返回,不再查询数据库。布隆过滤器存在一定的误判率,但可以通过调整参数来降低误判率。例如在 Java 中,可以使用 Google Guava 库中的 BloomFilter
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

// 创建布隆过滤器,预计元素数量为10000,误判率为0.01
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 10000, 0.01);
// 添加元素
bloomFilter.put(1);
// 判断元素是否存在
boolean mightContain = bloomFilter.mightContain(1);
- **缓存空值**:当查询数据库发现数据不存在时,也将空值缓存起来,并设置一个较短的过期时间,这样下次相同请求就可以直接从缓存获取空值,而不会穿透到数据库。
// 假设jedis是Jedis实例
String key = "nonexistent_key";
String valueFromDb = getValueFromDatabase(key);
if (valueFromDb == null) {
    // 缓存空值,过期时间设为1分钟
    jedis.setex(key, 60, "");
    return "";
} else {
    // 缓存正常数据
    jedis.setex(key, 3600, valueFromDb);
    return valueFromDb;
}

2. 缓存雪崩

  • 问题表现:大量的缓存数据在同一时间过期,导致这些过期数据的请求同时穿透到数据库,使数据库瞬间承受巨大压力,甚至可能导致数据库崩溃。
  • 可能原因
    • 缓存数据设置了相同的过期时间,比如在系统初始化时,将大量数据的缓存过期时间都设置为一天,一天后这些缓存同时过期。
    • 缓存服务器重启或故障,导致所有缓存数据丢失,重启后大量请求直接访问数据库。
  • 解决方法
    • 随机过期时间:在设置缓存过期时间时,采用一个随机的时间范围,避免大量缓存同时过期。例如原本过期时间设置为 1 小时,可以改为在 50 分钟到 70 分钟之间随机设置过期时间。
import java.util.Random;
// 假设jedis是Jedis实例
String key = "example_key";
String value = getValueFromDatabase(key);
// 随机过期时间在50到70分钟之间
Random random = new Random();
int expireTime = 50 + random.nextInt(21);
jedis.setex(key, expireTime * 60, value);
- **二级缓存**:使用两层缓存,第一层缓存失效后,先从第二层缓存获取数据。第二层缓存可以设置较长的过期时间或者不设置过期时间。例如,可以使用 Caffeine 作为本地缓存作为二级缓存。
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

// 创建Caffeine缓存
Cache<String, String> caffeineCache = Caffeine.newBuilder()
      .build();

// 假设jedis是Jedis实例
String key = "example_key";
String valueFromRedis = jedis.get(key);
if (valueFromRedis == null) {
    String valueFromCaffeine = caffeineCache.getIfPresent(key);
    if (valueFromCaffeine != null) {
        return valueFromCaffeine;
    } else {
        String valueFromDb = getValueFromDatabase(key);
        if (valueFromDb != null) {
            jedis.setex(key, 3600, valueFromDb);
            caffeineCache.put(key, valueFromDb);
            return valueFromDb;
        }
    }
}
return valueFromRedis;
- **缓存预热**:在系统上线前,提前将部分热点数据加载到缓存中,并设置不同的过期时间,避免上线后大量数据同时过期。

3. 缓存击穿

  • 问题表现:一个热点数据在缓存过期的瞬间,大量并发请求同时访问该数据,这些请求会绕过缓存直接访问数据库,导致数据库压力瞬间增大。
  • 可能原因
    • 热点数据的过期时间设置不合理,或者由于某些原因,热点数据的缓存突然失效。
    • 高并发场景下,对热点数据的访问频率极高,缓存过期时大量请求同时到达。
  • 解决方法
    • 互斥锁:在缓存过期时,使用互斥锁(如 Redis 的 SETNX 命令)保证只有一个请求能去查询数据库并更新缓存,其他请求等待。获取到锁的请求查询数据库并更新缓存后,释放锁,其他请求就可以从缓存中获取数据。
import redis.clients.jedis.Jedis;

// 假设jedis是Jedis实例
String key = "hot_key";
String mutexKey = "mutex:" + key;
String value = jedis.get(key);
if (value == null) {
    // 使用SETNX获取互斥锁
    if (jedis.setnx(mutexKey, "1") == 1) {
        try {
            value = getValueFromDatabase(key);
            if (value != null) {
                jedis.setex(key, 3600, value);
            }
        } finally {
            // 释放互斥锁
            jedis.del(mutexKey);
        }
    } else {
        // 未获取到锁,等待一段时间后重试
        Thread.sleep(100);
        return getValue(key);
    }
}
return value;
- **永不过期**:对于热点数据不设置过期时间,通过后台线程定时更新缓存数据,或者在数据发生变化时主动更新缓存。这样可以避免因缓存过期导致的缓存击穿问题。
// 后台线程定时更新缓存示例
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import redis.clients.jedis.Jedis;

public class CacheUpdater {
    private static final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    private static final String key = "hot_key";
    private static final Jedis jedis = new Jedis("localhost");

    static {
        executorService.scheduleAtFixedRate(() -> {
            String value = getValueFromDatabase(key);
            if (value != null) {
                jedis.set(key, value);
            }
        }, 0, 60, TimeUnit.MINUTES);
    }

    public static String getValue(String key) {
        return jedis.get(key);
    }

    private static String getValueFromDatabase(String key) {
        // 实际从数据库查询逻辑
        return "data from db";
    }
}
#牛客创作赏金赛##找工作如何保持松弛感?#
全部评论

相关推荐

拼多多&nbsp;暑期实习&nbsp;二面,总共用时1h&nbsp;左右,&nbsp;被面试官疯狂拷打,&nbsp;估计凉凉。首先介绍项目,&nbsp;对方完全不感兴趣:&nbsp;你做的这些和后端开发有什么关系?我简单介绍了下后端相关的,面试官可能觉得太简单了,没有继续问。然后就是痛苦的手撕拷打,持续50mins&nbsp;左右。问题1:&nbsp;给你两个班级,&nbsp;每个班级共有&nbsp;k&nbsp;个人,你是班主任,要从每个班级中挑出1个人,使得他们的身高差最小。回答:&nbsp;先排序,&nbsp;然后遍历A班级,二分查找B班级中的第一个大于等于(lower_bound)A班级里的那个&nbsp;的位置,然后比较那个位置和前一个位置。面试官和我不太同频,问我为什么要找第一个大于等于?二分查找不就是找一个位置吗?&nbsp;面试官笑眯眯的问我是第一次接触二分搜索吗?然后计算时间复杂度。感觉完全不同频。问还有更优解法吗?回答双指针,还是固定遍历A,&nbsp;然后另一个指针从B开始找比A大的,然后在和前一个也比较,取最小的。这样就是O(n).面试官提示一下,不要局限在当前这个和前一个比较,换种思路。 然后我一直在思考,对方问我双指针要怎么初始化?&nbsp;&nbsp;因为我还没想好,就没回答。面试官:&nbsp;双指针要怎么初始化?这你都不懂吗。。。?&nbsp;怎么不回答我。我说新思路还没想好,面试官表示刚才那种思路的双指针要怎么初始化?&nbsp;答:都初始化在第一个位置。问题2:&nbsp;两个班级,&nbsp;换成&nbsp;N&nbsp;个班级,&nbsp;每个班级选1个人,&nbsp;要求算出来的人里的&nbsp;max&nbsp;-&nbsp;min&nbsp;最小。答没思路,&nbsp;面试官提示下多个指针?&nbsp;考虑下指针应该如何移动。我想了想,移动最小的那个指针,直到所有指针都走到末尾,每次移动,从这个N个人里面取最大的,最小的,比较。然后算时间复杂度:n个班,每个班k个人,&nbsp;&nbsp;我想整体的数据规模是&nbsp;N&nbsp;=&nbsp;n*k&nbsp;,&nbsp;然后我用&nbsp;N&nbsp;去后续表示。面试官:你为什要定义新的符号N? 算完时间复杂度,问我“从这个N个人里面取最大的,最小的”这部分可以优化吗? 我回答可以使用&nbsp;map&nbsp;(cpp里的),面试官表示你直接说数据结构,不要说语言中的名字。问这个的时间复杂度,答logn面试官:那你开始写吧。写了大概&nbsp;3&nbsp;分钟。&nbsp;他看了眼。问题3:&nbsp;ping&nbsp;100ms&nbsp;&nbsp;,&nbsp;curl&nbsp;http://1.2.3.4:8080/hello&nbsp;&nbsp;需要多少时间?这里我考虑了4次挥手,&nbsp;面试官:需要考虑这个吗?答&nbsp;200ms.&nbsp;问题4:&nbsp;直播间,打赏金额最高的100个用户?&nbsp;你应该如何实现维护?我回答使用&nbsp;redis&nbsp;的&nbsp;zset&nbsp;,&nbsp;&nbsp;可以高效的获取top&nbsp;100.面试官问:这样有什么问题吗?&nbsp;如果用户特别多的情况。我想了一会,也没想出什么问题,回答没什么思路。面试官:用户太多了会有&nbsp;大&nbsp;key&nbsp;问题,&nbsp;zset&nbsp;删除的时候会阻塞几秒。&nbsp;(我不太理解)面试官:你应该考虑怎么优化?答:&nbsp;string&nbsp;配合&nbsp;zset&nbsp;使用,&nbsp;string&nbsp;kv&nbsp;中存&nbsp;user,&nbsp;money,&nbsp;&nbsp;而&nbsp;zset&nbsp;中只维护&nbsp;top&nbsp;100&nbsp;的。同时更新这俩。最后,&nbsp;反问部门业务:拼多多直播带货。技术栈:&nbsp;面试官看我的简历里面,cpp&nbsp;太底层了我们这边不用,golang&nbsp;也不用,主要是&nbsp;java&nbsp;,&nbsp;然后&nbsp;redis,&nbsp;mysql,&nbsp;kafka这些。面试官问我懂不懂二分,&nbsp;我当时多少有点生气💢,&nbsp;不过总的来说面试官人还不错,还算友善,给了很多引导。
查看13道真题和解析 牛客创作赏金赛
点赞 评论 收藏
分享
评论
1
2
分享

创作者周榜

更多
牛客网
牛客企业服务