내 연락처 정보
우편메소피아@프로톤메일.com
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
존재하지 않아야 하는 데이터를 쿼리하는 것을 의미합니다. 캐시가 히트되지 않았기 때문에 데이터베이스가 쿼리되지만 데이터베이스에 해당 레코드가 없습니다. 이 쿼리의 null 값을 캐시에 쓰지 않았습니다. 존재하지 않는 데이터에 대한 모든 요청에서 쿼리하려면 스토리지 계층으로 이동해야 하며, 이는 캐싱의 의미를 잃습니다.
위험:
존재하지 않는 데이터를 이용해 공격을 하게 되면 데이터베이스에 순간적인 부담이 가중되어 결국 붕괴로 이어질 수 있습니다.
해결하다:
null 결과가 캐시되고 짧은 만료 시간이 추가됩니다.
캐시 사태(Cache Avalanche): 캐시 사태는 캐시를 설정할 때 키가 동일한 만료 시간을 채택하여 특정 순간에 캐시가 무효화되고 모든 요청이 DB로 전달되며 DB가 순간적인 압력과 사태를 겪는다는 것을 의미합니다.
해결 방법: 원래 만료 시간에 무작위 값(예: 1~5분)을 추가하면 캐시된 각 만료 시간의 반복률이 줄어들어 집합적인 오류 이벤트가 발생하기 어렵게 됩니다.
캐시 분석:
해결하다:
잠그다
동시성이 많아 한 사람만 확인하고 다른 사람은 기다리면 확인 후 잠금이 해제되고 다른 사람은 잠금을 획득하고 마우스를 올려 캐시를 먼저 확인하면 db로 이동하지 않고 데이터가 있게 됩니다.
캐시 침투 및 눈사태를 먼저 해결하세요.
- private static final String CATALOG_JSON="CATALOG_JSON";
- @Override
- public Map<String, List<Catelog2Vo>> getCatalogJson() {
-
- /**
- * 空结果缓存:解决缓存穿透
- * 设置过期时间(加随机值) 缓存雪崩
- * 加锁 解决缓存击穿
- */
- Object result = redisTemplate.opsForValue().get(CATALOG_JSON);
- if(result!=null){
- return (Map<String, List<Catelog2Vo>>) result;
- }
-
- Map<String, List<Catelog2Vo>> map = getCatalogJsonFromDB();
- if (map==null){
- /**
- * 解决缓存穿透
- */
- map=new HashMap<>();
- }
- redisTemplate.opsForValue().set(CATALOG_JSON,map, Duration.ofDays(1));
- return map;
- }
springboot 컨테이너 객체는 기본적으로 싱글톤 모드이므로 동일한 객체를 동기화하고 잠글 수 있으며 이중 감지 모드를 사용하면 동시에 실행할 수 있습니다.
- public synchronized Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
-
- Object result = redisTemplate.opsForValue().get(CATALOG_JSON);
- if (result != null) {
- return (Map<String, List<Catelog2Vo>>) result;
- }
-
-
- //1.查出所有1级分类
- List<CategoryEntity> selectList = baseMapper.selectList(null);
- /**
- * 将数据库的多次查询变成一次
- */
-
- //2. 封装数据
- List<CategoryEntity> level1Category = selectList.stream().filter(s -> s.getParentCid().equals(0L)).collect(Collectors.toList());
- Map<String, List<Catelog2Vo>> map = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
- //1.每一个的一级分类,查到1级分类的所有二级分类
- List<CategoryEntity> categoryEntities = selectList.stream().filter(s -> s.getParentCid().equals(v.getCatId())).collect(Collectors.toList());
-
- List<Catelog2Vo> catelog2VoList = categoryEntities.stream().map(c -> {
- Catelog2Vo catelog2Vo = new Catelog2Vo();
- catelog2Vo.setId(c.getCatId().toString());
- catelog2Vo.setName(c.getName());
- catelog2Vo.setCatalog1Id(v.getCatId().toString());
-
- List<CategoryEntity> categoryEntities1 = selectList.stream().filter(s -> s.getParentCid().equals(c.getCatId())).collect(Collectors.toList());
- List<Catelog2Vo.Catelog3Vo> collect = categoryEntities1.stream().map(c3 -> {
- Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();
- catelog3Vo.setId(c3.getCatId().toString());
- catelog3Vo.setName(c3.getName());
- catelog3Vo.setCatalog2Id(c.getCatId().toString());
- return catelog3Vo;
- }).collect(Collectors.toList());
-
- catelog2Vo.setCatalog3List(collect);
-
- return catelog2Vo;
- }).collect(Collectors.toList());
-
-
- return catelog2VoList;
- }));
- return map;
- }
- public Map<String, List<Catelog2Vo>> getCatalogJson() {
-
- /**
- * 空结果缓存:解决缓存穿透
- * 设置过期时间(加随机值) 缓存雪崩
- * 加锁 解决缓存击穿
- */
- Object result = redisTemplate.opsForValue().get(CATALOG_JSON);
- if (result != null) {
- return (Map<String, List<Catelog2Vo>>) result;
- }
-
- Map<String, List<Catelog2Vo>> map = getCatalogJsonFromDB();
- if (map == null) {
- /**
- * 解决缓存穿透
- */
- map = new HashMap<>();
- }
- redisTemplate.opsForValue().set(CATALOG_JSON, map, Duration.ofDays(1));
- return map;
- }
위 코드의 논리에는 여전히 문제가 있습니다. 동시에 실행하면 첫 번째 스레드가 데이터베이스 검사를 완료하고 이를 캐시에 넣기 전에 잠금을 해제하게 됩니다. 데이터가 없으므로 데이터베이스를 다시 확인합니다. 데이터베이스를 확인할 스레드가 하나만 있다는 보장은 없습니다.
올바른 접근 방식
- public synchronized Map<String, List<Catelog2Vo>> getCatalogJsonFromDB() {
-
- Object result = redisTemplate.opsForValue().get(CATALOG_JSON);
- if (result != null) {
- return (Map<String, List<Catelog2Vo>>) result;
- }
-
-
- //1.查出所有1级分类
- List<CategoryEntity> selectList = baseMapper.selectList(null);
- /**
- * 将数据库的多次查询变成一次
- */
-
- //2. 封装数据
- List<CategoryEntity> level1Category = selectList.stream().filter(s -> s.getParentCid().equals(0L)).collect(Collectors.toList());
- Map<String, List<Catelog2Vo>> map = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
- //1.每一个的一级分类,查到1级分类的所有二级分类
- List<CategoryEntity> categoryEntities = selectList.stream().filter(s -> s.getParentCid().equals(v.getCatId())).collect(Collectors.toList());
-
- List<Catelog2Vo> catelog2VoList = categoryEntities.stream().map(c -> {
- Catelog2Vo catelog2Vo = new Catelog2Vo();
- catelog2Vo.setId(c.getCatId().toString());
- catelog2Vo.setName(c.getName());
- catelog2Vo.setCatalog1Id(v.getCatId().toString());
-
- List<CategoryEntity> categoryEntities1 = selectList.stream().filter(s -> s.getParentCid().equals(c.getCatId())).collect(Collectors.toList());
- List<Catelog2Vo.Catelog3Vo> collect = categoryEntities1.stream().map(c3 -> {
- Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo();
- catelog3Vo.setId(c3.getCatId().toString());
- catelog3Vo.setName(c3.getName());
- catelog3Vo.setCatalog2Id(c.getCatId().toString());
- return catelog3Vo;
- }).collect(Collectors.toList());
-
- catelog2Vo.setCatalog3List(collect);
-
- return catelog2Vo;
- }).collect(Collectors.toList());
-
-
- return catelog2VoList;
- }));
- if (map == null) {
- /**
- * 解决缓存穿透
- */
- map = new HashMap<>();
- }
- redisTemplate.opsForValue().set(CATALOG_JSON, map, Duration.ofDays(1));
- return map;
- }
캐시에 저장하는 연산을 동기화 코드 블록에 넣습니다.
- @Override
- public Map<String, List<Catelog2Vo>> getCatalogJson() {
-
- /**
- * 空结果缓存:解决缓存穿透
- * 设置过期时间(加随机值) 缓存雪崩
- * 加锁 解决缓存击穿
- */
- Object result = redisTemplate.opsForValue().get(CATALOG_JSON);
- if (result != null) {
- return (Map<String, List<Catelog2Vo>>) result;
- }
-
- Map<String, List<Catelog2Vo>> map = getCatalogJsonFromDB();
-
- return map;
- }
로컬 잠금은 현재 프로세스만 잠글 수 있으므로 분산 잠금이 필요합니다.
즉, 각 잠금은 현재 프로세스만 잠글 수 있습니다. 즉, 각 서비스는 데이터베이스를 확인합니다.