Redis缓存穿透雪崩击穿解决方案实战:从架构设计到编码实现

Redis缓存三大经典问题与架构设计

Redis作为高性能缓存中间件,在数据库高可用架构中承担着抵挡海量查询流量的核心角色。但缓存引入后,缓存穿透、缓存雪崩、缓存击穿三大问题随之而来。这三个问题本质上是缓存失效后的流量击穿到数据库的差异化表现。本文从架构设计到编码实现,给出经过生产验证的完整解决方案。

三者区别:

| 问题     | 触发条件                    | 本质                    |
|---------|---------------------------|------------------------|
| 穿透    | 查询不存在的数据             | 缓存和DB都没有,请求直达DB |
| 雪崩    | 大量Key同时过期              | 缓存批量失效,流量涌入DB   |
| 击穿    | 热点Key过期瞬间大量并发请求    | 单Key失效,并发击穿到DB   |

缓存穿透解决方案:布隆过滤器与空值缓存

缓存穿透的典型场景:恶意攻击查询不存在的ID,或业务数据尚未写入。解决方案分两层:

方案一:布隆过滤器前置拦截

@Component
public class BloomFilterService {
    
    private final RBloomFilter<Long> bloomFilter;
    
    public BloomFilterService(RedissonClient redisson) {
        this.bloomFilter = redisson.getBloomFilter("user:bloom");
        // 预期插入量100万,误判率0.01%
        bloomFilter.tryInit(1_000_000L, 0.0001);
    }
    
    public boolean mightExist(Long userId) {
        return bloomFilter.contains(userId);
    }
    
    public void addToFilter(Long userId) {
        bloomFilter.add(userId);
    }
}

// 在查询前先过布隆过滤器
@Service
public class UserService {
    
    @Autowired
    private BloomFilterService bloomFilterService;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private UserMapper userMapper;
    
    public User getUserById(Long id) {
        // 第一层:布隆过滤器判断
        if (!bloomFilterService.mightExist(id)) {
            return null;  // 一定不存在,直接返回
        }
        
        // 第二层:查缓存
        String key = "user:" + id;
        User user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
            return user;
        }
        
        // 第三层:查数据库
        user = userMapper.selectById(id);
        if (user != null) {
            redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
        }
        return user;
    }
}

方案二:空值缓存+短TTL

public User getUserById(Long id) {
    String key = "user:" + id;
    User user = (User) redisTemplate.opsForValue().get(key);
    if (user != null) {
        return user instanceof NullUser ? null : user;
    }
    
    user = userMapper.selectById(id);
    if (user != null) {
        redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
    } else {
        // 缓存空值,短TTL防止长期占用内存
        redisTemplate.opsForValue().set(key, NullUser.INSTANCE, 5, TimeUnit.MINUTES);
    }
    return user;
}

缓存雪崩解决方案:随机过期与多级缓存

缓存雪崩的核心问题是大量Key同时过期。解决方案:

方案一:过期时间加随机偏移

public void batchCacheUsers(List<User> users) {
    users.forEach(user -> {
        String key = "user:" + user.getId();
        // 基础过期时间30分钟 + 随机0~10分钟偏移
        long expireSeconds = 30 * 60 + ThreadLocalRandom.current().nextLong(600);
        redisTemplate.opsForValue().set(key, user, expireSeconds, TimeUnit.SECONDS);
    });
}

方案二:多级缓存架构

@Service
public class MultiLevelCacheService {
    
    private final Cache<String, User> localCache = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(5, TimeUnit.MINUTES)  // 本地缓存5分钟
        .recordStats()
        .build();
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private UserMapper userMapper;
    
    public User getUserById(Long id) {
        String key = "user:" + id;
        
        // L1: 本地缓存(Caffeine)
        User user = localCache.getIfPresent(key);
        if (user != null) return user;
        
        // L2: Redis分布式缓存
        user = (User) redisTemplate.opsForValue().get(key);
        if (user != null) {
            localCache.put(key, user);
            return user;
        }
        
        // L3: 数据库
        user = userMapper.selectById(id);
        if (user != null) {
            long expire = 30 * 60 + ThreadLocalRandom.current().nextLong(600);
            redisTemplate.opsForValue().set(key, user, expire, TimeUnit.SECONDS);
            localCache.put(key, user);
        }
        return user;
    }
}

方案三:熔断降级兜底

@HystrixCommand(
    fallbackMethod = "getUserFallback",
    commandProperties = {
        @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
        @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
        @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "30000")
    }
)
public User getUserById(Long id) {
    return multiLevelCacheService.getUserById(id);
}

public User getUserFallback(Long id) {
    // 降级:返回默认值或从备份存储读取
    return User.defaultUser(id);
}

缓存击穿解决方案:互斥锁与逻辑过期

缓存击穿针对热点Key过期时的并发穿透问题。两种主流方案:

方案一:互斥锁(Mutex Lock)

public User getUserWithMutex(Long id) {
    String key = "user:" + id;
    User user = (User) redisTemplate.opsForValue().get(key);
    if (user != null) return user;
    
    String lockKey = "lock:user:" + id;
    try {
        // 尝试获取分布式锁
        boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        if (locked) {
            try {
                // 双重检查
                user = (User) redisTemplate.opsForValue().get(key);
                if (user != null) return user;
                
                // 查数据库并重建缓存
                user = userMapper.selectById(id);
                if (user != null) {
                    redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);
                }
                return user;
            } finally {
                redisTemplate.delete(lockKey);
            }
        } else {
            // 获取锁失败,短暂等待后重试
            Thread.sleep(50);
            return getUserWithMutex(id);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return null;
    }
}

方案二:逻辑过期(Logical Expiry)

@Data
public class CacheData<T> {
    private T data;
    private LocalDateTime expireTime;
}

public User getUserWithLogicalExpiry(Long id) {
    String key = "user:" + id;
    CacheData cacheData = (CacheData) redisTemplate.opsForValue().get(key);
    
    if (cacheData == null) {
        // 缓存不存在,直接查库
        return loadAndCache(id, key);
    }
    
    if (cacheData.getExpireTime().isAfter(LocalDateTime.now())) {
        // 逻辑未过期,直接返回
        return (User) cacheData.getData();
    }
    
    // 逻辑已过期,异步刷新
    String lockKey = "lock:user:" + id;
    boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    
    if (locked) {
        // 获取锁,异步刷新缓存
        CompletableFuture.runAsync(() -> {
            try {
                loadAndCache(id, key);
            } finally {
                redisTemplate.delete(lockKey);
            }
        });
    }
    
    // 无论是否获取锁,都返回过期数据(保证可用性)
    return (User) cacheData.getData();
}

互斥锁方案保证数据强一致,但高并发下存在线程阻塞风险;逻辑过期方案保证高可用,但牺牲了数据一致性。选择哪种方案取决于业务场景:金融交易类选互斥锁,信息展示类选逻辑过期。生产环境中三种问题的解决方案需要组合使用,布隆过滤器防穿透、随机过期防雪崩、互斥锁防击穿,构建完整的Redis缓存高可用防护体系。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-chuan-tou-xue-beng-ji-chuan-jie-jue-fang-an/

(0)
小编小编
上一篇 25分钟前
下一篇 25分钟前

相关推荐