【Redis实战】深入理解Redis缓存策略:从原理到Spring Boot实践
📖 前言
在高并发系统中,缓存是提升性能的关键手段。Redis作为最流行的内存数据库,被广泛应用于各种缓存场景。然而,缓存的使用并非简单地set/get,不当的缓存策略可能导致数据不一致、缓存穿透、甚至系统崩溃。
本文将从实际开发角度,系统讲解Redis缓存策略的核心知识。
🎯 适合人群:有Redis基础,想深入理解缓存策略的Java/后端开发者
一、缓存读写策略全景图

| 策略 | 一致性 | 复杂度 | 适用场景 | |------|--------|--------|----------| | Cache Aside | 中 | 低 | 读多写少 | | Read/Write Through | 高 | 中 | 一致性要求高 | | Write Behind | 低 | 高 | 写多读少、可容忍延迟 |
二、Cache Aside Pattern(旁路缓存)
2.1 核心原理
Cache Aside 是最常用的缓存策略,核心逻辑:
读操作:先查缓存 → 命中则返回 → 未命中则查DB → 写入缓存 → 返回
写操作:更新DB → 删除缓存(而非更新缓存)
2.2 为什么是"删除缓存"而不是"更新缓存"?
java
// ❌ 错误做法:更新缓存
public void updateUser(User user) {
// 1. 更新数据库
userDao.update(user);
// 2. 更新缓存(存在并发问题!)
redisTemplate.opsForValue().set("user:" + user.getId(), user);
}
// ✅ 正确做法:删除缓存
public void updateUser(User user) {
// 1. 更新数据库
userDao.update(user);
// 2. 删除缓存
redisTemplate.delete("user:" + user.getId());
}
原因分析:
- 更新缓存在并发场景下可能导致脏数据(线程A先更新DB,线程B后更新DB,但线程A后写缓存)
- 删除缓存可以让下次读取时被动加载最新数据
2.3 Spring Boot完整实现
java
@Service
@Slf4j
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
private static final String CACHE_PREFIX = "user:";
private static final long CACHE_TTL = 3600; // 1小时过期
/**
* 查询用户 - Cache Aside读模式
*/
public User getUserById(Long userId) {
String cacheKey = CACHE_PREFIX + userId;
// 1. 先查缓存
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
log.info("缓存命中,userId={}", userId);
return user;
}
// 2. 缓存未命中,查数据库
log.info("缓存未命中,查询数据库,userId={}", userId);
user = userMapper.selectById(userId);
// 3. 写入缓存(设置过期时间)
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user, CACHE_TTL, TimeUnit.SECONDS);
}
return user;
}
/**
* 更新用户 - Cache Aside写模式
*/
@Transactional
public void updateUser(User user) {
String cacheKey = CACHE_PREFIX + user.getId();
// 1. 先更新数据库
userMapper.updateById(user);
// 2. 再删除缓存
redisTemplate.delete(cacheKey);
log.info("用户更新成功,已删除缓存,userId={}", user.getId());
}
}
2.4 Cache Aside的缺陷
问题 :首次请求必然穿透到数据库(缓存冷启动)
解决方案:
- 数据预热:系统启动时加载热点数据
- 布隆过滤器:快速判断key是否存在
三、Read/Write Through(读写穿透)
3.1 核心原理
应用程序只与缓存交互,缓存负责与数据库同步。
读:App → Cache → DB(Cache自动加载)
写:App → Cache → DB(Cache自动同步)
3.2 代码实现
java
/**
* Read/Write Through模式实现
* 使用CacheManager封装缓存和数据库操作
*/
@Component
public class UserCacheManager {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
private static final String CACHE_PREFIX = "user:";
/**
* Read Through:自动加载
*/
public User getOrLoad(Long userId) {
String cacheKey = CACHE_PREFIX + userId;
// 尝试从缓存获取
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user;
}
// 缓存未命中,加载数据(加锁防止缓存击穿)
synchronized (this) {
// 双重检查
user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user;
}
// 从数据库加载
user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
}
return user;
}
}
/**
* Write Through:同步更新
*/
@Transactional
public void saveOrUpdate(User user) {
String cacheKey = CACHE_PREFIX + user.getId();
// 1. 更新缓存
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
// 2. 更新数据库
if (user.getId() == null) {
userMapper.insert(user);
} else {
userMapper.updateById(user);
}
}
}
3.3 优缺点
| 优点 | 缺点 | |------|------| | 代码简洁,调用方无需关心缓存 | 缓存层复杂度增加 | | 一致性好 | 所有操作都经过缓存,延迟增加 | | 适合对一致性要求高的场景 | 实现成本高 |
四、Write Behind Pattern(异步写入)
4.1 核心原理
写操作只更新缓存,异步批量写入数据库。
写:App → Cache(立即返回)→ 异步批量 → DB
读:App → Cache(直接返回)
4.2 代码实现
java
/**
* Write Behind模式实现
* 使用消息队列异步持久化
*/
@Service
@Slf4j
public class WriteBehindService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 写入:只更新缓存 + 发送MQ
*/
public void updateUserAsync(User user) {
String cacheKey = "user:" + user.getId();
// 1. 立即更新缓存
redisTemplate.opsForValue().set(cacheKey, user, 2, TimeUnit.HOURS);
// 2. 发送到MQ,异步持久化
rabbitTemplate.convertAndSend("user.update.exchange", "user.update", user);
log.info("用户数据已更新缓存,等待异步持久化,userId={}", user.getId());
}
}
/**
* MQ消费者:批量持久化到数据库
*/
@Component
@Slf4j
public class UserUpdateConsumer {
@Autowired
private UserMapper userMapper;
private List<User> buffer = new ArrayList<>();
private static final int BATCH_SIZE = 100;
@RabbitListener(queues = "user.update.queue")
public void handleUserUpdate(User user) {
buffer.add(user);
// 达到批量大小,执行持久化
if (buffer.size() >= BATCH_SIZE) {
flushToDatabase();
}
}
@Scheduled(fixedRate = 5000) // 每5秒强制刷新
public void scheduledFlush() {
if (!buffer.isEmpty()) {
flushToDatabase();
}
}
private synchronized void flushToDatabase() {
if (buffer.isEmpty()) return;
log.info("批量持久化{}条用户数据", buffer.size());
userMapper.batchUpdate(buffer);
buffer.clear();
}
}
4.3 适用场景
- 计数器:点赞数、浏览量(允许短暂不一致)
- 日志系统:先写缓存,批量落盘
- 消息已读状态:实时响应,异步持久化
五、缓存三大问题及解决方案
5.1 缓存穿透(Cache Penetration)
问题描述 :查询一个根本不存在的数据,缓存和DB都查不到,请求每次都穿透到DB。

恶意攻击示例:
java
// 攻击者构造大量不存在的ID
GET /api/user/-1
GET /api/user/999999999
GET /api/user/abc
解决方案一:缓存空值
java
public User getUserById(Long userId) {
String cacheKey = "user:" + userId;
// 1. 查缓存
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
// 判断是否为空值标记
if (cached.equals("NULL")) {
return null;
}
return (User) cached;
}
// 2. 查数据库
User user = userMapper.selectById(userId);
// 3. 写缓存(包括空值)
if (user == null) {
// 缓存空值,设置较短过期时间
redisTemplate.opsForValue().set(cacheKey, "NULL", 5, TimeUnit.MINUTES);
} else {
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
}
return user;
}
解决方案二:布隆过滤器
java
@Component
public class BloomFilterService {
private BloomFilter<Long> userBloomFilter;
@Autowired
private UserMapper userMapper;
@PostConstruct
public void init() {
// 初始化布隆过滤器
userBloomFilter = BloomFilter.create(
Funnels.longFunnel(),
1000000, // 预计元素数量
0.01 // 误判率1%
);
// 加载所有用户ID到布隆过滤器
List<Long> userIds = userMapper.selectAllUserIds();
userIds.forEach(id -> userBloomFilter.put(id));
log.info("布隆过滤器初始化完成,加载{}个用户ID", userIds.size());
}
public boolean mightExist(Long userId) {
return userBloomFilter.mightContain(userId);
}
}
// 使用布隆过滤器
public User getUserWithBloomFilter(Long userId) {
// 1. 布隆过滤器判断
if (!bloomFilterService.mightExist(userId)) {
log.info("布隆过滤器拦截,userId不存在:{}", userId);
return null;
}
// 2. 正常查询流程
return getUserById(userId);
}
5.2 缓存击穿(Cache Breakdown)
问题描述 :某个热点key过期瞬间,大量请求同时穿透到数据库。

典型场景:
- 秒杀商品缓存过期
- 热门文章缓存失效
解决方案一:互斥锁(Mutex Lock)
java
public User getUserWithMutex(Long userId) {
String cacheKey = "user:" + userId;
String lockKey = "lock:user:" + userId;
// 1. 查缓存
User user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user;
}
// 2. 获取分布式锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 双重检查
user = (User) redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user;
}
// 3. 查询数据库
user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user, 1, TimeUnit.HOURS);
}
return user;
} finally {
// 4. 释放锁
redisTemplate.delete(lockKey);
}
} else {
// 未获取到锁,等待后重试
try {
Thread.sleep(50);
return getUserWithMutex(userId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
}
解决方案二:逻辑过期(不设置TTL)
java
@Data
public class CacheData<T> {
private T data;
private long expireTime; // 逻辑过期时间
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}
public User getUserWithLogicalExpire(Long userId) {
String cacheKey = "user:" + userId;
// 1. 查缓存
CacheData<User> cacheData = (CacheData<User>) redisTemplate.opsForValue().get(cacheKey);
if (cacheData == null) {
return null; // 缓存不存在,说明数据库也没有
}
// 2. 判断是否逻辑过期
if (!cacheData.isExpired()) {
return cacheData.getData(); // 未过期,直接返回
}
// 3. 已过期,尝试获取锁更新
String lockKey = "lock:user:" + userId;
if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(lockKey, "1"))) {
// 获取到锁,异步更新
CompletableFuture.runAsync(() -> {
try {
User newUser = userMapper.selectById(userId);
CacheData<User> newCache = new CacheData<>();
newCache.setData(newUser);
newCache.setExpireTime(System.currentTimeMillis() + 3600000);
redisTemplate.opsForValue().set(cacheKey, newCache);
} finally {
redisTemplate.delete(lockKey);
}
});
}
// 4. 返回旧数据(保证可用性)
return cacheData.getData();
}
5.3 缓存雪崩(Cache Avalanche)
问题描述 :大量key同时过期 或Redis宕机,导致请求全部打到数据库。

解决方案:
java
/**
* 缓存雪崩防护策略
*/
@Component
public class AvalancheProtection {
private final Random random = new Random();
/**
* 策略1:过期时间加随机值,避免同时失效
*/
public void setWithRandomTTL(String key, Object value, long baseTTL) {
// 基础TTL + 随机0-300秒
long randomTTL = baseTTL + random.nextInt(300);
redisTemplate.opsForValue().set(key, value, randomTTL, TimeUnit.SECONDS);
}
/**
* 策略2:多级缓存(L1: 本地缓存, L2: Redis缓存)
*/
@Autowired
private CacheManager caffeineCacheManager;
public User getUserWithMultiLevelCache(Long userId) {
String cacheKey = "user:" + userId;
// L1: 本地缓存
Cache localCache = caffeineCacheManager.getCache("users");
User user = localCache.get(userId, () -> {
// L2: Redis缓存
User cached = (User) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
// 数据库
return userMapper.selectById(userId);
});
return user;
}
/**
* 策略3:熔断降级(使用Sentinel或Resilience4j)
*/
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public User getUserWithCircuitBreaker(Long userId) {
// 正常查询逻辑
return getUserById(userId);
}
// 降级方法:返回默认值或从其他数据源获取
public User getUserFallback(Long userId, Throwable t) {
log.warn("触发熔断降级,userId={}", userId, t);
return new User(userId, "默认用户", "暂无数据");
}
}
六、缓存策略选型指南
| 场景 | 推荐策略 | 原因 | |------|----------|------| | 用户信息查询 | Cache Aside | 读多写少,实现简单 | | 订单系统 | Read/Write Through | 强一致性要求 | | 浏览量/点赞数 | Write Behind | 允许短暂延迟 | | 秒杀库存 | Cache Aside + 分布式锁 | 高并发+一致性 | | 搜索热词 | Write Behind | 容忍延迟,高写入 |
七、最佳实践总结
7.1 缓存设计CheckList
markdown
## Redis缓存设计清单
### 基础设置
- [ ] 所有key必须设置过期时间
- [ ] 过期时间加随机值(防止雪崩)
- [ ] 使用有意义的key命名规范(业务:类型:ID)
### 异常处理
- [ ] 缓存空值或使用布隆过滤器(防穿透)
- [ ] 互斥锁或逻辑过期(防击穿)
- [ ] 多级缓存或熔断降级(防雪崩)
### 数据一致性
- [ ] 写操作采用"先更新DB,再删缓存"策略
- [ ] 重要数据使用延迟双删(写DB→删缓存→延迟→再删缓存)
- [ ] 考虑使用Canal监听binlog同步缓存
### 监控告警
- [ ] 监控缓存命中率
- [ ] 监控Redis内存使用
- [ ] 设置慢查询告警
7.2 延迟双删代码实现
java
/**
* 延迟双删:解决并发场景下的缓存一致性问题
*/
@Transactional
public void updateUserWithDelayDoubleDelete(User user) {
String cacheKey = "user:" + user.getId();
// 1. 先删除缓存
redisTemplate.delete(cacheKey);
// 2. 更新数据库
userMapper.updateById(user);
// 3. 延迟后再删一次(异步执行,不阻塞主线程)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500); // 延迟500ms
redisTemplate.delete(cacheKey);
log.info("延迟双删完成,userId={}", user.getId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
八、性能测试数据参考
| 策略 | QPS | 延迟(P99) | 数据一致性 | 复杂度 | |------|-----|-----------|------------|--------| | 无缓存 | 1,000 | 50ms | 强一致 | 低 | | Cache Aside | 50,000 | 2ms | 最终一致 | 低 | | Read/Write Through | 30,000 | 5ms | 强一致 | 中 | | Write Behind | 80,000 | 1ms | 弱一致 | 高 | | 多级缓存 | 100,000+ | 0.5ms | 最终一致 | 中 |
总结
| 策略 | 核心思想 | 适用场景 | |------|----------|----------| | Cache Aside | 应用控制缓存,先更新DB再删缓存 | 通用场景,读多写少 | | Read/Write Through | 缓存层封装DB操作 | 一致性要求高 | | Write Behind | 异步写入,提升写性能 | 写多读少,容忍延迟 |
缓存三大问题解决方案:
- 缓存穿透:缓存空值 + 布隆过滤器
- 缓存击穿:互斥锁 + 逻辑过期
- 缓存雪崩:随机过期 + 多级缓存 + 熔断降级
参考资料
📝 作者简介:Java后端开发工程师,专注于高并发、分布式系统设计。欢迎关注,一起交流技术!
🔖 版权声明:本文为原创文章,转载请注明出处。