SpringBoot+Redis实战:破解高并发场景下的缓存三连击
Redis作为现代分布式系统的缓存利器,其性能优势毋庸置疑。但当缓存机制设计不当,系统便会遭遇缓存穿透、击穿、雪崩这三大致命问题。本文将基于黑马点评项目的实战经验,深入剖析这三种典型缓存问题的成因与解决方案。
1. 缓存穿透:当查询穿透到数据库
缓存穿透是指查询一个必然不存在的数据,由于缓存未命中,请求直接穿透到数据库层。恶意攻击者可能利用此漏洞发起大量无效查询,导致数据库不堪重负。
1.1 空对象缓存策略
黑马点评项目中的CacheClient类实现了空对象缓存机制:
public <R,ID> R queryWithPassThrough( String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit){ String key = keyPrefix + id; String json = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(json)){ return JSONUtil.toBean(json, type); } if (json != null){ // 命中空值 return null; } R r = dbFallback.apply(id); if (r == null){ // 缓存空对象,设置较短过期时间 stringRedisTemplate.opsForValue().set( key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } this.set(key, r, time, unit); return r; }关键实现点:
- 对数据库查询结果为null的情况,仍然缓存空字符串
- 为空的缓存设置较短的TTL(如2-5分钟),避免长期占用内存
- 下次相同查询直接返回null,避免穿透到数据库
1.2 布隆过滤器增强方案
对于海量数据场景,可结合布隆过滤器进行前置过滤:
| 方案 | 内存占用 | 误判率 | 实现复杂度 |
|---|---|---|---|
| 空对象缓存 | 较高 | 无 | 低 |
| 布隆过滤器 | 低 | 可配置 | 中 |
布隆过滤器虽然存在一定的误判率,但其内存效率极高。实际项目中可根据业务特点选择组合方案。
2. 缓存击穿:热点数据突然失效
当某个热点key过期瞬间,大量并发请求直接打到数据库,这就是缓存击穿。黑马点评采用逻辑过期+互斥锁双重保障机制。
2.1 逻辑过期设计
首先定义包含逻辑过期时间的Redis数据结构:
@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)); }2.2 互斥锁实现缓存重建
当检测到逻辑过期时,通过互斥锁控制只有一个线程进行缓存重建:
private boolean tryLock(String key){ Boolean flag = stringRedisTemplate.opsForValue() .setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } private void unlock(String key){ stringRedisTemplate.delete(key); }缓存查询的核心逻辑:
public <R,ID> R queryWithLogicalExpire(...){ // ... if (expireTime.isAfter(LocalDateTime.now())){ return r; } String lockKey = LOCK_SHOP_KEY + id; boolean isLock = tryLock(lockKey); if (isLock){ // 异步重建缓存 CACHE_REBUILD_EXECUTOR.submit(()->{ try { R r1 = dbFallback.apply(id); this.setWithLogicalExpire(key, r1, time, unit); } finally { unlock(lockKey); } }); } return r; }最佳实践建议:
- 锁的过期时间要短于缓存重建时间
- 使用线程池异步重建避免阻塞主线程
- 返回旧数据保证可用性,牺牲短暂一致性
3. 缓存雪崩:大规模缓存失效
当大量缓存同时过期或Redis宕机,请求洪峰直接冲击数据库,这就是缓存雪崩。黑马点评项目采用多级防御策略。
3.1 差异化过期时间
基础防护是为缓存设置随机过期时间:
// 基础过期时间 + 随机偏移量 long expireTime = BASE_TTL + RandomUtil.randomLong(5, 60); stringRedisTemplate.opsForValue().set( key, value, expireTime, TimeUnit.MINUTES);3.2 多级缓存架构
进阶方案是构建多级缓存体系:
用户请求 → Nginx本地缓存 → Redis集群 → JVM缓存 → 数据库各级缓存配置建议:
| 缓存层级 | 过期时间 | 特点 |
|---|---|---|
| Nginx | 1-5s | 应对瞬时高峰 |
| Redis | 5-30min | 主缓存层 |
| JVM | 1-2min | 进程级备份 |
3.3 熔断降级机制
配置Hystrix等熔断工具,当数据库压力过大时自动降级:
@HystrixCommand( fallbackMethod = "getShopInfoFallback", commandProperties = { @HystrixProperty( name="circuitBreaker.requestVolumeThreshold", value="20"), @HystrixProperty( name="circuitBreaker.sleepWindowInMilliseconds", value="10000") } ) public Shop getShopInfo(Long id) { // 正常业务逻辑 }4. 实战:优惠券秒杀中的缓存设计
黑马点评的秒杀模块完美融合了上述缓存策略,其核心流程如下:
- 库存预热:活动前将库存加载到Redis
- 资格校验:Lua脚本原子性扣减库存
- 订单处理:异步队列削峰填谷
库存扣减的Lua脚本:
local stockKey = 'seckill:stock:'..voucherId local orderKey = 'seckill:order:'..voucherId if (tonumber(redis.call('get', stockKey)) <= 0) then return 1 end if (redis.call('sismember', orderKey, userId) == 1) then return 2 end redis.call('incrby', stockKey, -1) redis.call('sadd', orderKey, userId) return 0秒杀系统的缓存要点:
- 库存数据永不过期(逻辑删除)
- 用户订单记录设置长TTL
- 使用Redis事务保证原子性
- 异步写库减轻数据库压力
5. 性能优化与监控
完善的缓存系统需要配套的监控措施:
5.1 关键指标监控
通过Prometheus监控以下核心指标:
redis_hit_rate{instance="cache01"} 0.98 redis_latency_ms{op="get"} 12.3 cache_penetration_count{type="null"} 425.2 缓存预热策略
系统启动时自动加载热点数据:
@PostConstruct public void initHotData() { List<Long> hotShopIds = shopMapper.selectHotShopIds(); hotShopIds.forEach(id -> { Shop shop = shopMapper.selectById(id); cacheClient.setWithLogicalExpire( CACHE_SHOP_KEY + id, shop, CACHE_SHOP_TTL, TimeUnit.MINUTES); }); }5.3 动态调整策略
基于监控数据自动优化缓存配置:
// 根据命中率动态调整TTL if (hitRate < 0.9) { cacheConfig.setDefaultTtl( cacheConfig.getDefaultTtl() * 1.5); } else if (hitRate > 0.98) { cacheConfig.setDefaultTtl( Math.max(300, cacheConfig.getDefaultTtl() * 0.8)); }在实际项目中,缓存策略需要根据业务特点灵活调整。黑马点评的CacheClient类提供了良好的基础封装,开发者可以基于此扩展更适合自己业务的缓存组件。记住:没有放之四海皆准的缓存方案,只有最适合当前场景的解决方案。