《Redis的缓存策略》

目录

引言

一、Redis缓存的基本原理

二、常见的Redis缓存策略

1.缓存预热

2.缓存雪崩

3.缓存穿透

4.缓存击穿


引言

Redis作为当今最流行的内存数据结构存储数之一,其高效的缓存策略是支撑高并发系统的核心。本文将深入探讨Redis的各种缓存策略及其适用场景。

一、Redis缓存的基本原理

Redis 是用 C 语言开发的一个开源的高性能键值对(key-value)数据库,因为其对数据的读写操作都是在内存中完成的,因此读写速度非常快,在缓存、消息队列、分布式锁等场景中应用广泛。

Redis中具有以下核心优势:

1.内存存储:数据主要驻留在RAM中。

2.单线程模型:避免锁竞争,原子性操作。

3.丰富的数据结构:字符串、哈希、列表、集合、有序集合等。

二、常见的Redis缓存策略

1.缓存预热

缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据。

一般当请求数量较高,主从之间数据吞吐量较大,数据同步操作频度较高,因为刚刚启动时,缓存中没有任何数据,这时我们就要进行缓存预热解决。

解决方案:日常例行统计数据访问记录,统计访问频度较高的热点数据,将统计结果中的数据分类,根据级别,redis优先加载级别较高的热点数据。然后使用脚本程序固定触发数据预热过程,如果条件允许,使用了CDN(内容分发网络),效果会更好。

基于Spring Boot的缓存预热实现:

java 复制代码
@Component
public class CacheWarmUp implements CommandLineRunner {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private HotKeyService hotKeyService; // 热点数据服务
    
    @Autowired
    private ProductService productService; // 业务服务
    
    @Value("${cache.warmup.enabled:true}")
    private boolean enabled;
    
    @Value("${cache.warmup.topN:100}")
    private int topN;
    
    @Override
    public void run(String... args) throws Exception {
        if (!enabled) {
            return;
        }
        
        // 1. 从统计系统获取热点Key列表
        List<String> hotKeys = hotKeyService.getTopNHotKeys(topN);
        
        // 2. 并行加载热点数据
        ExecutorService executor = Executors.newFixedThreadPool(10);
        List<CompletableFuture<Void>> futures = new ArrayList<>();
        
        for (String key : hotKeys) {
            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
                // 根据业务规则从数据库加载数据
                Object data = loadDataFromDB(key);
                
                // 设置缓存,并添加随机TTL避免雪崩
                int ttl = 3600 + new Random().nextInt(600); // 1小时±10分钟
                redisTemplate.opsForValue().set(key, data, ttl, TimeUnit.SECONDS);
            }, executor);
            
            futures.add(future);
        }
        
        // 等待所有任务完成
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        executor.shutdown();
        
        // 3. 加载系统关键数据
        warmUpCriticalData();
    }
    
    private Object loadDataFromDB(String key) {
        // 根据Key类型调用不同服务
        if (key.startsWith("product:")) {
            String productId = key.substring(8);
            return productService.getProductById(productId);
        }
        // 其他业务逻辑...
        return null;
    }
    
    private void warmUpCriticalData() {
        // 加载系统配置等关键数据
        redisTemplate.opsForValue().set("sys:config", loadSystemConfig(), 24, TimeUnit.HOURS);
    }
}

2.缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:给不同的Key的TTL添加随机值,利用Redis集群提高服务的可用性,给缓存业务添加降级限流策略,给业务添加多级缓存。

多级缓存:

java 复制代码
public class MultiLevelCache {
    
    // 本地缓存 (使用Caffeine)
    private final Cache<String, Object> localCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .build();
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private CacheAvalancheProtection cacheProtection;
    
    /**
     * 多级缓存获取
     */
    public <T> T get(String key, Class<T> type, Supplier<T> loader, 
                    long redisTtl, TimeUnit unit) {
        // 1. 检查本地缓存
        T value = (T) localCache.getIfPresent(key);
        if (value != null) {
            return value;
        }
        
        // 2. 检查Redis缓存(带雪崩保护)
        value = cacheProtection.safeGet(key, type, () -> {
            // 3. 二级缓存未命中,从数据源加载
            T data = loader.get();
            
            // 同时更新本地缓存
            if (data != null) {
                localCache.put(key, data);
            }
            
            return data;
        }, redisTtl, unit);
        
        return value;
    }
    
    /**
     * 更新多级缓存
     */
    public <T> void put(String key, T value, long redisTtl, TimeUnit unit) {
        if (value == null) {
            return;
        }
        
        // 更新本地缓存
        localCache.put(key, value);
        
        // 更新Redis缓存(带随机TTL)
        long ttl = unit.toSeconds(redisTtl) + new Random().nextInt(300);
        redisTemplate.opsForValue()
            .set(key, value, ttl, TimeUnit.SECONDS);
    }
}

3.缓存穿透

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

java 复制代码
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

public class CacheBreakdownProtection {
    
    private final RedisTemplate<String, Object> redisTemplate;
    
    // 使用Lua脚本保证原子性
    private static final RedisScript<Boolean> LOCK_SCRIPT = new DefaultRedisScript<>(
        "return redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])",
        Boolean.class
    );
    
    private static final String UNLOCK_SCRIPT =
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "   return redis.call('del', KEYS[1]) " +
        "else " +
        "   return 0 " +
        "end";
    
    public CacheBreakdownProtection(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    /**
     * 安全获取热点数据
     * @param key 缓存key
     * @param type 返回值类型
     * @param loader 数据加载函数(数据库查询等)
     * @param lockTimeout 锁超时时间(毫秒)
     * @param cacheTimeout 缓存超时时间(秒)
     */
    public <T> T getWithLock(String key, Class<T> type, 
                           Supplier<T> loader, 
                           long lockTimeout, 
                           long cacheTimeout) {
        // 1. 尝试从缓存获取
        T value = getFromCache(key, type);
        if (value != null) {
            return value;
        }
        
        // 2. 尝试获取分布式锁
        String lockKey = "lock:" + key;
        String lockId = UUID.randomUUID().toString();
        
        try {
            // 使用Lua脚本保证原子性
            Boolean locked = redisTemplate.execute(
                LOCK_SCRIPT,
                Collections.singletonList(lockKey),
                lockId,
                String.valueOf(lockTimeout)
            );
            
            if (Boolean.TRUE.equals(locked)) {
                try {
                    // 3. 双重检查,防止其他线程已经更新缓存
                    value = getFromCache(key, type);
                    if (value != null) {
                        return value;
                    }
                    
                    // 4. 从数据源加载数据
                    value = loader.get();
                    
                    // 5. 写入缓存(设置随机TTL防止雪崩)
                    if (value != null) {
                        long ttl = cacheTimeout + (long)(Math.random() * 300); // 添加随机值
                        redisTemplate.opsForValue().set(
                            key, 
                            value, 
                            ttl, 
                            TimeUnit.SECONDS
                        );
                    }
                    
                    return value;
                } finally {
                    // 6. 释放锁
                    redisTemplate.execute(
                        new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class),
                        Collections.singletonList(lockKey),
                        lockId
                    );
                }
            } else {
                // 7. 未获取到锁,短暂等待后重试
                Thread.sleep(50 + (long)(Math.random() * 50));
                return getWithLock(key, type, loader, lockTimeout, cacheTimeout);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Interrupted while waiting for cache lock", e);
        }
    }
    
    @SuppressWarnings("unchecked")
    private <T> T getFromCache(String key, Class<T> type) {
        Object value = redisTemplate.opsForValue().get(key);
        if (value == null) {
            return null;
        }
        return (T) value;
    }
}

4.缓存击穿

缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。这里我们展示布隆过滤来解决。

java 复制代码
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;

public class BloomFilterCache {
    
    private final RedisTemplate<String, Object> redisTemplate;
    private final String bloomFilterName;
    
    // Redis Bloom Filter命令脚本
    private static final RedisScript<Boolean> BF_ADD_SCRIPT = new DefaultRedisScript<>(
        "return redis.call('BF.ADD', KEYS[1], ARGV[1])",
        Boolean.class
    );
    
    private static final RedisScript<Boolean> BF_EXISTS_SCRIPT = new DefaultRedisScript<>(
        "return redis.call('BF.EXISTS', KEYS[1], ARGV[1])",
        Boolean.class
    );
    
    public BloomFilterCache(RedisTemplate<String, Object> redisTemplate, 
                          String bloomFilterName) {
        this.redisTemplate = redisTemplate;
        this.bloomFilterName = bloomFilterName;
    }
    
    /**
     * 安全获取数据,防止缓存穿透
     * @param key 缓存key
     * @param type 返回值类型
     * @param loader 数据加载函数
     * @param cacheSeconds 缓存时间(秒)
     */
    public <T> T get(String key, Class<T> type, 
                    Supplier<T> loader, 
                    long cacheSeconds) {
        // 1. 检查布隆过滤器
        Boolean mayExist = redisTemplate.execute(
            BF_EXISTS_SCRIPT,
            Collections.singletonList(bloomFilterName),
            key
        );
        
        // 2. 如果布隆过滤器判断不存在,直接返回null
        if (Boolean.FALSE.equals(mayExist)) {
            return null;
        }
        
        // 3. 检查缓存
        T value = getFromCache(key, type);
        if (value != null) {
            return value;
        }
        
        // 4. 从数据源加载
        value = loader.get();
        
        if (value != null) {
            // 5. 写入缓存
            redisTemplate.opsForValue().set(
                key, 
                value, 
                cacheSeconds, 
                TimeUnit.SECONDS
            );
        } else {
            // 6. 数据不存在,缓存空值防止穿透
            redisTemplate.opsForValue().set(
                key, 
                new NullValue(), 
                Math.min(cacheSeconds, 300), // 空值缓存时间较短
                TimeUnit.SECONDS
            );
            
            // 注意:这里不添加到布隆过滤器,因为实际不存在
        }
        
        return value;
    }
    
    /**
     * 添加元素到布隆过滤器
     */
    public void addToBloomFilter(String key) {
        redisTemplate.execute(
            BF_ADD_SCRIPT,
            Collections.singletonList(bloomFilterName),
            key
        );
    }
    
    /**
     * 批量添加元素到布隆过滤器
     */
    public void addAllToBloomFilter(List<String> keys) {
        for (String key : keys) {
            addToBloomFilter(key);
        }
    }
    
    @SuppressWarnings("unchecked")
    private <T> T getFromCache(String key, Class<T> type) {
        Object value = redisTemplate.opsForValue().get(key);
        if (value instanceof NullValue) {
            return null;
        }
        return (T) value;
    }
    
    // 空值标记类
    private static class NullValue implements Serializable {
        private static final long serialVersionUID = 1L;
    }
}
java 复制代码
@Service
public class ProductService {
    
    @Autowired
    private BloomFilterCache bloomFilterCache;
    
    @Autowired
    private ProductRepository productRepository;
    
    // 初始化布隆过滤器(系统启动时调用)
    @PostConstruct
    public void initBloomFilter() {
        List<String> allProductIds = productRepository.findAllIds();
        bloomFilterCache.addAllToBloomFilter(allProductIds);
    }
    
    public Product getProductById(String id) {
        String cacheKey = "product:" + id;
        
        Product product = bloomFilterCache.get(
            cacheKey,
            Product.class,
            () -> productRepository.findById(id).orElse(null),
            3600 // 缓存1小时
        );
        
        // 如果是新创建的产品,需要添加到布隆过滤器
        if (product != null && !product.isFromCache()) {
            bloomFilterCache.addToBloomFilter(cacheKey);
        }
        
        return product;
    }
}
相关推荐
没有bug.的程序员2 小时前
Redis Sentinel:高可用架构的守护者
java·redis·架构·sentinel
李游Leo2 小时前
npm / yarn / pnpm 包管理器对比与最佳实践(含国内镜像源配置与缓存优化)
前端·缓存·npm
凯子坚持 c3 小时前
Redis 核心数据结构:String 类型深度解析与 C++ 实战
数据结构·c++·redis
tjjingpan3 小时前
mosdns缓存dns服务器配置记录
运维·服务器·缓存
半夏知半秋4 小时前
基于跳跃表的zset实现解析(lua版)
服务器·开发语言·redis·学习·lua
小蒜学长4 小时前
基于uni-app的蛋糕订购小程序的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·小程序·uni-app
安然~~~4 小时前
mysql多表联查
android·数据库·mysql
乐世东方客4 小时前
使用my2sql进行mysql的binlog恢复数据
数据库·mysql
肃清14 小时前
《深入解析数据库事务的ACID特性》
数据库·mysql