目录
引言
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;
}
}