[ビジネス機能 第 87 章] マイクロサービス-スプリングクラウド-ローカル キャッシュ-redis-分散キャッシュ-キャッシュ ペネトレーション-アバランチ-ブレークダウン

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% の応答時間
Nginx 50 7,385 10 70
ゲートウェイ 50 23,170 3 14
サービスを個別にテストする 50 23,160 3 7
ゲートウェイ+サービス 50 8,461 12 46
Nginx+ゲートウェイ 50
Nginx+ゲートウェイ+サービス 50 2,816 27 42
メニュー 50 1,321 48 74
三段階分類圧力試験 50 12 4000 4000
3段階分類ストレステスト(業務最適化後) 50 448 113 227
3 レベル分類ストレス テスト (Redis キャッシュ) 50 1163 49 59

  比較すると、Redis キャッシュを追加した後のパフォーマンス向上効果が依然として非常に明白であることがわかります。

画像.png

5. キャッシュの侵入

  明らかに存在しないデータをクエリすることを指します。キャッシュがヒットしないため、データベースがクエリされますが、データベースにはそのようなレコードがありません。このクエリの null 値をキャッシュに書き込んでいません。この存在しないデータに対するすべてのリクエストが発生し、ストレージ層にアクセスしてクエリを実行する必要があり、キャッシュの意味が失われます。

画像.png

存在しないデータを使用して攻撃すると、データベースに対する瞬間的な圧力が増大し、最終的にはクラッシュにつながります。解決策は比較的単純で、null の結果を直接キャッシュし、短い有効期限を追加するだけです。

画像.png

6. キャッシュなだれ

  キャッシュ雪崩とは、キャッシュを設定するときにキーが同じ有効期限を使用し、特定の瞬間にキャッシュが同時に期限切れになり、すべてのリクエストが DB に転送され、DB が瞬間的な圧力と雪崩にさらされることを意味します。 。

画像.png

解決策: 元の有効期限にランダムな値 (1 ~ 5 分など) を追加すると、キャッシュされた各有効期限の繰り返し率が減少し、集合的な障害イベントが発生しにくくなります。
ここでの乱数は正の数である必要がありますが、負の数がランダムに存在する可能性があるため、有効期間は無効になり、例外が報告されることに注意してください。

画像.png

7. キャッシュの内訳

  有効期限が設定されている一部のキーの場合、これらのキーが特定の時点で非常に同時にアクセスされる可能性がある場合、それは非常に「ホット」なデータです。多数のリクエストが同時に届く前にこのキーの有効期限が切れると、このキーに対するすべてのデータ クエリがデータベースに送られます。これをキャッシュ ブレークダウンと呼びます。

画像.png

解決策: 同時実行性が高いロックでは、1 人だけがチェックでき、他の人は待機します。チェック後、ロックは解放されます。他の人がロックを取得した場合は、最初にキャッシュをチェックすると、DB に行かなくてもデータが存在します。

画像.png

しかし、ストレス テストを実行したところ、出力結果は私たちの予想を少し超えていました。

画像.png

ロックの解放とクエリ結果のキャッシュにおけるタイミングの問題により、クエリが 2 回実行されました。

画像.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