作为从业十年的后端工程师,我见过太多团队在缓存使用上栽跟头。上周刚处理过一个线上事故:某电商平台大促时,因为缓存设置不当导致数据库被打垮,直接损失300多万订单。今天我就用真实代码案例,带你避开这些价值百万的坑。
缓存本质上是用空间换时间的艺术。通过将高频访问数据放在更快的存储介质(如内存)中,减少对慢速存储(如磁盘数据库)的访问。但就像赛车改装,如果调校不当,轻则性能不升反降,重则引发系统性崩溃。我们先看几个关键指标:
去年有个金融项目,团队把用户账户余额全部放在Redis里,结果服务器宕机后,部分数据永远丢失。这就是典型的缓存误用。
正确认知:
java复制// 正确配置示例
@Configuration
public class RedisConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 默认30分钟过期
.disableCachingNullValues() // 不缓存null
.serializeValuesWith(...);
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(...)
.build();
}
}
内存管理黄金法则:
maxmemory 8gb(根据实例大小调整)allkeys-lru淘汰策略平衡命中率和内存使用used_memory指标,超过70%就要扩容或优化某社交App曾在元旦零点遭遇雪崩:所有用户签到数据同时过期,瞬间5万QPS直接打垮MySQL。以下是两种经过验证的解决方案:
python复制def set_with_jitter(key, value, base_ttl=1800):
jitter = random.randint(-300, 300) # ±5分钟随机扰动
redis_client.setex(key, base_ttl + jitter, value)
java复制@Scheduled(fixedRate = 30_000) // 每30秒刷新
public void refreshHotData() {
List<Product> products = productService.getTop100();
products.forEach(p -> {
redisTemplate.opsForValue().set(
"product:" + p.getId(),
p,
Duration.ofMinutes(45) // 设置过期时间兜底
);
});
}
选型建议:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 随机过期 | 实现简单 | 仍有瞬时压力 | 数据量<10万 |
| 异步刷新 | 压力平稳 | 实现复杂 | 核心业务数据 |
某直播平台的头条主播信息Key过期时,导致数据库CPU飙到100%。我们用Redisson分布式锁解决了这个问题:
java复制public String getHotStarInfo(Long starId) {
String key = "hot:star:" + starId;
String data = redisTemplate.opsForValue().get(key);
if (data == null) {
RLock lock = redissonClient.getLock("lock:star:" + starId);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 双重检查
data = redisTemplate.opsForValue().get(key);
if (data == null) {
data = fetchFromDB(starId); // 数据库查询
redisTemplate.opsForValue().set(key, data, 1, TimeUnit.HOURS);
}
} else {
// 获取锁失败时返回旧数据或默认值
return getFallbackData(starId);
}
} finally {
lock.unlock();
}
}
return data;
}
关键细节:
某P2P平台曾遭遇CC攻击,黑客用不存在ID疯狂请求,导致数据库瘫痪。我们最终用布隆过滤器+空值缓存组合拳解决:
python复制from pybloom_live import ScalableBloomFilter
# 初始化布隆过滤器
bloom = ScalableBloomFilter(initial_capacity=1000000, error_rate=0.001)
# 预热数据
for user_id in User.objects.values_list('id', flat=True):
bloom.add(user_id)
def get_user_profile(user_id):
# 第一层防御:布隆过滤器
if not bloom.check(user_id):
return None
# 第二层防御:空值缓存
cache_key = f"user:{user_id}"
data = redis.get(cache_key)
if data == "NULL": # 特殊空值标记
return None
elif data:
return json.loads(data)
# 数据库查询
user = User.objects.filter(id=user_id).first()
if not user:
redis.setex(cache_key, 300, "NULL") # 缓存空值5分钟
return None
result = serialize_user(user)
redis.setex(cache_key, 3600, json.dumps(result))
return result
性能对比:
| 方案 | 内存占用 | 误判率 | 实现复杂度 |
|---|---|---|---|
| 空值缓存 | 中 | 无 | 低 |
| 布隆过滤器 | 低 | 可配置 | 中 |
| 组合方案 | 中低 | 极低 | 高 |
电商库存更新时,我们曾因缓存同步问题导致超卖。最终采用"先DB后缓存删除"策略:
java复制@Transactional
public void updateProductStock(Long productId, int delta) {
// 1. 更新数据库
productDao.updateStock(productId, delta);
// 2. 删除缓存
redisTemplate.delete("product:" + productId);
// 3. 异步记录操作日志
logAsync("stock_update", productId, delta);
}
异常处理机制:
我们开发了实时监控系统统计Key访问频次:
python复制class HotKeyMonitor:
def __init__(self):
self.counter = defaultdict(int)
def count(self, key):
self.counter[key] += 1
if self.counter[key] > 1000: # 阈值
self.notify_hotkey(key)
def notify_hotkey(self, key):
# 触发处理逻辑:如本地缓存、Key拆分等
pass
处理方案:
某次排查发现一个2MB的用户关系集合,导致Redis响应变慢。优化方案:
java复制// 原始大Key
redisTemplate.opsForSet().add("user:friends:1001", friendIds);
// 优化后
int partition = 0;
for (List<Long> batch : Lists.partition(friendIds, 1000)) {
String key = String.format("user:friends:1001:%d", partition++);
redisTemplate.opsForSet().add(key, batch);
redisTemplate.expire(key, 1, TimeUnit.DAYS);
}
拆分原则:
我们的高并发系统采用三级缓存:
code复制请求 → Nginx本地缓存 → Redis集群 → 进程内缓存(Caffeine) → DB
配置示例:
java复制@Configuration
public class CacheConfig {
@Bean
public CaffeineCacheManager caffeineCacheManager() {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.SECONDS); // 短时间存活
return new CaffeineCacheManager("localCache", caffeine);
}
}
各级缓存TTL设置:
| 层级 | TTL | 特点 |
|---|---|---|
| Nginx | 1-5s | 极短,应对突发流量 |
| Redis | 5-30m | 主要缓存层 |
| 本地 | 10-60s | 减轻Redis压力 |
我们在Grafana中配置的关键看板:
keyspace_hits/(keyspace_hits+keyspace_misses)used_memory_rss/maxmemoryredis_instance_latency_microsecondsevicted_keys突然增长是危险信号当收到告警时的处理流程:
python复制# 自动降级脚本示例
def get_data_with_fallback(key):
try:
return redis.get(key) or db_get(key)
except RedisError as e:
logger.warning(f"Redis故障,降级到DB: {e}")
return db_get(key)
except DBError as e:
logger.error(f"DB故障: {e}")
return get_stale_data_from_backup(key)
Spring Cache注解的陷阱:
java复制@CacheEvict(value="users", key="#user.id") // 可能失败但不抛异常
public void updateUser(User user) {
userDao.update(user);
// 建议增加额外删除操作
redisTemplate.delete("user:detail:" + user.getId());
}
推荐使用aiocache处理异步场景:
python复制from aiocache import cached, RedisCache
@cached(ttl=60, cache=RedisCache)
async def get_article(slug):
return await db.query("SELECT * FROM articles WHERE slug=?", slug)
针对时间敏感数据,我们使用Java Time API处理时区问题:
java复制@Cacheable(value="holidays", key="#date.toString()")
public List<Holiday> getHolidays(LocalDate date) {
ZoneId zone = ZoneId.of("Asia/Shanghai");
ZonedDateTime zdt = date.atStartOfDay(zone);
return holidayDao.findByDate(zdt.toInstant());
}
缓存就像系统的肾上腺素,用得好能起死回生,用不好会加速死亡。我的经验法则是:每次添加缓存时,必须同步考虑四个问题——过期策略、击穿保护、一致性方案和降级措施。最近我们正在试验Caffeine+Redis的多级缓存方案,效果显著但调试复杂度也翻倍,这个坑等我踩稳了再和大家分享。