一、缓存?
对于一些经常访问但数据实时性不高且不经常改动的数据,我们希望通过一些方式减少数据库的访问压力,缓存便起到了很重要的作用。
在查询数据之前,我们先通过查询缓存(redis等)获取数据,如果获取到数据,则返回数据,如果获取不到数据,就对数据库进行查询,将查询到的结果先放入缓存中,然后再返回数据,下次再查询时就会从缓存中获取数据,于是大大减少了数据库的压力。
但是对于缓存也会存在许多的问题,常见的就有缓存穿透、缓存击穿、缓存雪崩等。
二、缓存穿透?
缓存穿透是指查询一个不存在的数据,由于缓存不命中,于是去查询数据库,但是数据库也没有这个数据,就导致了每次查询这个不存在的数据都要到数据库中去查询,缓存就失失去了作用
风险:恶意利用不存在的数据对数据库进行攻击,导致数据库压力增大,造成卡顿甚至是崩溃。
解决方法:①、对接口参数进行校验、防止出现恶意攻击;②、查询不到值时,将value设置成一个标记为加入缓存中,下次再查询就返回一个标记数而不必经过数据库,例如查询id为5的商品,不存在则返回一个-9999,然后在做逻辑判断,但是需要设置一个较短的缓存有效时间,防止以后key对应的value有数据的时候仍然返回空造成错误。③、使用bitmap类型定义一个可以访问的白名单,id作为偏移量。④、采用布隆过滤器
三、缓存雪崩?
缓存雪崩是指我们设置缓存时key采用了相同的到期时间,导致在某一时刻缓存同时失效,请求全部转到了数据,数据库压力过大导致崩溃。和缓存击穿不同的是,缓存击穿指并发查同一条不存在的数据而使缓存失效,数据库压力增大;缓存雪崩是指大量的key同时过期,导致直接查询数据库压力增大导致崩溃。
解决方法:①、设置热点数据永不过期;②、将缓存过期时间设置成某一段时间内的随机数,这样就不会同时过期;③、分布式处理缓存,将缓存存在不同的地方。
设置随机数过期时间,时间单位为天
Random random = new Random();
int i= random.nextInt(10);
stringRedisTemplate.opsForValue().set("catelogJSON", JSON.toJSONString(parent_cid), i, TimeUnit.DAYS);
四、缓存击穿?
缓存击穿是指对于一些设置了过期时间的key,这些key可能在某些时间被超高并发访问,是一种’热点‘数据,然后在这个数据被访问前正好key失效了,那么对这个key的查询会全部转到数据库上,造成数据库压力增大导致卡顿崩溃的现象。
解决方法:①、设置热点数据永不过期;②、加锁,大量并发只让一个人去查,其他人等待,直到以后释放锁,其他人读取到锁先查缓存。
本地锁
String data = stringRedisTemplate.opsForValue().get("data");//先从缓存中查数据
if (!StringUtils.isEmpty(data)) { //如果不为空,则直返回
return JSON.parseObject(data, new TypeReference< List<CategoryEntity>>() {});
}else { //缓存中无数据
synchronized (this) { //设置锁
// 双重检查 是否有缓存 (由于并发量较高,当大并发查询缓存无数据之后,进入该方法后应该再次查询是否有数据,有则返回)
data = stringRedisTemplate.opsForValue().get("data");
if (!StringUtils.isEmpty(data)) {
return JSON.parseObject(data, new TypeReference< List<CategoryEntity>>() {
});
}
List<CategoryEntity> entityList = baseMapper.selectList(null);
Random random = new Random();
int i= random.nextInt(10);
stringRedisTemplate.opsForValue().set("data", JSON.toJSONString(entityList), i, TimeUnit.DAYS);//将数据存入缓存中
return entityList;
}
}
使用redis的setnx作为分布式锁
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDBWithRedisLock() {
// 1.占分布式锁 设置这个锁10秒自动删除 [原子操作]
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if (lock) {
// 2.设置过期时间加锁成功 获取数据释放锁 [分布式下必须是Lua脚本删锁,不然会因为业务处理时间、网络延迟等等引起数据还没返回锁过期或者返回的过程中过期 然后把别人的锁删了]
Map<String, List<Catelog2Vo>> data;
try {
data = getDataFromDB(); //处理业务
} finally {
// 删除也必须是原子操作 Lua脚本操作 删除成功返回1 否则返回0
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
// 原子删锁
stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), uuid);
}
return data;
} else {
// 重试加锁
try {
// 登上两百毫秒
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatelogJsonFromDBWithRedisLock();
}
}