Redis缓存击穿

Redis缓存击穿是指一个热点key(高并发访问的key)在缓存中失效的瞬间,导致大量请求直接落到数据库上,从而给数据库服务器带来巨大压力的情况。

原因分析

  1. 热点key突然过期(可能是缓存策略设置的到期时间到了)。
  2. 大量并发请求同时查询这个key。
  3. 由于缓存失效,所有请求都直接打到了数据库。

解决方案

  1. 设置热点数据永不过期:对于一些热点key,可以设置其永不过期,而是通过后台线程异步更新缓存内容。
public String getDataWithPermanentKey(String key) {
    String data = jedis.get(key);
    if (data == null) {
        // 同步获取锁,单线程加载数据到缓存
        synchronized (key.intern()) {
            // 双重检测,防止多个线程进入同步块
            data = jedis.get(key);
            if (data == null) {
                data = loadDataFromDb(key);
                // 设置数据到redis,但不设置过期时间,使其永不过期
                jedis.set(key, data);
            }
        }
    }
    return data;
}

private String loadDataFromDb(String key) {
    // 数据库查询逻辑
    return "data";
}
  1. 使用互斥锁:当缓存失效时,不是所有请求都去加载数据,而是用一个互斥锁(或者其它排他锁机制)保证只有一个请求去查询数据库然后更新缓存。
public String getDataWithMutex(String key) {
    String data = jedis.get(key);
    if (data == null) {
        String lockKey = "lock:" + key;
        // 尝试获取锁
        String lock = jedis.set(lockKey, "1", "NX", "EX", 5);
        if ("OK".equals(lock)) {
            try {
                // 加锁成功,查询数据库
                data = loadDataFromDb(key);
                jedis.setex(key, 3600, data);  // 假设设置3600秒过期时间
            } finally {
                // 无论如何最后都释放锁
                jedis.del(lockKey);
            }
        } else {
            // 加锁失败,小睡一会儿后重试
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return getDataWithMutex(key);  // 重试
        }
    }
    return data;
}
  1. 设置过期标志更新:对于可能成为热点的数据,可以在缓存中设置两个值,一个是数据的过期时间expire_at,一个是数据值。当读取时,先检查expire_at,如果即将过期,则启动一个后台线程进行更新。
public String getDataWithExpireFlag(String key) {
    // 假设是一个Map对象,包含数据和过期时间戳
    Map<String, Object> dataMap = (Map<String, Object>) jedis.get(key);
    if (dataMap != null) {
        long expireAt = (Long) dataMap.get("expire_at");
        if (System.currentTimeMillis() > expireAt - 30000) {  // 例如提前30秒进行续期
            // 异步更新缓存
            Thread updateThread = new Thread(() -> {
                loadDataFromDb(key); // 加载数据并更新缓存
            });
            updateThread.start();
        }
        return (String) dataMap.get("data");
    } else {
        // 缓存中没有数据,同步查询数据库并设置缓存
        String data = loadDataFromDb(key);
        dataMap = new HashMap<>();
        dataMap.put("data", data);
        dataMap.put("expire_at", System.currentTimeMillis() + 3600000);  // 假设设置1小时过期时间
        jedis.set(key, dataMap);
        return data;
    }
}

注意事项

  • 锁的实现:上面的互斥锁示例简化了锁的实现,使用set命令的NX(Not eXists)和EX(Expire)选项来实现。在分布式环境下,应该使用基于Redis的RedLock算法或其他可靠的分布式锁实现。
  • 锁的粒度:锁的粒度要尽可能小,比如针对每个key加锁,以减少锁的竞争。
  • 重试的策略:加锁失败后的重试应该有合理的策略,如随机的延迟时间。
  • 异常处理:要确保即使在数据加载过程中发生异常,锁也能被正确释放。
  • 线程安全:如果使用异步线程更新缓存,确保操作是线程安全的。
  • 批量预热:对于知道会成为热点的key,在缓存到期之前,可以通过定时任务或其他机制提前重新加载缓存。

以上示例仅为说明如何避免Redis缓存击穿问题,并未考虑所有生产环境的复杂性。在实际生产环境中,应该使用更完善的异常处理和线程安全措施。

你可能感兴趣的:(Redis,redis,缓存)