Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Rails Hidden Gems:ActiveSupport 캐시 증가 및 감소

Rails는 특정 상황을 위해 내장된 편리한 도구가 많이 포함된 대규모 프레임워크입니다. 이 시리즈에서는 Rails의 대규모 코드베이스에 숨겨진 잘 알려지지 않은 도구를 살펴보겠습니다.

이 기사에서는 increment에 대해 설명합니다. 및 decrement Rails.cache의 메소드 .

Rails.cache 도우미

Rails.cache 애플리케이션의 캐시와 상호 작용하는 진입로입니다. 또한 내부에서 사용되는 실제 캐시 "저장소"에 관계없이 호출할 공통 API를 제공하는 추상화입니다. Rails는 기본적으로 다음을 지원합니다.

  • 파일 저장소
  • 메모리 저장소
  • MemCacheStore
  • 널스토어
  • RedisCacheStore

Rails.cache 검사 실행 중인 항목이 표시됩니다.

> Rails.cache
=> <#ActiveSupport::Cache::RedisCacheStore options={:namespace=>nil, ...

그것들을 모두 자세히 살펴보지는 않고 간단히 요약하자면 다음과 같습니다.

  • NullStore는 아무 것도 저장하지 않습니다. 이것을 다시 읽으면 항상 nil이 반환됩니다. . 이것은 새로운 Rails 앱의 기본값입니다.
  • FileStore는 캐시를 하드 드라이브에 파일로 저장하므로 Rails 서버를 다시 시작해도 유지됩니다.
  • MemoryStore는 캐시를 RAM에 보관하므로 Rails 서버를 중지하면 캐시도 삭제됩니다.
  • MemCacheStore 및 RedisCacheStore는 외부 프로그램(각각 MemCache 및 Redis)을 사용하여 캐시를 유지 관리합니다.

여러 가지 이유로 여기에서 처음 세 가지는 개발/테스트에 가장 자주 사용됩니다. 프로덕션에서는 Redis 또는 MemCache를 사용할 것입니다.

Rails.cache 이러한 서비스 간의 차이점을 추상화하면 코드를 변경하지 않고 다른 환경에서 다른 버전을 쉽게 실행할 수 있습니다. 예를 들어 NullStore를 사용할 수 있습니다. 개발 중, MemoryStore 테스트 중이며 RedisCacheStore 생산 중입니다.

캐시된 데이터

Rails의 Rails.cache.read를 통해 , Rails.cache.writeRails.cache.fetch , 캐시에서 임의의 데이터를 저장하고 검색하는 쉬운 방법이 있습니다. 이전 기사에서 이에 대해 더 자세히 다루었습니다. 이 기사에서 주목해야 할 중요한 점은 이러한 메소드에 내장된 스레드 안전성이 없다는 것입니다. 실행 횟수를 유지하기 위해 여러 스레드에서 캐시된 데이터를 업데이트한다고 가정해 보겠습니다. 경쟁 조건을 피하기 위해 일종의 잠금으로 읽기/쓰기 작업을 래핑해야 합니다. Redis 캐시 저장소를 사용하도록 설정했다고 가정하고 다음 예를 고려하십시오.

threads = []
# Set initial counter
Rails.cache.write(:test_counter, 0)

4.times do
  threads << Thread.new do
    100.times do 
      current_count = Rails.cache.read(:test_counter)
      current_count += 1
      Rails.cache.write(:test_counter, current_count)
    end
  end
end

threads.map(&:join)

puts Rails.cache.read(:test_counter)

여기에 4개의 스레드가 있으며 각 스레드는 캐시된 값을 100배 증가시킵니다. 결과는 400이어야 하지만 대부분의 경우 테스트 실행에서 269보다 훨씬 적습니다. 여기서 일어나고 있는 것은 경쟁 조건입니다. 이전 기사에서 이에 대해 더 자세히 다루었지만 빠른 요약으로 쓰레드는 모두 동일한 "공유" 데이터에서 작동하기 때문에 서로 동기화되지 않을 수 있습니다. 예를 들어, 한 스레드가 값을 읽은 다음 다른 스레드가 값을 인수하여 읽고 값을 증가시키고 저장합니다. 그런 다음 첫 번째 스레드가 지금은 오래된 값을 사용하여 재개됩니다.

이 문제를 해결하는 일반적인 방법은 코드를 상호 배타적인 잠금(또는 Mutex)으로 둘러싸서 한 번에 하나의 스레드만 잠금 내에서 코드를 실행할 수 있도록 하는 것입니다. 그러나 우리의 경우 Rails.cache에 이 시나리오를 처리하는 몇 가지 방법이 있습니다.

Rails 캐시 증가 및 감소

Rails.cache 개체에 increment가 모두 있습니다. 및 decrement 카운터 시나리오와 같이 캐시된 데이터에 직접 작동하는 방법:

  threads = []
  # Set initial counter
  Rails.cache.write(:test_counter, 0, raw: true)

  4.times do
    threads << Thread.new do
      100.times do 
        Rails.cache.increment(:test_counter)
        # repeating the increment just to highlight the thread safety
        Rails.cache.decrement(:test_counter)
        Rails.cache.increment(:test_counter)
      end
    end
  end

  threads.map(&:join)

  puts Rails.cache.read(:test_counter, raw: true)

increment를 사용하려면 및 decrement , 캐시 저장소에 '원시' 값이라고 알려야 합니다( raw: true를 통해 ). 값을 다시 읽을 때도 이 작업을 수행해야 합니다. 그렇지 않으면 오류가 발생합니다. 기본적으로 캐시에 이 값을 순수한 정수로 저장하여 증분/감소를 호출할 수 있도록 하고 있지만 expires_in을 계속 사용할 수 있습니다. 및 다른 캐시 플래그를 동시에.

여기서 핵심은 incrementdecrement 스레드로부터 안전하다는 것을 의미하는 원자적 작업(적어도 Redis 및 MemCache의 경우)을 사용합니다. 원자적 작업 중에 스레드가 일시 중지될 방법이 없습니다.

여기 예제에서는 사용하지 않았지만 두 메서드 모두 새 값을 반환한다는 점은 주목할 가치가 있습니다. 따라서 단순히 업데이트하는 것 이상으로 새 카운터 값을 사용해야 하는 경우 추가 read 없이도 사용할 수 있습니다. 전화하세요.

실제 애플리케이션

표면적으로는 이러한 incrementdecrement 메소드는 백그라운드 작업 처리 gem과 같은 것을 구현하거나 유지 관리하는 경우에만 관심이 있는 저수준 도우미 메소드처럼 보입니다. 하지만 일단 알고 나면 어디에 유용하게 사용할 수 있는지 놀랄 것입니다.

동시에 실행되는 중복 예약된 백그라운드 작업을 피하기 위해 프로덕션 애플리케이션에서 이것을 사용했습니다. 우리의 경우 검색 색인을 업데이트하고 버려진 카트를 표시하는 등의 다양한 예약 작업이 있습니다. 일반적으로 이것은 잘 작동합니다. 문제는 일부 작업(특히 검색 인덱스)이 많은 메모리를 소비한다는 것입니다. 두 개를 함께 실행하면 Heroku dyno의 한계를 초과하여 작업자가 죽게 될 것입니다. 이러한 작업이 몇 개 있기 때문에 재시도 금지로 표시하거나 고유한 작업을 강제하는 것만큼 간단하지 않습니다. 두 개의 서로 다른(따라서 고유한) 작업이 동시에 실행을 시도하여 작업자를 중단시킬 수 있습니다.

이를 방지하기 위해 현재 실행 중인 카운터를 유지하는 예약된 작업에 대한 기본 클래스를 만들었습니다. 개수가 너무 많으면 작업이 다시 대기열에 추가되고 기다립니다.

또 다른 예는 사용자가 기다려야 하는 동안 백그라운드 작업(또는 여러 작업)이 일부 처리를 수행하는 내 부 프로젝트에 있었습니다. 이것은 현재 진행 상황을 백그라운드 작업의 사용자에게 알리는 일반적인 문제를 나타냅니다. 이 문제를 해결할 수 있는 방법은 여러 가지가 있지만 실험으로 Rails.cache.increment를 사용해 보았습니다. 전 세계적으로 사용 가능한 카운터를 업데이트합니다. 구조는 다음과 같았습니다.

  1. 먼저 /app/models에 새 클래스를 추가했습니다. 카운터가 캐시에 있다는 사실을 추상화합니다. 이것은 값에 대한 모든 액세스가 통과하는 곳입니다. 그 중 일부는 작업과 관련된 고유한 캐시 키를 생성하는 것입니다.
  2. 그런 다음 작업은 이 모델의 인스턴스를 생성하고 항목이 처리될 때 업데이트합니다.
  3. 간단한 JSON 엔드포인트는 이 모델의 인스턴스를 생성하여 현재 값을 가져옵니다.
  4. 프론트 엔드는 UI를 업데이트하기 위해 몇 초마다 이 엔드포인트를 폴링합니다. 물론 ActionCable과 같은 기능으로 이 기능을 더 멋지게 만들고 업데이트를 푸시할 수도 있습니다.

결론

솔직히 Rails.cache.increment 캐시에 저장된 데이터를 업데이트하려는 경우가 많지 않기 때문에(이는 본질적으로 다소 임시적임) 자주 사용하는 도구가 아닙니다. 위에서 언급했듯이 작업이 이미 Redis에 데이터를 저장하고 있고(적어도 내가 작업한 대부분의 앱에서) 일반적으로 일시적이기 때문에 내가 도달하는 시간은 일반적으로 백그라운드 작업과 관련이 있습니다. 이러한 경우 유사한 수준의 단기 지속성을 가진 동일한 위치에 관련 데이터(예:완료율)를 저장하는 것이 자연스러워 보입니다.

"길을 벗어난" 모든 것과 마찬가지로 코드베이스에 이와 같은 것을 도입하는 것을 조심해야 합니다. 나는 적어도 미래의 개발자들이 익숙하지 않은 이 방법을 사용하는 이유를 설명하기 위해 몇 가지 설명을 추가하는 것이 좋습니다.