프로그래밍 기술 캐시 쓰기 방법(1)

伊谢尔伦
풀어 주다: 2023-03-01 09:30:01
원래의
1271명이 탐색했습니다.

소개

이 글은 주로 캐시 사용 경험과 일상적인 프로젝트에서 직면하는 문제에 대해 이야기합니다.

디렉토리

1: 기본 작성 방법

2: Cache avalanche

1: 글로벌 잠금, 인스턴스 잠금

2: 문자열 잠금

3: 캐시 침투

4: 캐시 사태에 대해 이야기해보자

다섯 번째: 요약

1: 기본 글쓰기

데모의 편의를 위해 Runtime.Cache를 캐시 컨테이너로 사용하고 간단한 작업 클래스를 정의합니다. 다음과 같습니다:

public class CacheHelper
   {
       public static object Get(string cacheKey)
       {
           return HttpRuntime.Cache[cacheKey];
       }
       public static void Add(string cacheKey, object obj, int cacheMinute)
       {
           HttpRuntime.Cache.Insert(cacheKey, obj, null, DateTime.Now.AddMinutes(cacheMinute),
               Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
       }
   }
로그인 후 복사

간단한 읽기:

public object GetMemberSigninDays1()
    {
        const int cacheTime = 5;
        const string cacheKey = "mushroomsir";
 
        var cacheValue = CacheHelper.Get(cacheKey);
        if (cacheValue != null)
            return cacheValue;
 
        cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
        CacheHelper.Add(cacheKey, cacheValue, cacheTime);
        return cacheValue;
    }
로그인 후 복사

프로젝트에는 이렇게 쓰는 방법이 많이 있습니다. 이렇게 작성해도 문제는 없지만 동시성 양이 늘어나면 문제가 발생합니다. 계속 읽기

둘: 캐시 눈사태

캐시 눈사태는 캐시 무효화(만료)로 인해 발생하며 새 캐시가 만료되지 않았습니다.

이 중간 기간 동안 모든 요청은 데이터베이스 쿼리로 이동하므로 데이터베이스 CPU와 메모리에 큰 부담이 가해지며 프런트 엔드 연결이 충분하지 않아 쿼리가 차단됩니다.

이 중간 시간은 SQL 쿼리의 1초에 전송 및 분석의 0.5초를 더해 그리 짧지 않습니다. 즉, 1.5초 내의 모든 사용자 쿼리는 데이터베이스에 직접 쿼리됩니다.

이 경우 우리가 가장 많이 생각하는 것은 locking과 queuing입니다.

1: 전역 잠금, 인스턴스 잠금

public static object obj1 = new object();
       public object GetMemberSigninDays2()
       {
           const int cacheTime = 5;
           const string cacheKey = "mushroomsir";
 
           var cacheValue = CacheHelper.Get(cacheKey);
 
           if (cacheValue != null)
               return cacheValue;
 
           //lock (obj1)         //全局锁
           //{
           //    cacheValue = CacheHelper.Get(cacheKey);
           //    if (cacheValue != null)
           //        return cacheValue;
           //    cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
           //    CacheHelper.Add(cacheKey, cacheValue, cacheTime);
           //}
           lock (this)
           {
               cacheValue = CacheHelper.Get(cacheKey);
               if (cacheValue != null)
                   return cacheValue;
 
               cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
               CacheHelper.Add(cacheKey, cacheValue, cacheTime);
           }
           return cacheValue;
       }
로그인 후 복사

첫 번째 유형: 잠금(obj1)은 충족할 수 있는 전역 잠금이지만 각 기능에 대해 obj를 선언해야 합니다. 그렇지 않으면 함수 A와 B가 모두 obj1을 잠그면 둘 중 하나가 필연적으로 차단됩니다.

두 번째 유형: 잠금(this)은 현재 인스턴스를 잠그며 다른 인스턴스에는 유효하지 않습니다. 싱글톤 모드를 사용하여 잠글 수 있습니다.

그러나 현재 인스턴스에서는 함수 A가 현재 인스턴스를 잠그고 현재 인스턴스를 잠그는 다른 함수도 읽기 및 쓰기가 차단됩니다. 바람직하지 않음

2: 문자열 잠금

객체 잠금이 불가능하므로 문자열의 특성을 이용하여 캐시 키를 직접 잠글 수 있습니다.

public object GetMemberSigninDays3()
       {
           const int cacheTime = 5;
           const string cacheKey = "mushroomsir";
 
           var cacheValue = CacheHelper.Get(cacheKey);
           if (cacheValue != null)
               return cacheValue;
           const string lockKey = cacheKey + "n(*≧▽≦*)n";
 
           //lock (cacheKey)
           //{
           //    cacheValue = CacheHelper.Get(cacheKey);
           //    if (cacheValue != null)
           //        return cacheValue;
           //    cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
           //    CacheHelper.Add(cacheKey, cacheValue, cacheTime);
           //}
           lock (lockKey)
           {
               cacheValue = CacheHelper.Get(cacheKey);
               if (cacheValue != null)
                   return cacheValue;
               cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
               CacheHelper.Add(cacheKey, cacheValue, cacheTime);
           }
           return cacheValue;
       }
로그인 후 복사

을 살펴보겠습니다. 첫 번째: 잠금(cacheName)은 문자열도 공유되고 이 문자열을 사용하는 다른 작업을 차단하므로 문제가 있습니다. 자세한 내용은 이전 블로그 게시물 멀티 스레딩의 C# 언어 잠금 시스템(1)을 참조하세요.

2015-01-04 13:36 업데이트: 문자열은 CLR(공용 언어 런타임)에 의해 유지되므로 전체 프로그램에서 특정 문자열의 인스턴스가 하나만 있음을 의미합니다. 그래서 우리는 두 번째 유형

을 사용합니다. 두 번째 유형인 잠금(lockKey)이면 충분합니다. 실제로 목적은 잠금의 최소 세분성과 전역 고유성을 보장하고 현재 캐시된 쿼리 동작만 잠그는 것입니다.

3: 캐시 침투

간단한 예: 일반적으로 사용자 검색 결과를 캐시합니다. 데이터베이스가 쿼리할 수 없으면 캐시되지 않습니다. 하지만 이 키워드를 자주 확인하게 되면 매번 데이터베이스를 직접 확인하게 됩니다.

이런 면에서 캐싱은 의미가 없으며, 이는 자주 제기되는 캐시 적중률 문제이기도 합니다.

public object GetMemberSigninDays4()
      {
          const int cacheTime = 5;
          const string cacheKey = "mushroomsir";
 
          var cacheValue = CacheHelper.Get(cacheKey);
          if (cacheValue != null)
              return cacheValue;
          const string lockKey = cacheKey + "n(*≧▽≦*)n";
 
          lock (lockKey)
          {
              cacheValue = CacheHelper.Get(cacheKey);
              if (cacheValue != null)
                  return cacheValue;
 
              cacheValue = null; //数据库查询不到,为空。
              //if (cacheValue2 == null)
              //{
              //    return null;  //一般为空,不做缓存
              //}
              if (cacheValue == null)
              {
                  cacheValue = string.Empty; //如果发现为空,我设置个默认值,也缓存起来。
              }
              CacheHelper.Add(cacheKey, cacheValue, cacheTime);
          }
          return cacheValue;
      }
로그인 후 복사

예제에서는 쿼리할 수 없는 결과도 캐시합니다. 이렇게 하면 쿼리가 비어 있을 때 캐시 침투를 피할 수 있습니다.

물론 별도의 캐시 영역을 설정하여 1차 제어 검증을 수행할 수도 있습니다. 일반 캐시와 구별하기 위해서입니다.

四:再谈缓存雪崩

额 不是用加锁排队方式就解决了吗?其实加锁排队只是为了减轻DB压力,并没有提高系统吞吐量。

在高并发下: 缓存重建期间,你是锁着的,1000个请求999个都在阻塞的。 用户体验不好,还浪费资源:阻塞的线程本可以处理后续请求的。

public object GetMemberSigninDays5()
        {
            const int cacheTime = 5;
            const string cacheKey = "mushroomsir";
 
            //缓存标记。
            const string cacheSign = cacheKey + "_Sign";
            var sign = CacheHelper.Get(cacheSign);
 
            //获取缓存值
            var cacheValue = CacheHelper.Get(cacheKey);
            if (sign != null)
                return cacheValue; //未过期,直接返回。
 
            lock (cacheSign)
            {
                sign = CacheHelper.Get(cacheSign);
                if (sign != null)
                    return cacheValue;
 
                CacheHelper.Add(cacheSign, "1", cacheTime);
                ThreadPool.QueueUserWorkItem((arg) =>
                {
                    cacheValue = "395"; //这里一般是 sql查询数据。 例:395 签到天数
                    CacheHelper.Add(cacheKey, cacheValue, cacheTime*2); //日期设缓存时间的2倍,用于脏读。
                });
            }
            return cacheValue;
        }
로그인 후 복사

代码中,我们多用个缓存标记key,双检锁校验。它设置为正常时间,过期后通知另外的线程去更新缓存数据。

而实际的缓存由于设置了2倍的时间,仍然可以能用脏数据给前端展现。

这样就能提高不少系统吞吐量了。

五:总结

补充下: 这里说的阻塞其他函数指的是,高并发下锁同一对象。

实际使用中,缓存层封装往往要复杂的多。 关于更新缓存,可以单开一个线程去专门跑这些,图方便就扔线程池吧。

具体使用场景,可根据实际用户量来平衡。


관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!