[업무기능 87장] 마이크로서비스-springcloud-로컬캐시-재배포-분산캐시-캐시침투-눈사태-고장

1. 캐시

1. 캐시란 무엇인가

  캐싱 기능은 데이터 소스에 대한 액세스 빈도를 줄이는 것입니다. 이를 통해 시스템 성능이 향상됩니다.

이미지.png

이미지.png

캐시된 순서도

이미지.png

2. 캐시의 분류

2.1 로컬 캐시

  실제로 캐시 데이터는 메모리(Map <String,Object>)에 저장되는데, 모놀리식 아키텍처에서는 확실히 문제가 없습니다.

이미지.png

모놀리식 아키텍처에서의 캐싱 처리

이미지.png

2.2 분산 캐시

  분산 환경에서는 다음과 같은 이유로 원래 로컬 캐시가 너무 많이 사용되지 않습니다.

  • 캐시 데이터 중복성
  • 캐싱이 효율적이지 않습니다.

이미지.png

  분산 캐시의 구조도

이미지.png

3. Redis 통합

  Redis를 통합하기 위해 SpringBoot 프로젝트 홈페이지에 해당 종속성을 추가합니다.

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

  그런 다음 해당 구성 정보를 추가해야 합니다.

이미지.png

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 클라이언트 연결을 통해 볼 수 있습니다.

이미지.png

도구를 통해서도 볼 수 있습니다.

이미지.png

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 캐시를 추가한 후의 성능 개선 효과가 여전히 매우 뚜렷하다는 것을 알 수 있습니다.

이미지.png

5. 캐시 침투

  존재하지 않아야 하는 데이터를 쿼리하는 것을 말합니다. 캐시가 히트되지 않았기 때문에 데이터베이스를 쿼리하지만 데이터베이스에 해당 레코드가 없습니다. 이 쿼리의 null 값을 캐시에 기록하지 않았으므로 결과는 다음과 같습니다. 존재하지 않는 데이터에 대한 모든 요청에서 쿼리하려면 스토리지 계층으로 이동해야 하며 이는 캐싱의 의미를 잃습니다.

이미지.png

존재하지 않는 데이터를 사용하여 공격하면 데이터베이스에 대한 즉각적인 압력이 증가하여 결국 충돌이 발생합니다.해결 방법은 상대적으로 간단하며 null 결과를 직접 캐시하고 짧은 만료 시간을 추가하면 됩니다.

이미지.png

6. 캐시 사태

  캐시 사태란 캐시를 설정할 때 키가 동일한 만료 시간을 사용하여 캐시가 특정 순간에 동시에 만료되고 모든 요청이 DB로 전달되어 DB가 순간적으로 압력을 받고 눈사태가 발생하는 것을 의미합니다. .

이미지.png

해결 방법: 원래 만료 시간에 무작위 값(예: 1~5분)을 추가하면 캐시된 각 만료 시간의 반복률이 줄어들어 집합적인 오류 이벤트가 발생하기 어렵게 됩니다.
여기서 난수는 반드시 양수여야 하며, 무작위로 음수가 포함될 수 있으므로 유효기간이 무효화되며 예외가 보고된다는 점 유의하시기 바랍니다.

이미지.png

7. 캐시 분석

  만료 시간이 설정된 일부 키의 경우 이러한 키가 특정 시점에 극도로 동시에 액세스될 수 있다면 이는 매우 "핫"한 데이터입니다. 동시에 많은 수의 요청이 들어오기 전에 이 키가 만료되면 이 키에 대한 모든 데이터 쿼리가 db로 이동하게 되는데, 이를 캐시 분석이라고 합니다.

이미지.png

해결 방법: 잠금은 동시성이 크므로 한 사람만 확인할 수 있고 다른 사람은 기다리게 됩니다. 확인 후 잠금이 해제됩니다. 다른 사람이 잠금을 얻으면 먼저 캐시를 확인하면 db로 이동하지 않고 데이터가 있게 됩니다.

이미지.png

그런데 스트레스 테스트를 해보니 출력 결과가 기대를 조금 벗어났습니다.

이미지.png

잠금 해제 및 쿼리 결과 캐싱의 타이밍 문제로 인해 쿼리가 두 번 수행되었습니다.

이미지.png

잠금 해제 타이밍과 결과 캐싱만 조정하면 됩니다.

이미지.png

그런 다음 완전한 코드 처리가 있습니다.

/**
     * 查询出所有的二级和三级分类的数据
     * 并封装为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. 로컬 잠금의 한계

  위에서 추가한 동기화 잠금은 로컬 잠금으로 단일 인스턴스만 잠글 수 있으며, 여러 컨테이너가 있는 분산 클러스터인 경우 이 잠금은 자신의 컨테이너만 잠글 수 있고 다른 컨테이너 노드는 잠글 수 없습니다.
분산 환경에서 로컬 잠금은 다른 노드의 작업을 잠글 수 없습니다. 이 상황은 확실히 문제가 있습니다. 데이터베이스를 확인하기 위해 여러 노드가 나타날 것입니다. 따라서
분산 클러스터 시나리오에서는 분산 잠금을 사용해야 합니다. 잠금

이미지.png

로컬 잠금 문제는 분산 잠금을 통해 해결해야 하는데, 분산 시나리오에서는 잠금 자체가 더 이상 필요하지 않다는 뜻인가요?

이미지.png

분산 환경의 각 노드가 요청 수를 제어하지 않으면 분산 잠금에 대한 압력이 매우 높기 때문에 분명히 그렇지 않습니다.이 때 각 노드의 동기화를 제어하려면 로컬 잠금이 필요합니다   . 분산 잠금 수 압력, 실제 개발에서는 로컬 잠금과 분산 잠금을 조합하여 사용합니다 .

추천

출처blog.csdn.net/studyday1/article/details/132546591