技術共有

キャッシュ - キャッシュは 2 を使用します

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

1. キャッシュのブレークダウン、ペネトレーション、アバランシェ

1. キャッシュの侵入

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

危険:

存在しないデータを使用して攻撃を実行すると、データベースに対する瞬間的な圧力が増大し、最終的にはデータベースの崩壊につながります。

解決する:

null の結果はキャッシュされ、短い有効期限が追加されます。

2. キャッシュなだれ

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

解決策: 元の有効期限にランダムな値 (1 ~ 5 分など) を追加すると、キャッシュされた各有効期限の繰り返し率が減少し、集合的な障害イベントが発生しにくくなります。

3. キャッシュの内訳

キャッシュの内訳:

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

解決する:

ロック

大量の同時実行により、1 つだけがチェックでき、他の人はチェック後にロックが解放され、他の人はロックを取得し、ホバーして最初にキャッシュをチェックすると、データベースにアクセスせずにデータが存在します。

2. 問題を解決する

まずキャッシュの侵入と雪崩を解決する

  1. private static final String CATALOG_JSON="CATALOG_JSON";
  2. @Override
  3. public Map<String, List<Catelog2Vo>> getCatalogJson() {
  4. /**
  5. * 空结果缓存:解决缓存穿透
  6. * 设置过期时间(加随机值) 缓存雪崩
  7. * 加锁 解决缓存击穿
  8. */
  9. Object result = redisTemplate.opsForValue().get(CATALOG_JSON);
  10. if(result!=null){
  11. return (Map<String, List<Catelog2Vo>>) result;
  12. }
  13. Map<String, List<Catelog2Vo>> map = getCatalogJsonFromDB();
  14. if (map==null){
  15. /**
  16. * 解决缓存穿透
  17. */
  18. map=new HashMap<>();
  19. }
  20. redisTemplate.opsForValue().set(CATALOG_JSON,map, Duration.ofDays(1));
  21. return map;
  22. }

キャッシュの内訳を解決する

1. ローカルロックを使用して解決する

springboot コンテナ オブジェクトはデフォルトでシングルトン モードになっているため、デュアル検出モードを使用する場合は、同じオブジェクトを同期してロックできます。

  1. public synchronized Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
  2. Object result = redisTemplate.opsForValue().get(CATALOG_JSON);
  3. if (result != null) {
  4. return (Map<String, List<Catelog2Vo>>) result;
  5. }
  6. //1.查出所有1级分类
  7. List<CategoryEntity> selectList = baseMapper.selectList(null);
  8. /**
  9. * 将数据库的多次查询变成一次
  10. */
  11. //2. 封装数据
  12. List<CategoryEntity> level1Category = selectList.stream().filter(s -> s.getParentCid().equals(0L)).collect(Collectors.toList());
  13. Map<String, List<Catelog2Vo>> map = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
  14. //1.每一个的一级分类,查到1级分类的所有二级分类
  15. List<CategoryEntity> categoryEntities = selectList.stream().filter(s -> s.getParentCid().equals(v.getCatId())).collect(Collectors.toList());
  16. List<Catelog2Vo> catelog2VoList = categoryEntities.stream().map(c -> {
  17. Catelog2Vo catelog2Vo = new Catelog2Vo();
  18. catelog2Vo.setId(c.getCatId().toString());
  19. catelog2Vo.setName(c.getName());
  20. catelog2Vo.setCatalog1Id(v.getCatId().toString());
  21. List<CategoryEntity> categoryEntities1 = selectList.stream().filter(s -> s.getParentCid().equals(c.getCatId())).collect(Collectors.toList());
  22. List<Catelog2Vo.Catelog3Vo> collect = categoryEntities1.stream().map(c3 -> {
  23. Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();
  24. catelog3Vo.setId(c3.getCatId().toString());
  25. catelog3Vo.setName(c3.getName());
  26. catelog3Vo.setCatalog2Id(c.getCatId().toString());
  27. return catelog3Vo;
  28. }).collect(Collectors.toList());
  29. catelog2Vo.setCatalog3List(collect);
  30. return catelog2Vo;
  31. }).collect(Collectors.toList());
  32. return catelog2VoList;
  33. }));
  34. return map;
  35. }
  1. public Map<String, List<Catelog2Vo>> getCatalogJson() {
  2. /**
  3. * 空结果缓存:解决缓存穿透
  4. * 设置过期时间(加随机值) 缓存雪崩
  5. * 加锁 解决缓存击穿
  6. */
  7. Object result = redisTemplate.opsForValue().get(CATALOG_JSON);
  8. if (result != null) {
  9. return (Map<String, List<Catelog2Vo>>) result;
  10. }
  11. Map<String, List<Catelog2Vo>> map = getCatalogJsonFromDB();
  12. if (map == null) {
  13. /**
  14. * 解决缓存穿透
  15. */
  16. map = new HashMap<>();
  17. }
  18. redisTemplate.opsForValue().set(CATALOG_JSON, map, Duration.ofDays(1));
  19. return map;
  20. }

上記のコードのロジックには依然として問題があり、同時に実行すると、最初のスレッドがデータベースのチェックを終了し、キャッシュに入れる前にロックが解放されます。その結果、2 番目のスレッドがキャッシュをチェックします。データがないため、データベースを再度チェックします。データベースをチェックするスレッドが 1 つだけであるという保証はありません。

正しいアプローチ

  1. public synchronized Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
  2. Object result = redisTemplate.opsForValue().get(CATALOG_JSON);
  3. if (result != null) {
  4. return (Map<String, List<Catelog2Vo>>) result;
  5. }
  6. //1.查出所有1级分类
  7. List<CategoryEntity> selectList = baseMapper.selectList(null);
  8. /**
  9. * 将数据库的多次查询变成一次
  10. */
  11. //2. 封装数据
  12. List<CategoryEntity> level1Category = selectList.stream().filter(s -> s.getParentCid().equals(0L)).collect(Collectors.toList());
  13. Map<String, List<Catelog2Vo>> map = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
  14. //1.每一个的一级分类,查到1级分类的所有二级分类
  15. List<CategoryEntity> categoryEntities = selectList.stream().filter(s -> s.getParentCid().equals(v.getCatId())).collect(Collectors.toList());
  16. List<Catelog2Vo> catelog2VoList = categoryEntities.stream().map(c -> {
  17. Catelog2Vo catelog2Vo = new Catelog2Vo();
  18. catelog2Vo.setId(c.getCatId().toString());
  19. catelog2Vo.setName(c.getName());
  20. catelog2Vo.setCatalog1Id(v.getCatId().toString());
  21. List<CategoryEntity> categoryEntities1 = selectList.stream().filter(s -> s.getParentCid().equals(c.getCatId())).collect(Collectors.toList());
  22. List<Catelog2Vo.Catelog3Vo> collect = categoryEntities1.stream().map(c3 -> {
  23. Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();
  24. catelog3Vo.setId(c3.getCatId().toString());
  25. catelog3Vo.setName(c3.getName());
  26. catelog3Vo.setCatalog2Id(c.getCatId().toString());
  27. return catelog3Vo;
  28. }).collect(Collectors.toList());
  29. catelog2Vo.setCatalog3List(collect);
  30. return catelog2Vo;
  31. }).collect(Collectors.toList());
  32. return catelog2VoList;
  33. }));
  34. if (map == null) {
  35. /**
  36. * 解决缓存穿透
  37. */
  38. map = new HashMap<>();
  39. }
  40. redisTemplate.opsForValue().set(CATALOG_JSON, map, Duration.ofDays(1));
  41. return map;
  42. }

キャッシュに保存する操作を同期コードブロックに入れる

  1. @Override
  2. public Map<String, List<Catelog2Vo>> getCatalogJson() {
  3. /**
  4. * 空结果缓存:解决缓存穿透
  5. * 设置过期时间(加随机值) 缓存雪崩
  6. * 加锁 解决缓存击穿
  7. */
  8. Object result = redisTemplate.opsForValue().get(CATALOG_JSON);
  9. if (result != null) {
  10. return (Map<String, List<Catelog2Vo>>) result;
  11. }
  12. Map<String, List<Catelog2Vo>> map = getCatalogJsonFromDB();
  13. return map;
  14. }

 

 

ローカルロックは現在のプロセスのみをロックできるため、分散ロックが必要です

3. 分散環境におけるローカルロックの問題

つまり、各ロックは現在のプロセスのみをロックできます。つまり、各サービスはデータベースをチェックします。