在霸王餐这类高并发活动系统中,活动配置信息(如参与规则、库存总量、有效期等)的读取频率极高,但更新频率相对较低。每次请求都直接访问数据库会导致严重的IO压力,影响系统整体性能。我们实测发现,在未引入缓存前,数据库在活动高峰期CPU使用率经常超过80%,响应时间波动明显。
活动配置数据具有以下特点:
我们采用了两级缓存架构:
为什么不使用三级缓存(如再加一层CDN)?因为活动配置数据量小且变化频繁,CDN的更新延迟可能无法满足业务需求。
Caffeine选择理由:
Redis选择理由:
Redis中采用String类型存储序列化后的JSON数据,键格式为:
code复制activity:config:{activityId}
这种设计考虑:
完整的数据加载流程如下:
java复制public TrialActivity get(String activityId) {
// 1. 检查本地缓存
TrialActivity activity = localCache.getIfPresent(activityId);
if (activity != null) {
cacheHitCounter.increment("local");
return activity;
}
// 2. 检查Redis缓存
String redisKey = REDIS_KEY_PREFIX + activityId;
String json = redisTemplate.opsForValue().get(redisKey);
if (json != null) {
activity = deserialize(json);
localCache.put(activityId, activity);
cacheHitCounter.increment("redis");
return activity;
}
// 3. 从数据库加载
activity = dbRepository.findByActivityId(activityId);
if (activity != null) {
String serialized = serialize(activity);
redisTemplate.opsForValue().set(
redisKey,
serialized,
REDIS_TTL,
TimeUnit.MINUTES
);
localCache.put(activityId, activity);
cacheLoadCounter.increment("db");
}
return activity;
}
当后台修改活动配置时,需要主动清除缓存:
java复制public void evict(String activityId) {
// 立即失效本地缓存
localCache.invalidate(activityId);
// 异步删除Redis缓存
redisTemplate.delete(REDIS_KEY_PREFIX + activityId);
// 记录操作日志
cacheEvictCounter.increment();
}
对于特别热点的活动,我们实现了带锁的加载机制:
java复制public TrialActivity getWithLock(String activityId) {
return localCache.get(activityId, key -> {
String redisKey = REDIS_KEY_PREFIX + key;
// 双重检查
String json = redisTemplate.opsForValue().get(redisKey);
if (json != null) return deserialize(json);
// 获取分布式锁
String lockKey = "lock:" + redisKey;
boolean locked = false;
try {
locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (locked) {
TrialActivity fromDb = dbRepository.findByActivityId(key);
if (fromDb != null) {
redisTemplate.opsForValue().set(
redisKey,
serialize(fromDb),
10,
TimeUnit.MINUTES
);
}
return fromDb;
}
// 等待并重试
Thread.sleep(100);
return getWithLock(key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
if (locked) redisTemplate.delete(lockKey);
}
});
}
在活动开始前30分钟,我们会主动预热缓存:
我们监控以下核心指标:
Caffeine配置优化:
java复制Cache<String, TrialActivity> localCache = Caffeine.newBuilder()
.maximumSize(2000) // 根据JVM堆内存调整
.expireAfterWrite(5, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES) // 后台刷新
.recordStats() // 开启统计
.build();
Redis优化建议:
实施多级缓存后:
本地缓存不一致问题:早期没有及时清除本地缓存,导致节点间数据不一致。解决方案是引入Redis Pub/Sub通知机制。
缓存雪崩风险:大量缓存同时失效导致数据库压力骤增。通过设置随机过期时间(±10%)来分散失效时间。
序列化性能瓶颈:JSON序列化在高并发下成为瓶颈。改用Protobuf后性能提升30%。
这种架构还可以应用于:
未来可能的优化方向: