Redis 缓存设计与实战
2026/3/20大约 19 分钟
Redis 缓存设计与实战
一、缓存基础概念
1.1 为什么需要缓存
在高并发系统中,数据库往往是性能瓶颈。使用缓存可以:
- 降低数据库压力:减少对数据库的直接访问
- 提升响应速度:内存访问比磁盘快几个数量级
- 提高系统吞吐量:单机 Redis QPS 可达 10W+
- 改善用户体验:更快的页面加载速度
1.2 缓存的分类
| 类型 | 说明 | 示例 |
|---|---|---|
| 本地缓存 | 应用进程内的缓存 | HashMap、Guava Cache、Caffeine |
| 分布式缓存 | 独立的缓存服务 | Redis、Memcached |
| 多级缓存 | 本地 + 分布式组合 | L1 Caffeine + L2 Redis |
| CDN 缓存 | 边缘节点缓存静态资源 | 图片、CSS、JS 文件 |
二、缓存读写模式
2.1 Cache Aside(旁路缓存)
这是最常用的缓存模式,应用程序直接管理缓存和数据库。
代码实现:
@Service
public class UserService {
@Autowired
private RedisTemplate<String, User> redisTemplate;
@Autowired
private UserMapper userMapper;
private static final String USER_CACHE_KEY = "user:";
private static final long CACHE_EXPIRE = 3600; // 1小时
// 读取用户
public User getUser(Long userId) {
String key = USER_CACHE_KEY + userId;
// 1. 查询缓存
User user = redisTemplate.opsForValue().get(key);
if (user != null) {
return user; // 缓存命中
}
// 2. 缓存未命中,查询数据库
user = userMapper.selectById(userId);
if (user == null) {
// 防止缓存穿透,缓存空值
redisTemplate.opsForValue().set(key, new User(), 60, TimeUnit.SECONDS);
return null;
}
// 3. 写入缓存
redisTemplate.opsForValue().set(key, user, CACHE_EXPIRE, TimeUnit.SECONDS);
return user;
}
// 更新用户
@Transactional
public void updateUser(User user) {
// 1. 更新数据库
userMapper.updateById(user);
// 2. 删除缓存(而不是更新)
String key = USER_CACHE_KEY + user.getId();
redisTemplate.delete(key);
}
}
为什么删除缓存而不是更新缓存?
2.2 Read/Write Through
缓存作为主要的数据存储,应用只与缓存交互:
2.3 Write Behind(Write Back)
写入缓存后异步更新数据库:
2.4 模式对比
| 模式 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| Cache Aside | 中等 | 高 | 低 | 通用场景 |
| Read/Write Through | 较高 | 中 | 中 | 需要缓存抽象 |
| Write Behind | 低 | 极高 | 高 | 写密集、允许丢失 |
三、缓存问题与解决方案
3.1 缓存穿透
缓存穿透是指查询一个不存在的数据,缓存和数据库都没有,导致每次请求都打到数据库。
解决方案:
方案 1:缓存空值
public User getUser(Long userId) {
String key = USER_CACHE_KEY + userId;
// 使用 String 存储,便于区分空值
String value = redisTemplate.opsForValue().get(key);
// 如果是空值标记,直接返回 null
if ("NULL".equals(value)) {
return null;
}
if (value != null) {
return JSON.parseObject(value, User.class);
}
User user = userMapper.selectById(userId);
if (user == null) {
// 缓存空值,设置较短的过期时间
redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
return null;
}
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 1, TimeUnit.HOURS);
return user;
}
方案 2:布隆过滤器
// 使用 Redisson 的布隆过滤器
@Service
public class UserService {
@Autowired
private RedissonClient redissonClient;
private RBloomFilter<Long> userBloomFilter;
@PostConstruct
public void init() {
// 创建布隆过滤器
userBloomFilter = redissonClient.getBloomFilter("userBloomFilter");
// 预期元素数量 100万,误判率 1%
userBloomFilter.tryInit(1000000L, 0.01);
// 加载所有用户 ID 到布隆过滤器
List<Long> allUserIds = userMapper.selectAllIds();
allUserIds.forEach(userBloomFilter::add);
}
public User getUser(Long userId) {
// 先检查布隆过滤器
if (!userBloomFilter.contains(userId)) {
return null; // 一定不存在
}
// 可能存在,查询缓存和数据库
// ...
}
// 新增用户时,加入布隆过滤器
public void addUser(User user) {
userMapper.insert(user);
userBloomFilter.add(user.getId());
}
}
3.2 缓存击穿
缓存击穿是指热点 key 过期的瞬间,大量请求同时打到数据库。
解决方案:
方案 1:互斥锁
public Product getProduct(Long productId) {
String key = "product:" + productId;
String lockKey = "lock:product:" + productId;
// 1. 查询缓存
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
return JSON.parseObject(value, Product.class);
}
// 2. 缓存未命中,尝试获取锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 获取锁成功,查询数据库
// 双重检查:可能其他线程已经加载了缓存
value = redisTemplate.opsForValue().get(key);
if (value != null) {
return JSON.parseObject(value, Product.class);
}
Product product = productMapper.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(key,
JSON.toJSONString(product), 1, TimeUnit.HOURS);
}
return product;
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
} else {
// 获取锁失败,等待重试
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getProduct(productId); // 递归重试
}
}
方案 2:逻辑过期
@Data
public class CacheData {
private Object data;
private Long expireTime; // 逻辑过期时间
}
public Product getProductWithLogicalExpire(Long productId) {
String key = "product:" + productId;
String lockKey = "lock:product:" + productId;
// 1. 查询缓存(缓存永不过期)
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
// 缓存不存在(首次加载或数据不存在)
return loadFromDb(productId);
}
CacheData cacheData = JSON.parseObject(value, CacheData.class);
// 2. 判断是否逻辑过期
if (System.currentTimeMillis() < cacheData.getExpireTime()) {
// 未过期,直接返回
return (Product) cacheData.getData();
}
// 3. 已过期,尝试异步更新
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 获取锁成功,异步更新缓存
executor.submit(() -> {
try {
Product product = productMapper.selectById(productId);
CacheData newData = new CacheData();
newData.setData(product);
newData.setExpireTime(System.currentTimeMillis() + 3600000); // 1小时
redisTemplate.opsForValue().set(key, JSON.toJSONString(newData));
} finally {
redisTemplate.delete(lockKey);
}
});
}
// 返回旧数据
return (Product) cacheData.getData();
}
方案 3:热点数据永不过期
// 热点商品不设置过期时间
// 通过消息队列或定时任务主动更新
redisTemplate.opsForValue().set("product:hotItem:" + productId,
JSON.toJSONString(product)); // 不设置过期时间
// 监听商品更新事件,主动刷新缓存
@KafkaListener(topics = "product-update")
public void onProductUpdate(ProductUpdateEvent event) {
String key = "product:hotItem:" + event.getProductId();
Product product = productMapper.selectById(event.getProductId());
redisTemplate.opsForValue().set(key, JSON.toJSONString(product));
}
3.3 缓存雪崩
缓存雪崩是指大量缓存同时过期,或缓存服务宕机,导致大量请求打到数据库。
解决方案:
方案 1:过期时间随机化
// 基础过期时间 + 随机偏移
public void cacheWithRandomExpire(String key, Object value, long baseExpireSeconds) {
// 随机偏移 0-10 分钟
long randomOffset = ThreadLocalRandom.current().nextLong(0, 600);
long expireTime = baseExpireSeconds + randomOffset;
redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);
}
// 使用示例
cacheWithRandomExpire("product:" + productId, product, 3600);
方案 2:多级缓存
@Service
public class MultiLevelCacheService {
// 本地缓存(Caffeine)
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Object get(String key) {
// 1. 查询本地缓存
Object value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// 2. 查询 Redis
value = redisTemplate.opsForValue().get(key);
if (value != null) {
// 写入本地缓存
localCache.put(key, value);
return value;
}
// 3. 查询数据库
value = loadFromDb(key);
if (value != null) {
// 写入两级缓存
localCache.put(key, value);
redisTemplate.opsForValue().set(key, value, 1, TimeUnit.HOURS);
}
return value;
}
}
方案 3:熔断降级
// 使用 Sentinel 或 Hystrix 进行熔断
@SentinelResource(value = "getProduct",
blockHandler = "getProductBlockHandler",
fallback = "getProductFallback")
public Product getProduct(Long productId) {
// 正常业务逻辑
}
// 限流/熔断后的处理
public Product getProductBlockHandler(Long productId, BlockException e) {
// 返回默认值或友好提示
return Product.getDefault();
}
// 异常降级
public Product getProductFallback(Long productId, Throwable e) {
log.error("Get product failed: {}", productId, e);
return Product.getDefault();
}
方案 4:Redis 高可用
四、数据一致性
4.1 最终一致性方案
方案 1:延迟双删
public void updateProduct(Product product) {
String key = "product:" + product.getId();
// 1. 删除缓存
redisTemplate.delete(key);
// 2. 更新数据库
productMapper.updateById(product);
// 3. 延迟后再次删除缓存
// 延迟时间 > 主从复制延迟 + 业务处理时间
executor.schedule(() -> {
redisTemplate.delete(key);
}, 500, TimeUnit.MILLISECONDS);
}
方案 2:订阅 Binlog
// Canal 客户端示例
@Component
public class CanalClient {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void processEntry(Entry entry) {
if (entry.getEntryType() == EntryType.ROWDATA) {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
for (RowData rowData : rowChange.getRowDatasList()) {
if (rowChange.getEventType() == EventType.UPDATE) {
handleUpdate(entry.getHeader().getTableName(), rowData);
} else if (rowChange.getEventType() == EventType.DELETE) {
handleDelete(entry.getHeader().getTableName(), rowData);
}
}
}
}
private void handleUpdate(String tableName, RowData rowData) {
// 获取主键
String id = getColumnValue(rowData.getAfterColumnsList(), "id");
// 删除或更新缓存
String key = tableName + ":" + id;
redisTemplate.delete(key);
}
}
4.2 强一致性方案
对于强一致性要求的场景,可以使用分布式事务或读写锁:
// 使用 Redisson 读写锁
public Product getProductWithLock(Long productId) {
String key = "product:" + productId;
RReadWriteLock lock = redissonClient.getReadWriteLock("lock:" + key);
lock.readLock().lock();
try {
// 读取缓存或数据库
return getProduct(productId);
} finally {
lock.readLock().unlock();
}
}
public void updateProductWithLock(Product product) {
String key = "product:" + product.getId();
RReadWriteLock lock = redissonClient.getReadWriteLock("lock:" + key);
lock.writeLock().lock();
try {
// 更新数据库
productMapper.updateById(product);
// 删除或更新缓存
redisTemplate.delete(key);
} finally {
lock.writeLock().unlock();
}
}
五、缓存设计最佳实践
5.1 Key 设计规范
# 格式:业务:类型:ID[:属性]
# 示例:
# 用户信息
user:info:1001
user:session:1001
user:token:abc123
# 商品信息
product:detail:2001
product:stock:2001
product:hot:list # 热门商品列表
# 订单信息
order:detail:3001
order:user:1001:list # 用户订单列表
# 缓存锁
lock:product:update:2001
lock:order:create:1001
5.2 Value 设计规范
// 1. 使用合适的序列化方式
// JSON:可读性好,通用性强
// Protobuf:性能好,体积小
// 2. 只缓存必要字段
@Data
public class ProductCacheDTO {
private Long id;
private String name;
private BigDecimal price;
private Integer stock;
// 不包含详情、图片列表等大字段
}
// 3. 避免大 value
// 单个 value 不超过 10KB
// 大数据考虑分片存储
5.3 过期时间设计
public class CacheExpireConfig {
// 不同类型数据的过期时间
public static final long USER_INFO_EXPIRE = 3600; // 1小时
public static final long PRODUCT_DETAIL_EXPIRE = 1800; // 30分钟
public static final long HOT_DATA_EXPIRE = 300; // 5分钟
public static final long SESSION_EXPIRE = 7200; // 2小时
public static final long TEMP_CACHE_EXPIRE = 60; // 1分钟
// 计算过期时间(带随机偏移)
public static long getExpireWithRandom(long baseExpire) {
// 随机偏移 10%
long offset = (long) (baseExpire * 0.1 * Math.random());
return baseExpire + offset;
}
}
5.4 缓存预热
@Component
public class CacheWarmUp implements ApplicationRunner {
@Autowired
private ProductService productService;
@Override
public void run(ApplicationArguments args) {
log.info("Starting cache warm up...");
// 预热热门商品
List<Long> hotProductIds = productService.getHotProductIds();
for (Long productId : hotProductIds) {
try {
productService.getProduct(productId);
} catch (Exception e) {
log.error("Warm up product {} failed", productId, e);
}
}
log.info("Cache warm up completed");
}
}
5.5 监控指标
// 使用 Prometheus + Grafana 监控缓存
@Component
public class CacheMetrics {
private final Counter cacheHitCounter = Counter.builder("cache_hit_total")
.description("Cache hit count")
.register(Metrics.globalRegistry);
private final Counter cacheMissCounter = Counter.builder("cache_miss_total")
.description("Cache miss count")
.register(Metrics.globalRegistry);
private final Timer cacheLoadTimer = Timer.builder("cache_load_duration")
.description("Cache load duration")
.register(Metrics.globalRegistry);
public void recordHit() {
cacheHitCounter.increment();
}
public void recordMiss() {
cacheMissCounter.increment();
}
public void recordLoad(Runnable loadTask) {
cacheLoadTimer.record(loadTask);
}
// 计算命中率
// hit_rate = cache_hit_total / (cache_hit_total + cache_miss_total)
}
六、实战案例
6.1 电商商品详情页
@Service
public class ProductDetailService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ProductMapper productMapper;
private static final String PRODUCT_KEY = "product:detail:";
private static final String LOCK_KEY = "lock:product:load:";
// 获取商品详情(解决缓存穿透 + 缓存击穿)
public Product getProductDetail(Long productId) {
String key = PRODUCT_KEY + productId;
// 1. 查询缓存
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
if ("NULL".equals(value)) {
return null; // 空值缓存
}
return JSON.parseObject(value, Product.class);
}
// 2. 缓存未命中,加锁查询
String lockKey = LOCK_KEY + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 等待获取锁,最多等待 3 秒
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 双重检查
value = redisTemplate.opsForValue().get(key);
if (value != null) {
return "NULL".equals(value) ? null :
JSON.parseObject(value, Product.class);
}
// 查询数据库
Product product = productMapper.selectById(productId);
if (product == null) {
// 缓存空值,防止穿透
redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
return null;
}
// 缓存商品数据
long expire = CacheExpireConfig.getExpireWithRandom(1800);
redisTemplate.opsForValue().set(key,
JSON.toJSONString(product), expire, TimeUnit.SECONDS);
return product;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
// 获取锁失败,返回默认数据或抛异常
throw new RuntimeException("系统繁忙,请稍后重试");
}
// 更新商品(延迟双删)
@Transactional
public void updateProduct(Product product) {
String key = PRODUCT_KEY + product.getId();
// 1. 删除缓存
redisTemplate.delete(key);
// 2. 更新数据库
productMapper.updateById(product);
// 3. 延迟再次删除
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500);
redisTemplate.delete(key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
6.2 排行榜缓存
@Service
public class RankingService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String RANKING_KEY = "ranking:product:sales";
private static final String RANKING_CACHE_KEY = "cache:ranking:product:sales";
// 更新销量排行(实时)
public void updateSalesRanking(Long productId, int salesCount) {
redisTemplate.opsForZSet().incrementScore(RANKING_KEY, String.valueOf(productId), salesCount);
}
// 获取排行榜(缓存 + 定时更新)
public List<RankingItem> getSalesRanking(int top) {
String cacheKey = RANKING_CACHE_KEY + ":" + top;
// 1. 查询缓存
String value = redisTemplate.opsForValue().get(cacheKey);
if (value != null) {
return JSON.parseArray(value, RankingItem.class);
}
// 2. 从 ZSet 查询
Set<ZSetOperations.TypedTuple<String>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(RANKING_KEY, 0, top - 1);
List<RankingItem> ranking = new ArrayList<>();
int rank = 1;
for (ZSetOperations.TypedTuple<String> tuple : tuples) {
RankingItem item = new RankingItem();
item.setRank(rank++);
item.setProductId(Long.parseLong(tuple.getValue()));
item.setSalesCount(tuple.getScore().intValue());
ranking.add(item);
}
// 3. 缓存结果(1分钟)
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(ranking), 60, TimeUnit.SECONDS);
return ranking;
}
}
七、总结
缓存模式选择
| 场景 | 推荐模式 |
|---|---|
| 通用场景 | Cache Aside |
| 写少读多 | Cache Aside + 缓存预热 |
| 写多读少 | Write Behind |
| 强一致性 | 分布式锁 + Cache Aside |
问题解决方案
| 问题 | 解决方案 |
|---|---|
| 缓存穿透 | 空值缓存 + 布隆过滤器 |
| 缓存击穿 | 互斥锁 + 逻辑过期 |
| 缓存雪崩 | 随机过期 + 多级缓存 + 熔断降级 |
| 数据不一致 | 延迟双删 + Binlog 订阅 |
最佳实践
- 合理设计 Key:规范命名,控制长度
- 控制 Value 大小:避免大 key,必要时分片
- 设置过期时间:避免数据无限增长
- 使用随机过期:防止缓存雪崩
- 监控命中率:目标 > 90%
- 预热热点数据:服务启动时加载