作者:微信小助手
发布时间:2025-05-20T21:57:39
"苏工!首页崩了!" 几年前的一天晚上,我接到电话时,我正梦见自己成了缓存之神。 打开监控一看: 根本原因竟是之前有同事写的这段代码: 直连数据库,未加缓存。 这一刻我意识到:不会用缓存的程序员,就像不会刹车的赛车手。 今天这篇文章跟大家一起聊聊使用缓存的10条军规,希望对你会有所帮助。 反例场景: 这里一次查询出了用户及其所有关联对象,然后添加到内存缓存中。 如果通过id查询用户信息的请求量非常大,会导致频繁的GC。 正确实践: 这种情况,需要拆分缓存对象,比如:将用户基本信息和用户详细信息分开缓存。 缓存不是存储数据的垃圾桶,需要根据数据访问频率、读写比例、数据一致性要求进行分级管理。 大对象缓存会导致内存碎片化,甚至触发Full GC。 建议将基础信息(如用户ID、名称)与扩展信息(如订单记录)分离存储。 血泪案例: 正确配置: Redis配置如下: 需要指定key的存活时间,比如:time-to-live设置成5分钟。 TTL设置公式: 深层思考: 建议采用动态TTL策略。 例如电商商品详情页可设置30分钟基础TTL+随机5分钟抖动。 典型事故: 解决方案: 使用基础TTL + 随机抖动的方案: TTL增加0-5分钟随机值。 使用示例 失效时间分布: 我们在使用缓存的时候,需要增加熔断降级策略,防止万一缓存挂了,不能影响整个服务的可用性。 Hystrix实现示例: 熔断状态机: 在用户请求并发量大的业务场景种,我们需要把空值缓存起来。 防止大批量在系统中不存在的用户id,没有命中缓存,而直接查询数据库的情况。 典型代码: 空值缓存原理: 需要将数据库中返回的空值,缓存起来。 后面如果有相同的key查询数据,则直接从缓存中返回空值。 而无需再查询一次数据库。 用Redis做分布式锁的时候,可能会遇到很多问题。 感兴趣的小伙伴可以看看我的这篇文章《聊聊redis分布式锁的8大坑》。 建议大家使用Redisson做分布式锁。 Redisson分布式锁实现: 锁竞争流程图: 在保证数据库和缓存双写数据一致性的业务场景种,可以使用延迟双删的策略。 例如: 延迟双删可能还有其他的问题。 对延迟双删问题比较感兴趣的小伙伴可以看看我的《如何保证数据库和缓存双写一致性?》,里面有详细的介绍。 我们可以使用最终一致性方案。 基于Binlog的方案: DB更新数据之后,Canal会自动监听数据的变化,它会解析数据事件,然后发送一条MQ消息。 在MQ消费者中,删除缓存。 对于一些经常使用的热点数据,我们可以提前做数据的预加载。 实时监控方案: 定时任务检测热点,并且更新到缓存中。 血泪案例: 错误用String存储对象: 每次更新单个字段都需要反序列化整个对象。 导致问题: 数据结构选择矩阵: 各数据结构最佳实践: 计数器 分布式锁 存储商品信息 部分更新 消息队列 最新N条记录 标签系统 共同好友 排行榜 延迟队列 最后忠告:缓存是把双刃剑,用得好是性能利器,用不好就是定时炸弹。 当你准备引入缓存时,先问自己三个问题:缓存命中率:0%
数据库QPS:10万+
线程阻塞数:2000+public Product getProduct(Long id) {
return productDao.findById(id);
}
军规1: 避免大key
@Cacheable(value = "user", key = "#id")
public User getUser(Long id) {
return userDao.findWithAllRelations(id);
}@Cacheable(value = "user_base", key = "#id")
public UserBase getBaseInfo(Long id) { /*...*/ }
@Cacheable(value = "user_detail", key = "#id")
public UserDetail getDetailInfo(Long id) { /*...*/ }军规2: 永远设置过期时间
某系统将配置信息缓存设置为永不过期,导致修改配置后三天才生效。@Cacheable(value = "config", key = "#key",
unless = "#result == null",
cacheManager = "redisCacheManager")
public String getConfig(String key) {
return configDao.get(key);
}spring.cache.redis.time-to-live=300000 // 5分钟
spring.cache.redis.cache-null-values=false最优TTL = 平均数据变更周期 × 0.3
过期时间过短会导致缓存穿透风险,过长会导致数据不一致。
军规3: 避免批量失效
所有缓存设置相同TTL,导致每天凌晨集中失效,数据库瞬时被打爆。public long randomTtl(long baseTtl) {
return baseTtl + new Random().nextInt(300);
} redisTemplate.opsForValue().set(key, value, randomTtl(1800), TimeUnit.SECONDS);
军规4: 需要增加熔断降级
@HystrixCommand(fallbackMethod = "getProductFallback",
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000")
})
public Product getProduct(Long id) {
return productDao.findById(id);
}
public Product getProductFallback(Long id) {
return new Product().setDefault(); // 返回兜底数据
}
▶ 军规5: 空值缓存
public Product getProduct(Long id) {
String key = "product:" + id;
Product product = redis.get(key);
if (product != null) {
if (product.isEmpty()) { // 空对象标识
returnnull;
}
return product;
}
product = productDao.findById(id);
if (product == null) {
redis.setex(key, 300, "empty"); // 缓存空值5分钟
returnnull;
}
redis.setex(key, 3600, product);
return product;
}
军规6: 分布式锁用Redisson
public Product getProduct(Long id) {
String key = "product:" + id;
Product product = redis.get(key);
if (product == null) {
RLock lock = redisson.getLock("lock:" + key);
try {
if (lock.tryLock(3, 30, TimeUnit.SECONDS)) {
product = productDao.findById(id);
redis.setex(key, 3600, product);
}
} finally {
lock.unlock();
}
}
return product;
}
军规7: 延迟双删策略
@Transactional
public void updateProduct(Product product) {
// 1. 先删缓存
redis.delete("product:" + product.getId());
// 2. 更新数据库
productDao.update(product);
// 3. 延时再删
executor.schedule(() -> {
redis.delete("product:" + product.getId());
}, 500, TimeUnit.MILLISECONDS);
}军规8: 最终一致性方案
军规9: 热点数据预加载
// 使用Redis HyperLogLog统计访问频率
public void recordAccess(Long productId) {
String key = "access:product:" + productId;
redis.pfadd(key, UUID.randomUUID().toString());
redis.expire(key, 60); // 统计最近60秒
}
// 定时任务检测热点
@Scheduled(fixedRate = 10000)
public void detectHotKeys() {
Set
keys.forEach(key -> {
long count = redis.pfcount(key);
if (count > 1000) { // 阈值
Long productId = extractId(key);
preloadProduct(productId);
}
});
}军规10: 根据场景选择数据结构
某社交平台使用String类型存储用户信息。redis.set("user:123", JSON.toJSONString(user));
// 使用Hash存储
redis.opsForHash().putAll("user:123", userToMap(user));
// 局部更新
redis.opsForHash().put("user:123", "age", "25");
1.String
redis.opsForValue().increment("article:123:views");
redis.opsForValue().set("lock:order:456", "1", "NX", "EX", 30);
2.Hash
Map
productMap.put("name", "iPhone15");
productMap.put("price", "7999");
redis.opsForHash().putAll("product:789", productMap); redis.opsForHash().put("product:789", "stock", "100");
3.List
redis.opsForList().leftPush("queue:payment", orderJson);
redis.opsForList().trim("user:123:logs", 0, 99);
4.Set
redis.opsForSet().add("article:123:tags", "科技", "数码");
redis.opsForSet().intersect("user:123:friends", "user:456:friends");
5.ZSet
redis.opsForZSet().add("leaderboard", "player1", 2500);
redis.opsForZSet().reverseRange("leaderboard", 0, 9); redis.opsForZSet().add("delay:queue", "task1", System.currentTimeMillis() + 5000);
总结
缓存治理黄金法则