是什么
广义的缓存就是在第一次加载某些可能会复用数据的时候,在加载数据的同时,将数据放到一个指定的地点做保存。再下次加载的时候,从这个指定地点去取数据。这里加缓存是有一个前提的,就是从这个地方取数据,比从数据源取数据要快的多。
Java狭义一些的缓存,主要是指三大类
-
虚拟机缓存(ehcache,JBoss Cache)
-
分布式缓存(redis,memcache)
-
数据库缓存
正常来说,速度由上到下依次减慢↓
怎么做
1)缓存雪崩
由于
原有缓存失效
(或者数据未加载到缓存中),新缓存未到期间(缓存正常从Redis中获取,如下图)所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机,造成系统的崩溃。
正常情况:
缓存失效:
解决措施
方式1:加锁或者队列
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量
。
比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。虽然能够在一定的程度上缓解了数据库的压力但是与此同时又降低了系统的吞吐量。
public Users getByUsers(Long id) {
// 1.先查询redis
String key = this.getClass().getName() + "-" + Thread.currentThread().getStackTrace()[1].getMethodName() + "-id:" + id;
String userJson = redisService.getString(key);
if (!StringUtils.isEmpty(userJson)) {
Users users = JSONObject.parseObject(userJson, Users.class);
return users;
}
Users user = null;
try {
lock.lock(); // 查询db
user = userMapper.getUser(id);
redisService.setSet(key, JSONObject.toJSONString(user));
} catch (Exception e) {
} finally {
lock.unlock();
}
return user;
}
加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法。
方式2:设置不同的过期时间
分析用户的行为,不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
2)缓存穿透
缓存穿透是指用户查询数据,
在数据库没有,自然在缓存中也不会有
。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
解决措施
方式1:设一个默认值放到缓存
如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
方式2:缓存空结果
把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,既可以避免当查询的值为空时引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放行给后面的正常缓存处理逻辑。
public String getByUsers2(Long id) {
// 1.先查询redis
String key = this.getClass().getName() + "-" + Thread.currentThread().getStackTrace()[1].getMethodName() + "-id:" + id;
String userName = redisService.getString(key);
if (!StringUtils.isEmpty(userName)) {
return userName;
}
System.out.println("######开始发送数据库DB请求########");
Users user = userMapper.getUser(id);
String value = null;
if (user == null) { // 标识为null
value = "";
} else {
value = user.getName();
}
redisService.setString(key, value);
return value;
}
再给对应的ip存放真值的时候,需要先清除对应的之前的空缓存。
最好是设置过期时间,这样不影响正常值
3)缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热 点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对
某一key缓存失效
,前者则是很多key。
热点key:
某个key访问非常频繁,当key失效的时候有大量线程来构建缓存,导致负载增加,系统崩溃。
解决方案
解决办法:
- ①使用锁,单机用synchronized,lock等,分布式用分布式锁。
- ②缓存过期时间不设置,而是设置在key对应的value里。如果检测到存的时间超过过期时间则异步更新缓存。