案例演示缓存穿透、缓存击穿、缓存雪崩
缓存处理流程分析:
前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库更新到缓存,并返回结果,数据库也没取到则返回空结果。
缓存穿透:
描述:
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,这种请求会导致数据库压力过大从而引发宕机
解决方案:
- 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
- 从缓存取到不同的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存设置的时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样也可以防止攻击的用户反复用同一个id暴力攻击
- 使用布隆过滤器
/**
* 查询spu商品库存
* @param goodsId
* @return
*/
public Map<Object, Object> getItemsStock(long goodsId) {
//防止缓存穿透,非法id直接返回
if(goodsId <= 0){
Map<Object, Object> result = new HashMap<>();
result.put("0", 0);
return result;
}
//查询数据库之前先查询缓存
Map<Object, Object> entries = redisTemplate.opsForHash().entries("goodsstock:" + goodsId);
if (entries != null && !entries.isEmpty()) {
return entries;
}
//查询数据库
List<Item> itemList = goodsFeign.getItemList(goodsId);
//判断数据库中查不到数据的话,添加空值缓存,防止缓存穿透
if(itemList == null || itemList.isEmpty()){
// 将空数据进行缓存起来
redisTemplate.opsForHash().put("goodsstock:" + goodsId, "0", "0");
redisTemplate.expire("goodsstock:" + goodsId, 5, TimeUnit.MINUTES);
Map<Object, Object> result = new HashMap<>();
result.put("0", "0");
return result;
}
Map<Object, Object> result = new HashMap<>();
itemList.forEach(item -> {
result.put(item.getId(), item.getNum());
//添加到缓存
redisTemplate.opsForHash().put("goodsstock:" + goodsId, item.getId().toString(), item.getNum().toString());
});
//提高缓存的利用率设置缓存的过期时间
redisTemplate.expire("goodsstock:" + goodsId, 1, TimeUnit.DAYS);
return result;
}
正常的库存:
非法库存:
当然,也可以使用过滤器直接对其进行拦截或者使用zull / gateway网关对这些直接进行拦截,由于比较简单就不再赘述。
缓存击穿:
描述:
缓存击穿是指缓存中没有数据但数据库中有数据(一般是缓存过期),这时由于并发用户特别多,同时读取缓存没有读到数据,又同时取数据库取数据,引起数据压力过大导致宕机
解决方案
- 设置热点数据永不过期,热点数据只要连续访问即可达到“永不过期”的状态
//查询数据库之前先查询缓存
Map<Object, Object> entries = redisTemplate.opsForHash().entries("goodsstock:" + goodsId);
if (entries != null && !entries.isEmpty()) {
//重置过期时间,连续访问就可以达到永不过期的状态
redisTemplate.expire("goodsstock:" + goodsId, 1, TimeUnit.DAYS);
return entries;
}
- 加互斥锁
//全局锁
package com.supergo.manager.config;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
@Component
public class GoodsMap {
//使用ConcurrentHashMap,线程同步
private ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<>();
//获得锁
public ReentrantLock getLock(Long goodsId){
return lockMap.getOrDefault(goodsId, new ReentrantLock());
}
//释放锁
public void removeLock(Long goodsId){
lockMap.remove(goodsId);
}
}
//互斥锁
/**
* 查询spu商品库存
* @param goodsId
* @return
*/
public Map<Object, Object> getItemsStock(long goodsId) {
//防止缓存穿透,非法id直接返回
if(goodsId <= 0){
Map<Object, Object> result = new HashMap<>();
result.put("0", 0);
return result;
}
//先获得锁, 获得goodsId对应的lock, 保证只能有一个线程访问同一个商品
ReentrantLock lock = goodsLock.getLock(goodsId);
if(lock.tryLock()){
//查询数据库
List<Item> itemList = goodsFeign.getItemList(goodsId);
//判断数据库中查不到数据的话,添加空值缓存,防止缓存穿透
if(itemList == null || itemList.isEmpty()){
// 将空数据进行缓存起来
redisTemplate.opsForHash().put("goodsstock:" + goodsId, "0", "0");
redisTemplate.expire("goodsstock:" + goodsId, 1, TimeUnit.MINUTES);
Map<Object, Object> result = new HashMap<>();
result.put("0", "0");
return result;
}
Map<Object, Object> result = new HashMap<>();
itemList.forEach(item -> {
result.put(item.getId(), item.getNum());
//添加到缓存
redisTemplate.opsForHash().put("goodsstock:" + goodsId, item.getId().toString(), item.getNum().toString());
});
//提高缓存的利用率设置缓存的过期时间
redisTemplate.expire("goodsstock:" + goodsId, 1, TimeUnit.DAYS);
//释放锁
lock.unlock();
//全局锁释放
goodsLock.removeLock(goodsId);
return result;
}else{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//进行递归获得锁
return getItemsStock(goodsId);
}
}
缓存雪崩:
描述:
缓存雪崩是指缓存中大批量的key到过期时间,而引起数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿是指并发查一条数据,缓存雪崩是大量不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署的话,将热点数据均匀分布在不同的缓存数据库中。
- 设置热点数据永不过期