1. 캐시
1. 캐시란 무엇인가
캐싱 기능은 데이터 소스에 대한 액세스 빈도를 줄이는 것입니다. 이를 통해 시스템 성능이 향상됩니다.
캐시된 순서도
2. 캐시의 분류
2.1 로컬 캐시
실제로 캐시 데이터는 메모리(Map <String,Object>
)에 저장되는데, 모놀리식 아키텍처에서는 확실히 문제가 없습니다.
모놀리식 아키텍처에서의 캐싱 처리
2.2 분산 캐시
분산 환경에서는 다음과 같은 이유로 원래 로컬 캐시가 너무 많이 사용되지 않습니다.
- 캐시 데이터 중복성
- 캐싱이 효율적이지 않습니다.
분산 캐시의 구조도
3. Redis 통합
Redis를 통합하기 위해 SpringBoot 프로젝트 홈페이지에 해당 종속성을 추가합니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
그런 다음 해당 구성 정보를 추가해야 합니다.
Redis 데이터 테스트 동작
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testStringRedisTemplate(){
// 获取操作String类型的Options对象
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 插入数据
ops.set("name","bobo"+ UUID.randomUUID());
// 获取存储的信息
System.out.println("刚刚保存的值:"+ops.get("name"));
}
보기는 Redis 클라이언트 연결을 통해 볼 수 있습니다.
도구를 통해서도 볼 수 있습니다.
4. 3단계 분류를 변화시키세요
홈페이지에서 2차 및 3차 분류 데이터를 조회할 때 Redis를 사용하여 해당 데이터를 캐시하고 저장하여 검색 효율성을 향상시킬 수 있습니다.
@Override
public Map<String, List<Catalog2VO>> getCatelog2JSON() {
// 从Redis中获取分类的信息
String catalogJSON = stringRedisTemplate.opsForValue().get("catalogJSON");
if(StringUtils.isEmpty(catalogJSON)){
// 缓存中没有数据,需要从数据库中查询
Map<String, List<Catalog2VO>> catelog2JSONForDb = getCatelog2JSONForDb();
// 从数据库中查询到的数据,我们需要给缓存中也存储一份
String json = JSON.toJSONString(catelog2JSONForDb);
stringRedisTemplate.opsForValue().set("catalogJSON",json);
return catelog2JSONForDb;
}
// 表示缓存命中了数据,那么从缓存中获取信息,然后返回
Map<String, List<Catalog2VO>> stringListMap = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2VO>>>() {
});
return stringListMap;
}
그런 다음 3단계로 분류된 데이터에 대해 스트레스 테스트를 수행합니다.
스트레스 테스트 내용 | 스트레스 테스트용 스레드 수 | 처리량/초 | 90% 응답 시간 | 99% 응답 시간 |
---|---|---|---|---|
엔진스 | 50 | 7,385 | 10 | 70 |
게이트웨이 | 50 | 23,170 | 삼 | 14 |
서비스를 개별적으로 테스트 | 50 | 23,160 | 삼 | 7 |
게이트웨이+서비스 | 50 | 8,461 | 12 | 46 |
Nginx+게이트웨이 | 50 | |||
Nginx+게이트웨이+서비스 | 50 | 2,816 | 27 | 42 |
메뉴 | 50 | 1,321 | 48 | 74 |
3단계 분류 압력 테스트 | 50 | 12 | 4000 | 4000 |
3단계 분류 스트레스 테스트(비즈니스 최적화 후) | 50 | 448 | 113 | 227 |
3단계 분류 스트레스 테스트(Redis Cache) | 50 | 1163 | 49 | 59 |
비교해 보면 Redis 캐시를 추가한 후의 성능 개선 효과가 여전히 매우 뚜렷하다는 것을 알 수 있습니다.
5. 캐시 침투
존재하지 않아야 하는 데이터를 쿼리하는 것을 말합니다. 캐시가 히트되지 않았기 때문에 데이터베이스를 쿼리하지만 데이터베이스에 해당 레코드가 없습니다. 이 쿼리의 null 값을 캐시에 기록하지 않았으므로 결과는 다음과 같습니다. 존재하지 않는 데이터에 대한 모든 요청에서 쿼리하려면 스토리지 계층으로 이동해야 하며 이는 캐싱의 의미를 잃습니다.
존재하지 않는 데이터를 사용하여 공격하면 데이터베이스에 대한 즉각적인 압력이 증가하여 결국 충돌이 발생합니다.해결 방법은 상대적으로 간단하며 null 결과를 직접 캐시하고 짧은 만료 시간을 추가하면 됩니다.
6. 캐시 사태
캐시 사태란 캐시를 설정할 때 키가 동일한 만료 시간을 사용하여 캐시가 특정 순간에 동시에 만료되고 모든 요청이 DB로 전달되어 DB가 순간적으로 압력을 받고 눈사태가 발생하는 것을 의미합니다. .
해결 방법: 원래 만료 시간에 무작위 값(예: 1~5분)을 추가하면 캐시된 각 만료 시간의 반복률이 줄어들어 집합적인 오류 이벤트가 발생하기 어렵게 됩니다.
여기서 난수는 반드시 양수여야 하며, 무작위로 음수가 포함될 수 있으므로 유효기간이 무효화되며 예외가 보고된다는 점 유의하시기 바랍니다.
7. 캐시 분석
만료 시간이 설정된 일부 키의 경우 이러한 키가 특정 시점에 극도로 동시에 액세스될 수 있다면 이는 매우 "핫"한 데이터입니다. 동시에 많은 수의 요청이 들어오기 전에 이 키가 만료되면 이 키에 대한 모든 데이터 쿼리가 db로 이동하게 되는데, 이를 캐시 분석이라고 합니다.
해결 방법: 잠금은 동시성이 크므로 한 사람만 확인할 수 있고 다른 사람은 기다리게 됩니다. 확인 후 잠금이 해제됩니다. 다른 사람이 잠금을 얻으면 먼저 캐시를 확인하면 db로 이동하지 않고 데이터가 있게 됩니다.
그런데 스트레스 테스트를 해보니 출력 결과가 기대를 조금 벗어났습니다.
잠금 해제 및 쿼리 결과 캐싱의 타이밍 문제로 인해 쿼리가 두 번 수행되었습니다.
잠금 해제 타이밍과 결과 캐싱만 조정하면 됩니다.
그런 다음 완전한 코드 처리가 있습니다.
/**
* 查询出所有的二级和三级分类的数据
* 并封装为Map<String, Catalog2VO>对象
* @return
*/
@Override
public Map<String, List<Catalog2VO>> getCatelog2JSON() {
String key = "catalogJSON";
// 从Redis中获取分类的信息
String catalogJSON = stringRedisTemplate.opsForValue().get(key);
if(StringUtils.isEmpty(catalogJSON)){
System.out.println("缓存没有命中.....");
// 缓存中没有数据,需要从数据库中查询
Map<String, List<Catalog2VO>> catelog2JSONForDb = getCatelog2JSONForDb();
if(catelog2JSONForDb == null){
// 那就说明数据库中也不存在 防止缓存穿透
stringRedisTemplate.opsForValue().set(key,"1",5, TimeUnit.SECONDS);
}else{
// 从数据库中查询到的数据,我们需要给缓存中也存储一份
// 防止缓存雪崩
String json = JSON.toJSONString(catelog2JSONForDb);
stringRedisTemplate.opsForValue().set("catalogJSON",json,10,TimeUnit.MINUTES);
}
return catelog2JSONForDb;
}
System.out.println("缓存命中了....");
// 表示缓存命中了数据,那么从缓存中获取信息,然后返回
Map<String, List<Catalog2VO>> stringListMap = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2VO>>>() {
});
return stringListMap;
}
/**
* 从数据库查询的结果
* 查询出所有的二级和三级分类的数据
* 并封装为Map<String, Catalog2VO>对象
* 在SpringBoot中,默认的情况下是单例
* @return
*/
public Map<String, List<Catalog2VO>> getCatelog2JSONForDb() {
String keys = "catalogJSON";
synchronized (this){
/*if(cache.containsKey("getCatelog2JSON")){
// 直接从缓存中获取
return cache.get("getCatelog2JSON");
}*/
// 先去缓存中查询有没有数据,如果有就返回,否则查询数据库
// 从Redis中获取分类的信息
String catalogJSON = stringRedisTemplate.opsForValue().get(keys);
if(!StringUtils.isEmpty(catalogJSON)){
// 说明缓存命中
// 表示缓存命中了数据,那么从缓存中获取信息,然后返回
Map<String, List<Catalog2VO>> stringListMap = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2VO>>>() {
});
return stringListMap;
}
System.out.println("-----------》查询数据库操作");
// 获取所有的分类数据
List<CategoryEntity> list = baseMapper.selectList(new QueryWrapper<CategoryEntity>());
// 获取所有的一级分类的数据
List<CategoryEntity> leve1Category = this.queryByParenCid(list,0l);
// 把一级分类的数据转换为Map容器 key就是一级分类的编号, value就是一级分类对应的二级分类的数据
Map<String, List<Catalog2VO>> map = leve1Category.stream().collect(Collectors.toMap(
key -> key.getCatId().toString()
, value -> {
// 根据一级分类的编号,查询出对应的二级分类的数据
List<CategoryEntity> l2Catalogs = this.queryByParenCid(list,value.getCatId());
List<Catalog2VO> Catalog2VOs =null;
if(l2Catalogs != null){
Catalog2VOs = l2Catalogs.stream().map(l2 -> {
// 需要把查询出来的二级分类的数据填充到对应的Catelog2VO中
Catalog2VO catalog2VO = new Catalog2VO(l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
// 根据二级分类的数据找到对应的三级分类的信息
List<CategoryEntity> l3Catelogs = this.queryByParenCid(list,l2.getCatId());
if(l3Catelogs != null){
// 获取到的二级分类对应的三级分类的数据
List<Catalog2VO.Catalog3VO> catalog3VOS = l3Catelogs.stream().map(l3 -> {
Catalog2VO.Catalog3VO catalog3VO = new Catalog2VO.Catalog3VO(l3.getParentCid().toString(), l3.getCatId().toString(), l3.getName());
return catalog3VO;
}).collect(Collectors.toList());
// 三级分类关联二级分类
catalog2VO.setCatalog3List(catalog3VOS);
}
return catalog2VO;
}).collect(Collectors.toList());
}
return Catalog2VOs;
}
));
// 从数据库中获取到了对应的信息 然后在缓存中也存储一份信息
//cache.put("getCatelog2JSON",map);
// 表示缓存命中了数据,那么从缓存中获取信息,然后返回
if(map == null){
// 那就说明数据库中也不存在 防止缓存穿透
stringRedisTemplate.opsForValue().set(keys,"1",5, TimeUnit.SECONDS);
}else{
// 从数据库中查询到的数据,我们需要给缓存中也存储一份
// 防止缓存雪崩
String json = JSON.toJSONString(map);
stringRedisTemplate.opsForValue().set("catalogJSON",json,10,TimeUnit.MINUTES);
}
return map;
} }
8. 로컬 잠금의 한계
위에서 추가한 동기화 잠금은 로컬 잠금으로 단일 인스턴스만 잠글 수 있으며, 여러 컨테이너가 있는 분산 클러스터인 경우 이 잠금은 자신의 컨테이너만 잠글 수 있고 다른 컨테이너 노드는 잠글 수 없습니다.
분산 환경에서 로컬 잠금은 다른 노드의 작업을 잠글 수 없습니다. 이 상황은 확실히 문제가 있습니다. 데이터베이스를 확인하기 위해 여러 노드가 나타날 것입니다. 따라서
분산 클러스터 시나리오에서는 분산 잠금을 사용해야 합니다. 잠금
로컬 잠금 문제는 분산 잠금을 통해 해결해야 하는데, 분산 시나리오에서는 잠금 자체가 더 이상 필요하지 않다는 뜻인가요?
분산 환경의 각 노드가 요청 수를 제어하지 않으면 분산 잠금에 대한 압력이 매우 높기 때문에 분명히 그렇지 않습니다.이 때 각 노드의 동기화를 제어하려면 로컬 잠금이 필요합니다 . 분산 잠금 수 압력, 실제 개발에서는 로컬 잠금과 분산 잠금을 조합하여 사용합니다 .