第四部分:缓存与性能优化:让系统飞起来
4.1 Redis缓存策略:热点数据预热
秒杀活动开始前,需要将商品信息预热到Redis中。
@Service
public class CacheService {
@PostConstruct
public void initCache() {
log.info("开始预热商品缓存...");
List<SeckillGoods> goodsList = seckillGoodsMapper.selectList(null);
for (SeckillGoods goods : goodsList) {
// 缓存商品详情
String goodsKey = "seckill:goods:" + goods.getId();
redisTemplate.opsForValue().set(goodsKey, goods, 1, TimeUnit.HOURS);
// 缓存商品库存(用于秒杀预减)
String stockKey = "seckill:stock:" + goods.getId();
redisTemplate.opsForValue().set(stockKey, goods.getStockCount());
log.info("缓存商品: id={}, stock={}", goods.getId(), goods.getStockCount());
}
}
}
为什么需要预热?
秒杀开始时,大量请求会查询商品信息
如果缓存是空的,所有请求都会打到数据库,数据库瞬间被打垮
预热让缓存提前准备好,数据库压力大大降低
4.2 缓存三大问题及解决方案
问题一:缓存穿透
缓存穿透是指查询一个不存在的数据。由于缓存中没有,请求会直接打到数据库。恶意攻击者可以利用这一点,用大量不存在的key攻击系统。
解决方案:缓存空值
public SeckillGoods getGoods(Long goodsId) {
// 先从缓存获取
SeckillGoods goods = redisTemplate.opsForValue().get("goods:" + goodsId);
if (goods != null) {
return goods;
}
// 缓存没有,查询数据库
goods = seckillGoodsMapper.selectById(goodsId);
if (goods != null) {
// 存在,写入缓存
redisTemplate.opsForValue().set("goods:" + goodsId, goods);
} else {
// 不存在,缓存空对象(设置较短过期时间)
redisTemplate.opsForValue().set("goods:" + goodsId, new SeckillGoods(), 60);
}
return goods;
}
问题二:缓存击穿
缓存击穿是指一个热点key过期瞬间,大量请求同时打到数据库。
解决方案:分布式锁
public SeckillGoods getGoodsWithLock(Long goodsId) {
SeckillGoods goods = redisTemplate.opsForValue().get("goods:" + goodsId);
if (goods != null) {
return goods;
}
// 尝试获取锁
RLock lock = redissonClient.getLock("goods:lock:" + goodsId);
if (lock.tryLock()) {
try {
// 双重检查
goods = redisTemplate.opsForValue().get("goods:" + goodsId);
if (goods != null) {
return goods;
}
// 查询数据库
goods = seckillGoodsMapper.selectById(goodsId);
redisTemplate.opsForValue().set("goods:" + goodsId, goods);
} finally {
lock.unlock();
}
} else {
// 等待一小段时间后重试
Thread.sleep(100);
return getGoodsWithLock(goodsId);
}
return goods;
}
问题三:缓存雪崩
缓存雪崩是指大量缓存同时过期,导致数据库压力骤增。
解决方案:随机过期时间
// 设置随机过期时间,避免同时失效
int randomTtl = 3600 + new Random().nextInt(600); // 3600~4200秒
redisTemplate.opsForValue().set(key, value, randomTtl, TimeUnit.SECONDS);