기술나눔

캐시 - 캐시는 2를 사용합니다.

2024-07-12

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

1. 캐시 고장, 침투, 눈사태

1. 캐시 침투

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

위험:

존재하지 않는 데이터를 이용해 공격을 하게 되면 데이터베이스에 순간적인 부담이 가중되어 결국 붕괴로 이어질 수 있습니다.

해결하다:

null 결과가 캐시되고 짧은 만료 시간이 추가됩니다.

2. 캐시 사태

캐시 사태(Cache Avalanche): 캐시 사태는 캐시를 설정할 때 키가 동일한 만료 시간을 채택하여 특정 순간에 캐시가 무효화되고 모든 요청이 DB로 전달되며 DB가 순간적인 압력과 사태를 겪는다는 것을 의미합니다.

해결 방법: 원래 만료 시간에 무작위 값(예: 1~5분)을 추가하면 캐시된 각 만료 시간의 반복률이 줄어들어 집합적인 오류 이벤트가 발생하기 어렵게 됩니다.

3. 캐시 분석

캐시 분석:

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

해결하다:

잠그다

동시성이 많아 한 사람만 확인하고 다른 사람은 기다리면 확인 후 잠금이 해제되고 다른 사람은 잠금을 획득하고 마우스를 올려 캐시를 먼저 확인하면 db로 이동하지 않고 데이터가 있게 됩니다.

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. }

위 코드의 논리에는 여전히 문제가 있습니다. 동시에 실행하면 첫 번째 스레드가 데이터베이스 검사를 완료하고 이를 캐시에 넣기 전에 잠금을 해제하게 됩니다. 데이터가 없으므로 데이터베이스를 다시 확인합니다. 데이터베이스를 확인할 스레드가 하나만 있다는 보장은 없습니다.

올바른 접근 방식

  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. 분산 환경의 로컬 잠금 문제

즉, 각 잠금은 현재 프로세스만 잠글 수 있습니다. 즉, 각 서비스는 데이터베이스를 확인합니다.