1. 项目背景与核心挑战
霸王餐活动作为餐饮行业常见的营销手段,其配置信息的读取性能直接影响用户体验和系统稳定性。在高峰期,传统数据库直连模式往往面临以下痛点:
- 瞬时查询压力:活动开始瞬间可能产生数万级QPS,直接冲击数据库
- 配置热数据集中:90%请求集中在少量热门活动配置上
- 数据一致性要求:活动规则变更需快速生效,不能有分钟级延迟
- 成本敏感:纯Redis方案内存成本过高,需平衡性能与资源消耗
我们团队通过Caffeine本地缓存+Redis分布式缓存的多级架构,将配置读取耗时从平均78ms降至3ms,Redis内存占用减少60%。下面分享具体实现方案。
2. 技术选型与架构设计
2.1 缓存层级划分策略
采用经典的三层缓存架构:
code复制┌─────────────┐ ┌─────────┐ ┌─────────┐
│ 本地缓存 │ ← │ 应用节点 │ → │ 分布式 │
│ (Caffeine) │ │ │ │ (Redis) │
└─────────────┘ └─────────┘ └─────────┘
↑ ↑
└─────────────────────┘
数据库同步监听
设计考量:
- Caffeine:作为JVM堆内缓存,提供纳秒级读取(性能比Guava Cache高30%)
- Redis:保证集群间数据一致,采用Hash结构存储配置项
- 数据库:作为唯一真实源,通过binlog监听实现数据同步
2.2 关键参数设计
java复制// Caffeine配置示例
Caffeine<Long, ActivityConfig> caffeine = Caffeine.newBuilder()
.maximumSize(10_000) // 根据业务测算的热key数量
.expireAfterWrite(30, TimeUnit.SECONDS) // 短TTL保证最终一致
.refreshAfterWrite(15, TimeUnit.SECONDS) // 异步刷新避免突发流量
.recordStats(); // 开启命中率统计
// Redis配置
String redisKey = "activity:{actId}"; // 冒号分隔的命名空间
int redisTtl = 300; // 5分钟过期
3. 核心实现细节
3.1 缓存加载流程
mermaid复制graph TD
A[客户端请求] --> B{本地缓存命中?}
B -->|是| C[返回缓存值]
B -->|否| D{Redis缓存命中?}
D -->|是| E[回填本地缓存]
D -->|否| F[查询数据库]
F --> G[异步写入Redis]
G --> H[返回结果]
关键代码实现:
java复制public ActivityConfig getConfig(Long actId) {
// 先查本地缓存
ActivityConfig config = localCache.getIfPresent(actId);
if (config != null) {
return config;
}
// 查Redis
String redisKey = "activity:" + actId;
String json = redisTemplate.opsForValue().get(redisKey);
if (json != null) {
config = JSON.parseObject(json, ActivityConfig.class);
localCache.put(actId, config); // 回填本地缓存
return config;
}
// 查数据库
config = dao.selectById(actId);
if (config != null) {
// 异步写入Redis
CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(
redisKey,
JSON.toJSONString(config),
300, TimeUnit.SECONDS
);
});
}
return config;
}
3.2 一致性保障方案
采用"标记失效+双删策略"保证数据一致性:
- 数据库变更时发送MQ消息
- 消费者接收到消息后:
- 先删除Redis数据
- 再发延迟消息(2s后执行)
- 延迟消息触发二次删除
注意:本地缓存通过短TTL(30s)保证最终一致,关键配置可调用
localCache.invalidate()主动清除
4. 性能优化实践
4.1 缓存预热策略
通过历史数据分析,提前加载热点活动配置:
sql复制-- 识别热点活动
SELECT act_id
FROM activity_access_log
WHERE create_time > NOW() - INTERVAL 7 DAY
GROUP BY act_id
ORDER BY COUNT(*) DESC
LIMIT 1000;
预热脚本示例:
python复制for act_id in hot_activities:
config = get_from_db(act_id)
redis.set(f"activity:{act_id}", json.dumps(config), ex=300)
4.2 监控指标设计
核心监控看板包含:
- 本地缓存命中率(预期>85%)
- Redis QPS及平均耗时
- 数据库查询频次
- 缓存加载耗时P99值
通过Grafana配置报警规则:
code复制local_cache_hit_rate < 80% for 5m
redis_latency > 50ms for 10m
5. 踩坑经验与避坑指南
典型问题1:缓存雪崩
- 现象:大量活动同时过期导致数据库压力骤增
- 解决方案:
- Redis过期时间添加随机偏移(±60s)
- 本地缓存采用refreshAfterWrite异步刷新
典型问题2:大Value阻塞
- 现象:某个活动配置包含10MB的规则描述
- 优化:
- 拆分为元数据+详情两个Key存储
- 对详情内容启用压缩(GZIP)
典型问题3:ABA问题
- 场景:并发更新导致配置回滚
- 解决:
- Redis操作使用CAS模式
- 版本号校验机制
6. 效果验证与数据对比
上线前后关键指标对比:
| 指标 | 改造前 | 改造后 | 提升幅度 |
|---|---|---|---|
| 平均响应时间 | 78ms | 3ms | 96%↓ |
| 数据库QPS | 12k | 150 | 98%↓ |
| Redis内存占用 | 8GB | 3.2GB | 60%↓ |
| 99分位延迟 | 210ms | 15ms | 92%↓ |
实际业务场景中,活动开始时的系统负载从原来的80%降至15%,未再出现因配置读取导致的超时问题。