1. 项目背景与核心需求
在本地生活服务类应用中,店铺信息的缓存管理直接影响用户体验和系统稳定性。黑马点评作为一个典型的O2O平台,面临着高并发查询请求与数据实时性的双重挑战。当用户频繁刷新附近店铺列表时,不当的缓存策略会导致:
- 雪崩风险:大量缓存同时失效引发数据库查询风暴
- 脏读问题:后台修改店铺信息后前端仍显示旧数据
- 资源浪费:冷门店铺数据长期占用内存空间
我们需要的解决方案必须同时满足三个核心诉求:
- 自动清理过期缓存释放内存(超时剔除)
- 数据变更时立即更新缓存(主动更新)
- 避免高频更新导致的系统抖动
2. 技术方案设计
2.1 缓存架构选型
采用二级缓存结构实现分层防护:
用户请求 → Redis集群(分布式缓存) → Caffeine(本地缓存) → 数据库选择Redis作为主缓存的原因:
- 支持丰富的过期策略(TTL/惰性删除/定期删除)
- 提供Pub/Sub机制实现跨节点通知
- 原生支持原子操作和Lua脚本
2.2 超时剔除策略实现
2.2.1 基础TTL设置
// 店铺基础信息缓存30分钟 redisTemplate.opsForValue().set( "shop:" + id, shop, 30, TimeUnit.MINUTES );2.2.2 动态过期时间优化
针对不同热度的店铺实施差异化过期策略:
| 店铺类型 | 访问频率 | TTL设置 | 淘汰策略 |
|---|---|---|---|
| 头部热门店铺 | >100次/分 | 2小时 | 滑动过期时间 |
| 普通店铺 | 10-100次 | 30分钟 | 固定过期时间 |
| 长尾冷门店铺 | <10次 | 10分钟 | 提前异步淘汰 |
实现代码示例:
public void setShopCache(Shop shop, int accessFrequency) { long ttl = 30 * 60; // 默认30分钟 if (accessFrequency > 100) { ttl = 2 * 60 * 60; } else if (accessFrequency < 10) { ttl = 10 * 60; } redisTemplate.opsForValue().set( "shop:" + shop.getId(), shop, ttl, TimeUnit.SECONDS ); }2.3 主动更新机制
2.3.1 数据库变更监听
通过MySQL binlog订阅实现数据变更感知:
@EventListener public void handleShopUpdate(ShopUpdateEvent event) { // 1. 更新Redis缓存 redisTemplate.opsForValue().set( "shop:" + event.getShopId(), event.getNewData(), getRemainingTTL(event.getShopId()), TimeUnit.SECONDS ); // 2. 发布缓存更新消息 redisTemplate.convertAndSend( "shop.update", event.getShopId() ); }2.3.2 多级缓存同步
使用Redis Pub/Sub通知所有节点更新本地缓存:
@RedisListener(channel = "shop.update") public void onShopUpdate(Long shopId) { // 1. 删除本地缓存 caffeineCache.invalidate("shop:" + shopId); // 2. 异步预加载 executorService.submit(() -> { Shop shop = loadFromRedis(shopId); caffeineCache.put("shop:" + shopId, shop); }); }3. 关键实现细节
3.1 缓存雪崩防护
采用三级防护策略:
错峰过期:基础TTL添加随机扰动值(±5分钟)
long baseTtl = 30 * 60 * 1000; // 30分钟 long randomOffset = (long)(Math.random() * 5 * 60 * 1000); long finalTtl = baseTtl + (randomOffset % 2 == 0 ? randomOffset : -randomOffset);熔断降级:当数据库QPS超过阈值时返回降级数据
@CircuitBreaker( fallbackMethod = "getShopFallback", threshold = 5000 // 5秒内超过5000次查询触发熔断 ) public Shop getShop(Long id) { // 正常查询逻辑 }预热加载:定时任务提前刷新即将过期的热门店铺缓存
3.2 缓存一致性保障
实现双删策略+消息队列重试:
public void updateShop(Shop shop) { // 第一次删除 redisTemplate.delete("shop:" + shop.getId()); // 更新数据库 shopMapper.updateById(shop); // 第二次删除(延迟500ms) executorService.schedule(() -> { redisTemplate.delete("shop:" + shop.getId()); }, 500, TimeUnit.MILLISECONDS); // 消息队列补偿 mqTemplate.send("cache_clean", shop.getId()); }4. 性能优化技巧
4.1 热点数据特殊处理
对Top 100店铺实施:
本地缓存:使用Caffeine实现JVM级缓存
Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(5, TimeUnit.MINUTES) .build();缓存分片:将热门店铺数据分散到多个key
// 基础信息 redisTemplate.opsForValue().set("shop:base:"+id, shopBase); // 统计信息 redisTemplate.opsForValue().set("shop:stats:"+id, shopStats);
4.2 批量查询优化
使用Redis Pipeline批量获取店铺数据:
List<Object> results = redisTemplate.executePipelined( (RedisCallback<Object>) connection -> { for (Long shopId : shopIds) { connection.stringCommands().get(("shop:" + shopId).getBytes()); } return null; } );5. 监控与运维方案
5.1 监控指标配置
通过Prometheus采集关键指标:
| 指标名称 | 告警阈值 | 监控意义 |
|---|---|---|
| cache_hit_rate | <90% 持续5分钟 | 缓存命中率下降 |
| redis_memory_usage | >80% 持续10分钟 | 内存不足风险 |
| cache_update_delay | >1秒 持续3次 | 数据一致性延迟 |
5.2 运维操作手册
缓存强制刷新流程:
- 查询缓存键模式:
keys shop:* - 批量删除旧缓存:
redis-cli --scan --pattern 'shop:*' | xargs redis-cli del - 触发预热加载:
POST /cache/warm-up?type=shop
紧急扩容方案: 当Redis内存超过警戒线时:
- 临时增加节点分担压力
- 动态调整淘汰策略为volatile-lru
- 对非核心业务数据降级处理
6. 实测效果对比
上线前后关键指标对比:
| 指标项 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 平均响应时间 | 320ms | 85ms | 73%↓ |
| 数据库QPS峰值 | 8500 | 1200 | 85%↓ |
| 缓存命中率 | 68% | 93% | 25%↑ |
| 内存使用量 | 32GB | 18GB | 44%↓ |
这个方案在实际运行中表现出三个显著优势:
- 通过动态TTL机制,冷门店铺缓存及时释放,内存占用减少40%+
- 结合主动推送和惰性更新,数据一致性延迟控制在500ms内
- 多级缓存架构使核心接口的P99耗时从420ms降至110ms