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

복잡한 데이터 모델을 캐시하는 더 빠른 방법

데이터 모델이 복잡해지고 API가 1초의 슬픈 응답 시간에 도달하면 일반적으로 다음과 같이 쉽게 해결할 수 있습니다. :includes . 모델의 연결을 미리 로드하면 SQL 호출이 많지 않습니다. 그러면 많은 시간을 절약할 수 있습니다.

그러나 사이트 속도가 다시 느려지고 응답 캐싱에 대해 생각하게 됩니다. 이제 문제가 생겼습니다. 캐시에서 응답을 받고 싶다면:

results = {lawyer_1: 1, lawyer_2: 2, lawyer_3: 3}
cached_objects = Rails.cache.fetch_multi(results.keys) do |key|
  Lawyer.find(results[key]).as_json
end

이제 모든 :includes가 손실되었습니다. . 둘 다 가질 수 있습니까? 캐시된 개체에 대한 빠른 응답을 얻고 캐시에 없는 개체를 빠르게 로드하는 방법은 무엇입니까?

할 일이 너무 많아서 생각하기가 어렵습니다. 문제를 더 작은 조각으로 나누고 간단한 다음 단계를 생각해내면 더 쉽습니다.

그래서 가장 먼저 할 수 있는 일은 무엇입니까? 많은 작업을 수행하려면 캐시에 있는 개체와 아직 찾아야 하는 개체를 알아야 합니다.

캐시된 것과 캐시되지 않은 것을 분리

따라서 캐시 키가 많다고 가정해 보겠습니다.

cache_keys = [:key_1, :key_2, :key_3]

이들 중 어느 것이 캐시에 있는지 어떻게 알 수 있습니까?

ActiveSupport::Cache read_multi라는 편리한 방법이 있습니다. :

# When only lawyer_1 is cached

cache_keys = [:lawyer_1, :lawyer_2, :lawyer_3]
Rails.cache.read_multi(cache_keys) # => {:lawyer_1 => {"id": 1, "name": "Bob the Lawyer"} }

read_multi {key: value}의 해시를 반환합니다. 캐시에서 찾은 각 키에 대해 하지만 아닌 모든 키를 어떻게 찾을 수 있습니까? 캐시에? 다음과 같이 간단한 방법으로 할 수 있습니다. 모든 캐시 키를 반복하여 read_multi 해시에 없는 키를 찾습니다. 반환:

cache_keys = [:lawyer_1, :lawyer_2, :lawyer_3]
uncached_keys = []

cached_keys_with_values = Rails.cache.read_multi(cache_keys)

cache_keys.each do |key|
  uncached_keys << key unless cached_keys_with_values.has_key?(key)
end

그래서, 당신은 지금 무엇을 가지고 있습니까?

  • 객체가 원하는 모든 캐시 키의 배열입니다.
  • {key: value}의 해시 캐시에서 찾은 각 개체에 대한 쌍입니다.
  • 캐시에 없는 키 목록입니다.

다음으로 필요한 것은 무엇입니까?

  • 가치 캐시에 없는 키에 대해 한 번에 모두 가져오는 것이 좋습니다.

이것이 다음 단계입니다.

캐시되지 않은 값 미리 로드

곧 캐시 키를 사용하여 개체를 찾아야 합니다. 작업을 더 쉽게 하기 위해 코드를 다음과 같이 변경할 수 있습니다.

cache_identifiers = {lawyer_1: 1, lawyer_2: 2, lawyer_3: 3}
cache_keys = cache_identifiers.keys
uncached_keys = []

cached_keys_with_values = Rails.cache.read_multi(cache_keys)

cache_keys.each do |key|
  uncached_keys << key unless cached_keys_with_values.has_key?(key)
end

그래서 cache_identifiers 이제 캐시 키 을 추적합니다. 가져올 개체 ID입니다.

이제 캐시되지 않은 키로:

uncached_keys # => [:lawyer_2, :lawyer_3]

그리고 cache_identifiers 해시:

cache_identifiers # => {lawyer_1: 1, lawyer_2: 2, lawyer_3: 3}

이러한 모든 개체를 한 번에 가져오고, 미리 로드하고, 직렬화할 수 있습니다.

uncached_ids = uncached_keys.map { |key| cache_identifiers[key] }
uncached_lawyers = Lawyer.where(id: uncached_ids)
                         .includes([:address, :practice_areas, :awards, ...])
                         .map(&:as_json))

그래서 지금 무엇을 가지고 있습니까?

  • 시작할 개체를 원하는 모든 캐시 키의 배열입니다.
  • {key: value}의 해시 캐시에서 찾은 각 개체에 대한 쌍입니다.
  • 캐시에 없는 키 목록입니다.
  • 캐시에서 찾을 수 없는 모든 값

다음으로 필요한 것은 무엇입니까?

  • 방금 가져온 모든 값을 캐시하기 위해 다음 번에 이 전체 프로세스를 거치지 않아도 됩니다.
  • 캐시에서 가져왔는지 여부에 관계없이 모든 개체의 최종 목록입니다.

캐시되지 않은 값 캐시

두 개의 목록이 있습니다. 하나는 캐시되지 않은 키 목록이고 다른 하나는 캐시되지 않은 값 목록입니다. 하지만 캐시하려면 하나 [key, value] 목록 쌍을 이루어 value key 바로 옆에 있습니다. . 이것은 내가 가장 좋아하는 방법 중 하나인 zip을 사용하기 위한 변명입니다. :

[1, 2, 3].zip(["a", "b", "c"]) # => [[1, "a"], [2, "b"], [3, "c"]]

zip 사용 , 가져온 값을 쉽게 캐시할 수 있습니다.

uncached_keys.zip(uncached_lawyers).each do |key, value|
  Rails.cache.write(key, value)
end

당신은 지금 무엇을 가지고 있습니까?

  • 시작할 개체를 원하는 모든 캐시 키의 배열입니다.
  • {key: value}의 해시 캐시에서 찾은 각 개체에 대한 쌍입니다.
  • 방금 캐시한 이전에 캐시되지 않은 값 목록입니다.

그리고 여전히 필요한 것은 무엇입니까?

  • 캐시에서 가져왔는지 여부에 관계없이 모든 개체에 대한 하나의 큰 목록입니다.

모든 것을 하나로 통합

이제 캐시 키의 정렬된 목록이 있습니다.

cache_keys = cache_identifiers.keys

캐시에서 가져온 개체 목록:

cached_keys_with_values = Rails.cache.read_multi(cache_keys)

그리고 데이터베이스에서 방금 가져온 개체 목록:

uncached_ids = uncached_keys.map { |key| cache_identifiers[key] }
uncached_lawyers = Lawyer.where(id: uncached_ids)
                         .includes([:address, :practice_areas, :awards, ...])
                         .map(&:as_json))

이제 모든 것을 합치는 마지막 루프만 있으면 됩니다.

results = []
cache_keys.each do |key|
  results << cache_keys_with_values[key] || uncached_lawyers.shift
end

즉, 각 캐시 키에 대해 해당 키에 대해 캐시에서 찾은 개체를 가져옵니다. 해당 키가 원래 캐시에 없었다면 데이터베이스에서 가져온 다음 개체를 가져옵니다.

그러면 완료됩니다!

전체 모습은 다음과 같습니다.

cache_identifiers = {lawyer_1: 1, lawyer_2: 2, lawyer_3: 3}
cache_keys = cache_identifiers.keys
uncached_keys = []

# Fetch the cached values from the cache
cached_keys_with_values = Rails.cache.read_multi(cache_keys)

# Create the list of keys that weren't in the cache
cache_keys.each do |key|
  uncached_keys << key unless cached_keys_with_values.has_key?(key)
end

# Fetch all the uncached values, in bulk
uncached_ids = uncached_keys.map { |key| cache_identifiers[key] }
uncached_lawyers = Lawyer.where(id: uncached_ids)
                         .includes([:address, :practice_areas, :awards, ...])
                         .map(&:as_json))

# Write the uncached values back to the cache
uncached_keys.zip(uncached_lawyers).each do |key, value|
  Rails.cache.write(key, value)
end

# Create our final result set from the cached and uncached values
results = []
cache_keys.each do |key|
  results << cache_keys_with_values[key] || uncached_lawyers.shift
end
results

그럴만한 가치가 있었나요? 아마도. 많은 코드입니다. 하지만 연관이 많은 개체를 캐싱하는 경우 수십 또는 수백 개의 SQL 호출을 줄일 수 있습니다. 그러면 API 응답 시간을 엄청나게 단축할 수 있습니다.

Avvo에서 이 패턴은 매우 유용했습니다. 많은 JSON API가 이 패턴을 사용하여 캐시된 응답을 엄청나게 빠르게 반환합니다.

패턴이 너무 유용해서 bulk_cache_fetcher라는 보석을 캡슐화하기 위해 작성했습니다. 따라서 크고 복잡한 데이터 모델을 캐싱하려는 경우 시도해 보십시오!