缓存

Redis 缓存设计与应用模式

高并发场景下的缓存最佳实践

中级 30 分钟 2026-05-05

涉及技术

Redis 7 Spring Data Redis Redisson Lettuce

文档简介

详解缓存穿透、缓存击穿、缓存雪崩的解决方案,介绍布隆过滤器、分布式锁、缓存一致性等高级模式,结合 Spring Data Redis 提供完整代码示例。

正文内容

为什么使用缓存

在高并发系统中,数据库往往是性能瓶颈。Redis 作为内存数据库,读写性能是 MySQL 的 10-50 倍。通过合理的缓存策略,可以显著降低数据库压力,提升系统响应速度。

三种经典缓存问题

生产环境中,缓存使用不当会引发三种典型问题:

1. 缓存穿透 — 查询一个不存在的数据,缓存和数据库都无法命中,导致每次请求都打到数据库。

// 解决方案:缓存空值 + 布隆过滤器
@Component
public class CachePenetrationSolution {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RBloomFilter<String> bloomFilter;

    public User getUser(Long id) {
        String key = "user:" + id;
        String cached = redisTemplate.opsForValue().get(key);

        if (cached != null) {
            // 缓存命中(包括空值标记 "null")
            return "null".equals(cached) ? null : JSON.parseObject(cached, User.class);
        }

        // 布隆过滤器拦截
        if (!bloomFilter.contains(key)) {
            // 数据一定不存在,直接返回
            return null;
        }

        // 查询数据库
        User user = userMapper.selectById(id);
        if (user == null) {
            // 缓存空值,过期时间设置较短
            redisTemplate.opsForValue().set(key, "null", 60, TimeUnit.SECONDS);
        } else {
            redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
        }
        return user;
    }
}

2. 缓存击穿 — 热点数据在缓存失效的瞬间,大量并发请求同时打到数据库。

// 解决方案:互斥锁 + 逻辑过期
@Component
public class CacheBreakdownSolution {

    @Autowired
    private RedissonClient redissonClient;

    public User getHotUser(Long id) {
        String key = "user:" + id;
        String json = redisTemplate.opsForValue().get(key);

        if (json != null) {
            RedisData redisData = JSON.parseObject(json, RedisData.class);
            LocalDateTime expireTime = redisData.getExpireTime();

            if (expireTime.isAfter(LocalDateTime.now())) {
                // 逻辑未过期,直接返回
                return JSON.parseObject(redisData.getData(), User.class);
            }

            // 逻辑已过期,尝试获取锁重建缓存
            RLock lock = redissonClient.getLock("lock:user:" + id);
            if (lock.tryLock()) {
                try {
                    // 双重检查
                    json = redisTemplate.opsForValue().get(key);
                    redisData = JSON.parseObject(json, RedisData.class);
                    if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
                        return JSON.parseObject(redisData.getData(), User.class);
                    }
                    // 重建缓存
                    User user = userMapper.selectById(id);
                    cacheUserWithLogicalExpire(id, user);
                    return user;
                } finally {
                    lock.unlock();
                }
            }
        }

        // 未获取到锁,返回旧数据(保证可用性)
        return JSON.parseObject(JSON.parseObject(json, RedisData.class).getData(), User.class);
    }
}

3. 缓存雪崩 — 大量缓存同时失效或 Redis 宕机,导致数据库瞬时压力过大。

// 解决方案:过期时间加随机值 + 多级缓存
@Component
public class CacheAvalancheSolution {

    @Autowired
    private CacheManager caffeineCacheManager;

    public User getUserWithMultiLevel(Long id) {
        // L1: Caffeine 本地缓存(进程内,毫秒级)
        Cache localCache = caffeineCacheManager.getCache("userLocal");
        User user = localCache.get(id, User.class);
        if (user != null) return user;

        // L2: Redis 分布式缓存(微秒级)
        String key = "user:" + id;
        String json = redisTemplate.opsForValue().get(key);
        if (json != null) {
            user = JSON.parseObject(json, User.class);
            localCache.put(id, user);  // 回填本地缓存
            return user;
        }

        // L3: 数据库
        user = userMapper.selectById(id);
        if (user != null) {
            // 过期时间加随机值,避免同时失效
            long expire = 30 * 60 + ThreadLocalRandom.current().nextInt(300);
            redisTemplate.opsForValue().set(key, JSON.toJSONString(user), expire, TimeUnit.SECONDS);
            localCache.put(id, user);
        }
        return user;
    }
}

分布式锁

Redisson 提供了可重入、可续期的分布式锁,是 Redis 实现分布式锁的最佳实践:

@Component
public class DistributedLockDemo {

    @Autowired
    private RedissonClient redissonClient;

    public void deductStock(String productId, int count) {
        RLock lock = redissonClient.getLock("stock:" + productId);

        try {
            // 尝试获取锁,最多等待 10 秒,锁持有时长 30 秒(看门狗自动续期)
            boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (!acquired) {
                throw new BusinessException("获取库存锁失败,请稍后重试");
            }

            int stock = getStock(productId);
            if (stock < count) {
                throw new BusinessException("库存不足");
            }
            updateStock(productId, stock - count);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BusinessException("操作被中断");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

缓存一致性策略

缓存与数据库的数据一致性是分布式系统的经典难题。推荐采用 Cache-Aside 模式:

// 读:先查缓存,未命中再查数据库并回填
public User getById(Long id) {
    String key = "user:" + id;
    User user = cache.get(key);
    if (user == null) {
        user = userMapper.selectById(id);
        if (user != null) cache.set(key, user, 30, MINUTES);
    }
    return user;
}

// 写:先更新数据库,再删除缓存(非更新缓存)
@Transactional
public void update(User user) {
    userMapper.updateById(user);
    cache.delete("user:" + user.getId());
}

// 删除:先删数据库,再删缓存
@Transactional
public void delete(Long id) {
    userMapper.deleteById(id);
    cache.delete("user:" + id);
}