找回密码
 立即注册
首页 业界区 业界 高效缓存的10条军规

高效缓存的10条军规

蔓好 2025-6-3 00:00:19
前言

"苏工!首页崩了!"
凌晨三点接到电话时,我正梦见自己成了缓存之神。
打开监控一看:
  1. 缓存命中率:0%  
  2. 数据库QPS:10万+  
  3. 线程阻塞数:2000+
复制代码
根本原因竟是之前有同事写的这段代码:
  1. public Product getProduct(Long id) {  
  2.     return productDao.findById(id);
  3. }
复制代码
直连数据库,未加缓存。
这一刻我意识到:不会用缓存的程序员,就像不会刹车的赛车手
今天这篇文章跟大家一起聊聊使用缓存的10条军规,希望对你会有所帮助。
军规1: 避免大key

反例场景
  1. @Cacheable(value = "user", key = "#id")  
  2. public User getUser(Long id) {  
  3.     return userDao.findWithAllRelations(id);
  4. }
复制代码
这里一次查询出了用户及其所有关联对象,然后添加到内存缓存中。
如果通过id查询用户信息的请求量非常大,会导致频繁的GC。
正确实践
  1. @Cacheable(value = "user_base", key = "#id")  
  2. public UserBase getBaseInfo(Long id) { /*...*/ }  
  3. @Cacheable(value = "user_detail", key = "#id")  
  4. public UserDetail getDetailInfo(Long id) { /*...*/ }
复制代码
这种情况,需要拆分缓存对象,比如:将用户基本信息和用户详细信息分开缓存。
缓存不是存储数据的垃圾桶,需要根据数据访问频率、读写比例、数据一致性要求进行分级管理。
大对象缓存会导致内存碎片化,甚至触发Full GC。
建议将基础信息(如用户ID、名称)与扩展信息(如订单记录)分离存储。
军规2: 永远设置过期时间

血泪案例
某系统将配置信息缓存设置为永不过期,导致修改配置后三天才生效。
正确配置
  1. @Cacheable(value = "config", key = "#key",  
  2.            unless = "#result == null",  
  3.            cacheManager = "redisCacheManager")  
  4. public String getConfig(String key) {  
  5.     return configDao.get(key);  
  6. }
复制代码
Redis配置如下:
  1. spring.cache.redis.time-to-live=300000 // 5分钟  
  2. spring.cache.redis.cache-null-values=false
复制代码
需要指定key的存活时间,比如:time-to-live设置成5分钟。
TTL设置公式
  1. 最优TTL = 平均数据变更周期 × 0.3
复制代码
深层思考
过期时间过短会导致缓存穿透风险,过长会导致数据不一致。
建议采用动态TTL策略。
例如电商商品详情页可设置30分钟基础TTL+随机5分钟抖动。
军规3: 避免批量失效

典型事故
所有缓存设置相同TTL,导致每天凌晨集中失效,数据库瞬时被打爆。
解决方案
使用基础TTL + 随机抖动的方案:
  1. public long randomTtl(long baseTtl) {  
  2.     return baseTtl + new Random().nextInt(300);
  3. }  
复制代码
TTL增加0-5分钟随机值。
使用示例
  1. redisTemplate.opsForValue().set(key, value, randomTtl(1800), TimeUnit.SECONDS);
复制代码
失效时间分布
1.webp

军规4: 需要增加熔断降级

我们在使用缓存的时候,需要增加熔断降级策略,防止万一缓存挂了,不能影响整个服务的可用性。
Hystrix实现示例
  1. @HystrixCommand(fallbackMethod = "getProductFallback",  
  2.                commandProperties = {  
  3.                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),  
  4.                    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000")  
  5.                })  
  6. public Product getProduct(Long id) {  
  7.     return productDao.findById(id);  
  8. }  
  9. public Product getProductFallback(Long id) {  
  10.     return new Product().setDefault(); // 返回兜底数据  
  11. }
复制代码
熔断状态机
2.webp

▶ 军规5: 空值缓存

在用户请求并发量大的业务场景种,我们需要把空值缓存起来。
防止大批量在系统中不存在的用户id,没有命中缓存,而直接查询数据库的情况。
典型代码
  1. public Product getProduct(Long id) {  
  2.     String key = "product:" + id;  
  3.     Product product = redis.get(key);  
  4.     if (product != null) {  
  5.         if (product.isEmpty()) { // 空对象标识  
  6.             return null;  
  7.         }  
  8.         return product;  
  9.     }  
  10.     product = productDao.findById(id);  
  11.     if (product == null) {  
  12.         redis.setex(key, 300, "empty"); // 缓存空值5分钟  
  13.         return null;  
  14.     }  
  15.     redis.setex(key, 3600, product);  
  16.     return product;  
  17. }
复制代码
空值缓存原理
3.webp

需要将数据库中返回的空值,缓存起来。
后面如果有相同的key查询数据,则直接从缓存中返回空值。
而无需再查询一次数据库。
军规6: 分布式锁用Redisson

用Redis做分布式锁的时候,可能会遇到很多问题。
感兴趣的小伙伴可以看看我的这篇文章《聊聊redis分布式锁的8大坑》。
建议大家使用Redisson做分布式锁。
Redisson分布式锁实现
  1. public Product getProduct(Long id) {  
  2.     String key = "product:" + id;  
  3.     Product product = redis.get(key);  
  4.     if (product == null) {  
  5.         RLock lock = redisson.getLock("lock:" + key);  
  6.         try {  
  7.             if (lock.tryLock(3, 30, TimeUnit.SECONDS)) {  
  8.                 product = productDao.findById(id);  
  9.                 redis.setex(key, 3600, product);  
  10.             }  
  11.         } finally {  
  12.             lock.unlock();  
  13.         }  
  14.     }  
  15.     return product;  
  16. }
复制代码
锁竞争流程图
4.webp

军规7: 延迟双删策略

在保证数据库和缓存双写数据一致性的业务场景种,可以使用延迟双删的策略。
例如:
  1. @Transactional  
  2. public void updateProduct(Product product) {  
  3.     // 1. 先删缓存  
  4.     redis.delete("product:" + product.getId());  
  5.     // 2. 更新数据库  
  6.     productDao.update(product);  
  7.     // 3. 延时再删  
  8.     executor.schedule(() -> {  
  9.         redis.delete("product:" + product.getId());  
  10.     }, 500, TimeUnit.MILLISECONDS);  
  11. }
复制代码
军规8: 最终一致性方案

延迟双删可能还有其他的问题。
对延迟双删问题比较感兴趣的小伙伴可以看看我的《如何保证数据库和缓存双写一致性?》,里面有详细的介绍。
我们可以使用最终一致性方案。
基于Binlog的方案
5.webp

DB更新数据之后,Canal会自动监听数据的变化,它会解析数据事件,然后发送一条MQ消息。
在MQ消费者中,删除缓存。
军规9: 热点数据预加载

对于一些经常使用的热点数据,我们可以提前做数据的预加载。
实时监控方案
  1. // 使用Redis HyperLogLog统计访问频率  
  2. public void recordAccess(Long productId) {  
  3.     String key = "access:product:" + productId;  
  4.     redis.pfadd(key, UUID.randomUUID().toString());  
  5.     redis.expire(key, 60); // 统计最近60秒  
  6. }  
  7. // 定时任务检测热点  
  8. @Scheduled(fixedRate = 10000)  
  9. public void detectHotKeys() {  
  10.     Set<String> keys = redis.keys("access:product:*");  
  11.     keys.forEach(key -> {  
  12.         long count = redis.pfcount(key);  
  13.         if (count > 1000) { // 阈值  
  14.             Long productId = extractId(key);  
  15.             preloadProduct(productId);  
  16.         }  
  17.     });  
  18. }
复制代码
定时任务检测热点,并且更新到缓存中。
军规10: 根据场景选择数据结构

血泪案例
某社交平台使用String类型存储用户信息。
错误用String存储对象:
  1. redis.set("user:123", JSON.toJSONString(user));  
复制代码
每次更新单个字段都需要反序列化整个对象。
导致问题:

  • 序列化/反序列化开销大
  • 更新单个字段需读写整个对象
  • 内存占用高
    正确实践:
  1. // 使用Hash存储  
  2. redis.opsForHash().putAll("user:123", userToMap(user));  
  3. // 局部更新  
  4. redis.opsForHash().put("user:123", "age", "25");
复制代码
数据结构选择矩阵:
6.webp

各数据结构最佳实践:
1.String

计数器
  1. redis.opsForValue().increment("article:123:views");  
复制代码
分布式锁
  1. redis.opsForValue().set("lock:order:456", "1", "NX", "EX", 30);  
复制代码
2.Hash

存储商品信息
  1. Map<String, String> productMap = new HashMap<>();  
  2. productMap.put("name", "iPhone15");  
  3. productMap.put("price", "7999");  
  4. redis.opsForHash().putAll("product:789", productMap);  
复制代码
部分更新
  1. redis.opsForHash().put("product:789", "stock", "100");  
复制代码
3.List

消息队列
  1. redis.opsForList().leftPush("queue:payment", orderJson);  
复制代码
最新N条记录
  1. redis.opsForList().trim("user:123:logs", 0, 99);
复制代码
4.Set

标签系统
  1. redis.opsForSet().add("article:123:tags", "科技", "数码");  
复制代码
共同好友
  1. redis.opsForSet().intersect("user:123:friends", "user:456:friends");
复制代码
5.ZSet

排行榜
  1. redis.opsForZSet().add("leaderboard", "player1", 2500);  
  2. redis.opsForZSet().reverseRange("leaderboard", 0, 9);  
复制代码
延迟队列
  1. redis.opsForZSet().add("delay:queue", "task1", System.currentTimeMillis() + 5000);
复制代码
总结

缓存治理黄金法则

问题类型推荐方案工具推荐缓存穿透空值缓存+布隆过滤器Redisson BloomFilter缓存雪崩随机TTL+熔断降级Hystrix/Sentinel缓存击穿互斥锁+热点预加载Redisson Lock数据一致性延迟双删+最终一致性Canal+RocketMQ
7.webp

最后忠告:缓存是把双刃剑,用得好是性能利器,用不好就是定时炸弹。
当你准备引入缓存时,先问自己三个问题:

  • 真的需要缓存吗?
  • 缓存方案是否完整?
  • 有没有兜底措施?
最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,我的所有文章都会在公众号上首发,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
本文收录于我的技术网站:http://www.susan.net.cn

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册