视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
在高并发系统中,Redis 几乎成了“标配”。但很多团队以为加了 Redis 就万事大吉,结果上线后反而引发更严重的性能问题:CPU 飙升、内存爆炸、接口超时、数据库被打垮……
为什么?因为错误使用 Redis,比不用更危险!
本文结合真实生产事故,总结Redis 性能优化中最常见的 6 大陷阱,并提供Java + Spring Boot 实战解决方案,帮你避开“看似正确实则致命”的坑。
陷阱一:用KEYS *做模糊查询 → Redis 卡死!
❌ 反例(千万别写!)
// 错误:全量扫描所有 key(O(N) 复杂度) Set<String> keys = redisTemplate.keys("user:*"); for (String key : keys) { // 处理用户数据... }🔥 后果:
- 当 Redis 有 100 万个 key 时,
KEYS *会阻塞主线程数秒甚至数十秒。 - 所有请求排队等待,接口大面积超时,服务雪崩。
✅ 正确做法:用SCAN分批遍历
public List<String> scanUserKeys() { List<String> result = new ArrayList<>(); Cursor<String> cursor = redisTemplate.scan( ScanOptions.scanOptions() .match("user:*") .count(100) // 每次取100条 .build() ); while (cursor.hasNext()) { result.add(cursor.next()); } cursor.close(); // 别忘了关闭! return result; }📌原理:
SCAN是非阻塞的,每次只处理一小部分,不卡主线程。
陷阱二:缓存穿透 → 黑客刷不存在的 ID,打爆数据库!
🎯 场景:
用户请求/user?id=-999999,缓存查不到 → 直接查数据库 → 返回 null。
黑客每秒刷 1000 次 → 数据库连接池耗尽 → 系统瘫痪。
❌ 反例(裸奔式缓存):
public User getUser(Long id) { String json = redisTemplate.opsForValue().get("user:" + id); if (json != null) { return JSON.parseObject(json, User.class); } // 缓存未命中,直接查 DB! User user = userMapper.selectById(id); // 危险! if (user != null) { redisTemplate.opsForValue().set("user:" + id, JSON.toJSONString(user), 30, TimeUnit.MINUTES); } return user; }✅ 正确做法:缓存空值 + 布隆过滤器
// 方案1:缓存空值(简单有效) public User getUserWithPassThrough(Long id) { String key = "user:" + id; String json = redisTemplate.opsForValue().get(key); if (json != null) { return "null".equals(json) ? null : JSON.parseObject(json, User.class); } User user = userMapper.selectById(id); if (user != null) { redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES); } else { // 缓存空值,防止穿透! redisTemplate.opsForValue().set(key, "null", 2, TimeUnit.MINUTES); } return user; } // 方案2:布隆过滤器(适合海量数据) // (需引入 Guava 或 RedisBloom 插件)陷阱三:缓存击穿 → 热点 Key 过期瞬间,10万请求冲进 DB!
🎯 场景:
热门商品缓存 TTL=1 小时,第 3601 秒过期,10 万用户同时访问 → 全部打到数据库 → DB CPU 100%。
❌ 反例:普通缓存 + 固定过期
// 商品详情缓存(无防护) public Product getProduct(Long id) { String key = "product:" + id; String json = redisTemplate.opsForValue().get(key); if (json != null) return JSON.parseObject(json, Product.class); Product product = productMapper.selectById(id); redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 3600, TimeUnit.SECONDS); // 1小时后集体失效! return product; }✅ 正确做法:逻辑过期 + 互斥锁重建
// 1. 缓存结构包含逻辑过期时间 @Data public class RedisData { private LocalDateTime expireTime; private Object data; } // 2. 查询逻辑 public Product queryWithLogicalExpire(Long id) { String key = "product:" + id; String json = redisTemplate.opsForValue().get(key); if (json == null) return getProductFromDBAndSave(id); // 缓存未加载 RedisData redisData = JSON.parseObject(json, RedisData.class); Product product = (Product) redisData.getData(); // 未过期,直接返回 if (redisData.getExpireTime().isAfter(LocalDateTime.now())) { return product; } // 已过期,尝试重建(加锁) String lockKey = "lock:product:" + id; boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); if (isLock) { // 获取锁成功,开启异步重建 CACHE_REBUILD_EXECUTOR.submit(() -> { try { this.saveProductToCache(id, 20L); // 重建缓存,TTL=20分钟 } finally { redisTemplate.delete(lockKey); // 释放锁 } }); } return product; // 先返回旧数据(保底) }📌核心思想:物理上永不过期,业务层判断是否需要异步更新。
陷阱四:缓存雪崩 → 大量 Key 同时过期,系统崩溃!
🎯 场景:
促销活动结束,10 万个商品缓存同时过期 → 流量洪峰直冲数据库 → 服务雪崩。
❌ 反例:统一设置相同 TTL
// 所有商品缓存都设 2 小时过期 redisTemplate.opsForValue().set("product:" + id, json, 7200, TimeUnit.SECONDS);✅ 正确做法:随机过期时间
public void saveProductWithRandomTTL(Long id, Product product) { long baseTTL = 3600; // 基础1小时 long randomTTL = new Random().nextInt(600); // +0~10分钟随机值 redisTemplate.opsForValue().set( "product:" + id, JSON.toJSONString(product), baseTTL + randomTTL, TimeUnit.SECONDS ); }📌效果:避免“整点失效”,让过期时间分散。
陷阱五:Big Key 导致主线程阻塞
🎯 场景:
一个 Hash 存了 10 万个字段(如user:orders:1001),执行HGETALL时,Redis 主线程被占用 500ms → 所有请求延迟飙升。
❌ 反例:不分拆大对象
// 错误:把用户所有订单塞进一个 key Map<String, Order> allOrders = loadAllOrders(userId); redisTemplate.opsForHash().putAll("user:orders:" + userId, allOrders);✅ 正确做法:拆分 + 分页查询
// 按月份拆分 String key = "user:orders:" + userId + ":" + yearMonth; // 如 202501 redisTemplate.opsForHash().putAll(key, monthlyOrders); // 查询时指定月份 Map<Object, Object> orders = redisTemplate.opsForHash().entries("user:orders:1001:202501");🔍检测工具:定期运行
redis-cli --bigkeys找出大 Key。
陷阱六:连接池配置不当 → 高并发下连接耗尽
❌ 反例:默认连接池(太小!)
// Spring Boot 默认 JedisPool maxTotal=8,远远不够! @Autowired private StringRedisTemplate redisTemplate;✅ 正确做法:合理配置连接池
# application.yml spring: redis: jedis: pool: max-active: 200 # 最大连接数(建议 = QPS / 1000 * 2) max-idle: 50 min-idle: 10 max-wait: 2000ms # 获取连接最大等待时间💡计算公式:
max-active ≈ (峰值QPS × 平均响应时间(ms)) / 1000
例如:QPS=5000,平均耗时=20ms →5000×20/1000 = 100→ 建议设 120~200。
总结:Redis 性能优化黄金法则
| 陷阱 | 关键词 | 防御策略 |
|---|---|---|
| 全量扫描 | KEYS * | 改用SCAN |
| 缓存穿透 | 无效 ID | 缓存空值 + 布隆过滤器 |
| 缓存击穿 | 热点 Key 过期 | 逻辑过期 + 互斥锁 |
| 缓存雪崩 | 批量失效 | 随机 TTL + 高可用 |
| Big Key | 大对象 | 拆分 + 监控 |
| 连接池 | 耗尽 | 合理配置 max-active |
结语
Redis 是一把“双刃剑”:用得好,系统飞快;用得差,灾难现场。
记住:缓存不是银弹,防御性编程才是王道!
下次写 Redis 代码前,先问自己三个问题:
- 这个 key 会不会很大?
- 如果缓存失效,DB 能扛住吗?
- 我有没有用阻塞命令?
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!