데이터 모델이 복잡해지고 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라는 보석을 캡슐화하기 위해 작성했습니다. 따라서 크고 복잡한 데이터 모델을 캐싱하려는 경우 시도해 보십시오!