缓存穿透
概念
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要发生jdbc请求,去数据库再查询一遍,然后返回空。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
解决方案
如果查询数据库也为空,直接在Redis中设置一个为null的结果,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。当真正存入该数据时,清空相应的缓存即可。
缓存雪崩
概念
由于原有缓存失效(或者数据未加载到缓存中),新缓存未到期间(缓存正常从Redis中获取)所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机,造成系统的崩溃。
如果Redis中所有的key在同一实际失效的话,如果有大量的请求调用数据库,灰指甲导致整套系统瘫痪,这就是雪崩效应。
解决方案
- 加锁或者队列的方式
这样可以保证来保证不会有大量的线程对数据库一次性进行读写,避免缓存失效时对数据库造成太大的压力,虽然能够在一定的程度上缓解了数据库的压力。但是与此同时又降低了系统的吞吐量。
- 分析用户的行为,尽量让缓存失效的时间均匀分布(也可以均摊失效时间)
- 使用二级缓存(EhCache+Redis)
- 使用消息中间件方式
如果Redis查不到结果的情况下,这个时间直接扔给消息中间件,等到生产者生产了该消息后,消费者拿到消息就可以进行反馈了。
现在着重讲一下二级缓存(EhCache+Redis)
二级缓存(EhCache+Redis)解决雪崩效应
现在用EhCache作为一级缓存,Redis作为二级缓存,先走本地内存查询,本地缓存如果没有,再走网络连接,这样效率会更高。
1、pom文件引入
<dependencies>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- SpringBoot web 核心组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--开启 cache 缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- ehcache缓存 -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.9.1</version><!--$NO-MVN-MAN-VER$ -->
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- mysql 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
2、后端相关代码
首先封装对EhCache的基本操作的工具类
//封装对EhCache的基本操作
@Component
public class EhCacheUtils {
@Autowired
private EhCacheCacheManager ehCacheCacheManager;
// cacheName 和 key 区别 就是 redis中的db库 组
// 添加本地缓存 (相同的key 会直接覆盖)
public void put(String cacheName, String key, Object value) {
Cache cache = ehCacheCacheManager.getCacheManager().getCache(cacheName);
Element element = new Element(key, value);
cache.put(element);
}
// 获取本地缓存
public Object get(String cacheName, String key) {
Cache cache = ehCacheCacheManager.getCacheManager().getCache(cacheName);
Element element = cache.get(key);
return element == null ? null : element.getObjectValue();
}
//清除缓存
public void remove(String cacheName, String key) {
Cache cache = ehCacheCacheManager.getCacheManager().getCache(cacheName);
cache.remove(key);
}
}
.添加Redis的Service
@Component
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void setString(String key, Object object) {
String value = (String) object;
stringRedisTemplate.opsForValue().set(key, value);
}
public void setSet(String key, Object object) {
Set<String> value = (Set<String>) object;
for (String oj : value) {
stringRedisTemplate.opsForSet().add(key, oj);
}
}
public String getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
}
整合二级缓存
需要注意的是,Redis存放的是Json格式,而EhCache存放的是对象,所以需要格式转换。
@Service
public class UserService {
@Autowired
private EhCacheUtils ehCacheUtils;
@Autowired
private RedisService redisService;
@Autowired
private UserMapper userMapper;
private String cacheName = "userCache";
/**
* 1.先查询一级缓存
* 1.1如果一级缓存有对应的值,则直接返回
* 1.2如果没有,则查询二级缓存
* 1.2.1如果二级缓存有对应的值,则修改一级缓存,并返回
* 1.2.2如果没有,查询数据库,修改一级二级缓存
* @param id
* @return
*/
public Users getUser(Long id) {
// 1.先查询一级缓存 key 以 当前的类名+方法名称+id +参数值FD
String key = this.getClass().getName() + "-" + Thread.currentThread().getStackTrace()[1].getMethodName()
+ "-id:" + id;
// 1.1 查询一级缓存数据有对应的值存在,如果存在直接返回
Users users = (Users) ehCacheUtils.get(cacheName, key);
if (users != null) {
System.out.println("key:" + key + ",从一级缓存获取数据:users:" + users.toString());
return users;
}
// 1.1 查询一级缓存数据没有对应的值存在,直接查询二级缓存redis redis 中如何存放对象呢? json格式
// 2.查询二级缓存
String userJSON = redisService.getString(key);
// 如果redis缓存中有这个对应的 值,修改一级缓存
if (!StringUtils.isEmpty(userJSON)) {
JSONObject jsonObject = new JSONObject();
Users resultUser = jsonObject.parseObject(userJSON, Users.class);
// 存放在一级缓存
ehCacheUtils.put(cacheName, key, resultUser);
return resultUser;
}
// 3.查询db 数据库
Users user = userMapper.getUser(id);
if (user == null) {
return null;
}
// 存放二级缓存redis
redisService.setString(key, new JSONObject().toJSONString(user));
// 存放在一级缓存
ehCacheUtils.put(cacheName, key, user);
return user;
}
}