1. Redis缓存实战:从基础策略到高并发优化
在电商、社交等互联网应用中,缓存是提升系统性能的关键组件。作为一款高性能的内存数据库,Redis凭借其丰富的数据结构和出色的读写速度,成为缓存系统的首选方案。但在实际业务中,简单的"读取-写入"缓存模式往往无法应对复杂场景,本文将基于真实项目经验,深入剖析Redis缓存的三大核心问题及解决方案。
2. 缓存更新策略与一致性保障
2.1 主动更新机制设计
缓存系统的首要挑战是如何保证数据一致性。我们采用"先更新数据库,再删除缓存"的策略,这种方案在大多数场景下能有效平衡性能与一致性需求。
@Override @Transactional public Result update(Shop shop) { Long id = shop.getId(); if (id == null) { return Result.fail("店铺id不能为空"); } // 1.更新数据库 updateById(shop); // 2.删除缓存 stringRedisTemplate.delete(CACHE_SHOP_KEY + id); return Result.ok(); }关键点:必须将数据库更新和缓存操作放在同一个事务中,确保原子性。如果第二步失败,整个事务应该回滚。
2.2 缓存查询流程优化
查询时采用标准的"缓存优先"策略,并设置合理的过期时间(TTL),防止长期数据不一致:
@Override public Result queryById(Long id) { String key = CACHE_SHOP_KEY + id; // 1.从redis查询商铺缓存 String shopJson = stringRedisTemplate.opsForValue().get(key); // 2.判断是否存在 if (StrUtil.isNotBlank(shopJson)) { // 3.存在,直接返回 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return Result.ok(shop); } // 4.不存在,根据id查询数据库 Shop shop = getById(id); // 5.不存在,返回错误 if (shop == null) { return Result.fail("店铺不存在!"); } // 6.存在,写入redis(设置30分钟过期) stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES); return Result.ok(shop); }实践经验:
- 过期时间应根据业务特点设置:高频变化数据设置较短TTL(如5-10分钟),低频变化数据可适当延长(如1-2小时)
- 使用Jackson或FastJSON等高效序列化工具,减少CPU开销
- 对于大对象,考虑采用压缩算法(如Snappy)减少内存占用
3. 缓存穿透防护实战
3.1 问题本质与危害分析
缓存穿透是指查询一个根本不存在的数据,导致请求直接穿透缓存层到达数据库。在恶意攻击场景下,大量这类请求可能导致数据库崩溃。
典型特征:
- Redis命中率骤降
- 数据库QPS异常升高
- 日志中出现大量
Key not found记录
3.2 解决方案对比
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 缓存空对象 | 将不存在的数据也缓存起来 | 实现简单 | 内存浪费 | 数据范围有限的情况 |
| 布隆过滤器 | 使用概率型数据结构判断存在性 | 内存效率高 | 存在误判 | 海量数据过滤 |
3.3 空对象缓存实现
public <R,ID> R queryWithPassThrough( String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){ String key = keyPrefix + id; // 1.从redis查询缓存 String json = stringRedisTemplate.opsForValue().get(key); // 2.判断是否存在 if (StrUtil.isNotBlank(json)) { return JSONUtil.toBean(json, type); } // 3.判断是否为空值(防穿透标记) if (json != null) { return null; } // 4.查询数据库 R r = dbFallback.apply(id); // 5.数据库不存在,缓存空值 if (r == null) { stringRedisTemplate.opsForValue().set( key, "", 2L, TimeUnit.MINUTES); // 短TTL return null; } // 6.存在则正常缓存 this.set(key, r, time, unit); return r; }优化技巧:
- 空值缓存应设置较短的TTL(如2-5分钟),避免长期占用内存
- 可结合IP限流等措施,防止恶意攻击者不断变换Key发起请求
- 对于明确无效的ID范围(如负数),可在业务层直接拦截
4. 缓存雪崩预防方案
4.1 问题场景还原
当大量缓存同时失效或Redis节点宕机时,请求洪峰直接冲击数据库,导致级联故障。某电商平台曾在促销活动开始时,因优惠券缓存集体过期,造成数据库瞬间过载。
4.2 多级防御策略
4.2.1 过期时间随机化
// 基础过期时间 + 随机偏移量(0~300秒) long expireTime = TimeUnit.MINUTES.toSeconds(30) + ThreadLocalRandom.current().nextInt(300); stringRedisTemplate.opsForValue().set( key, value, expireTime, TimeUnit.SECONDS);4.2.2 热点数据永不过期+逻辑过期
@Data public class RedisData { private LocalDateTime expireTime; // 逻辑过期时间 private Object data; } // 写入逻辑过期数据 public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) { RedisData redisData = new RedisData(); redisData.setData(value); redisData.setExpireTime(LocalDateTime.now() .plusSeconds(unit.toSeconds(time))); stringRedisTemplate.opsForValue().set( key, JSONUtil.toJsonStr(redisData)); }4.2.3 熔断降级机制
// 使用Resilience4j实现熔断 CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("dbCircuit"); Supplier<Shop> supplier = CircuitBreaker.decorateSupplier( circuitBreaker, () -> queryFromDB(id)); try { return supplier.get(); } catch (Exception e) { // 返回兜底数据 return getFallbackShop(id); }5. 缓存击穿应对方案
5.1 互斥锁方案
public <R, ID> R queryWithMutex( String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) { String key = keyPrefix + id; // 1.查询缓存 String json = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(json)) { return JSONUtil.toBean(json, type); } // 2.尝试获取锁 String lockKey = "lock:" + key; try { boolean isLock = tryLock(lockKey); if (!isLock) { Thread.sleep(50); // 休眠后重试 return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit); } // 3.再次检查缓存(双重检查) json = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(json)) { return JSONUtil.toBean(json, type); } // 4.查询数据库 R r = dbFallback.apply(id); if (r == null) { stringRedisTemplate.opsForValue().set( key, "", 2L, TimeUnit.MINUTES); return null; } // 5.写入缓存 this.set(key, r, time, unit); return r; } finally { unlock(lockKey); } }5.2 逻辑过期+异步重建
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); public <R, ID> R queryWithLogicalExpire( String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) { String key = keyPrefix + id; // 1.查询缓存 String json = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isBlank(json)) { return null; } // 2.解析逻辑过期时间 RedisData redisData = JSONUtil.toBean(json, RedisData.class); R r = JSONUtil.toBean((JSONObject) redisData.getData(), type); if (redisData.getExpireTime().isAfter(LocalDateTime.now())) { return r; // 未过期 } // 3.过期则尝试获取锁 String lockKey = "lock:" + key; if (tryLock(lockKey)) { // 4.异步重建 CACHE_REBUILD_EXECUTOR.submit(() -> { try { R newR = dbFallback.apply(id); this.setWithLogicalExpire(key, newR, time, unit); } finally { unlock(lockKey); } }); } return r; // 返回旧数据 }6. 工程化实践与工具类封装
6.1 缓存客户端设计
@Slf4j @Component public class CacheClient { private final StringRedisTemplate stringRedisTemplate; private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); // 基础set/get方法 public void set(String key, Object value, Long time, TimeUnit unit) { stringRedisTemplate.opsForValue().set( key, JSONUtil.toJsonStr(value), time, unit); } // 防穿透查询 public <R, ID> R queryWithPassThrough(...) { ... } // 互斥锁查询 public <R, ID> R queryWithMutex(...) { ... } // 逻辑过期查询 public <R, ID> R queryWithLogicalExpire(...) { ... } }6.2 业务层调用示例
// 防穿透查询 Shop shop = cacheClient.queryWithPassThrough( "cache:shop:", id, Shop.class, this::getById, 30L, TimeUnit.MINUTES); // 热点数据查询(需提前预热) cacheClient.setWithLogicalExpire( "hot:product:"+id, product, 10L, TimeUnit.MINUTES); Product product = cacheClient.queryWithLogicalExpire( "hot:product:", id, Product.class, this::getProductById, 10L, TimeUnit.MINUTES);7. 性能优化与监控
7.1 关键指标监控
- 缓存命中率:正常应保持在80%以上
- Redis内存使用率:避免超过70%警戒线
- 命令延迟:P99应小于5ms
- 连接数:合理设置连接池大小
7.2 实战建议
键设计规范:
- 使用冒号分隔的命名空间(如
user:session:123) - 避免过长的Key(不超过1KB)
- 对热点Key增加随机后缀(分片)
- 使用冒号分隔的命名空间(如
内存优化:
- 使用Hash结构存储对象
- 启用内存淘汰策略(volatile-lru)
- 定期分析大Key(redis-cli --bigkeys)
高可用保障:
- 部署Redis哨兵或集群
- 设置合理的超时和重试机制
- 实现本地缓存降级(Caffeine)
在电商秒杀系统中,我们通过组合使用逻辑过期和互斥锁方案,成功将数据库负载降低80%,QPS提升至3万+/秒。关键在于根据业务特点选择合适的缓存策略,并建立完善的监控体系。