Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Tối ưu mô hình dữ liệu phức tạp: Chiến lược caching nhanh chóng

Khi mô hình dữ liệu của bạn phức tạp, và API chạm thời gian phản hồi 1 giây buồn bã đó, thường có cách sửa dễ dàng: :includes. Khi bạn preload association của model, bạn sẽ không làm nhiều lời gọi SQL. Và điều đó có thể tiết kiệm ton thời gian.

Nhưng sau đó trang của bạn chậm lại, và bạn nghĩ về cache phản hồi. Và bây giờ bạn có vấn đề. Bởi vì nếu bạn muốn lấy phản hồi từ cache:

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

Bây giờ bạn đã mất tất cả :includes. Bạn có thể có cả hai không? Làm sao bạn có phản hồi nhanh cho đối tượng cached, và vẫn load đối tượng không trong cache, nhanh?

Có nhiều việc làm, nên suy nghĩ về nó khó. Dễ hơn khi bạn chia vấn đề thành mảnh nhỏ, và tìm bước tiếp theo đơn giản.

Vậy điều đầu tiên bạn có thể làm là gì? Để làm nhiều thứ, bạn cần biết đối tượng nào trong cache, và cái nào bạn vẫn cần tìm.

Tách cached khỏi uncached

Vì vậy, giả sử bạn có đống cache key:

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

Làm sao bạn biết cái nào trong cache?

ActiveSupport::Cache có phương thức tiện lợi gọi là read_multi:

# Khi chỉ lawyer_1 được 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 trả về hash {key: value} cho mỗi key tìm thấy trong cache. Nhưng làm sao bạn tìm tất cả key không trong cache? Bạn có thể làm cách trực tiếp: Lặp qua tất cả cache key và tìm cái nào không trong hash read_multi trả về:

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

Vậy bạn có gì bây giờ?

  • Mảng tất cả cache key bạn muốn đối tượng.
  • Hash cặp {key: value} cho mỗi đối tượng tìm thấy trong cache.
  • Danh sách key không trong cache.

Và bạn cần gì tiếp?

  • Giá trị cho key không trong cache. Thích hợp fetch tất cả cùng lúc.

Đó là bước tiếp theo.

Preload giá trị uncached

Sớm, bạn sẽ phải tìm đối tượng dùng cache key. Để làm dễ hơn, bạn có thể đổi mã thành gì đó như:

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

Vì vậy cache_identifiers giờ theo dõi cache key và object id để fetch.

Bây giờ, với uncached keys:

uncached_keys # => [:lawyer_2, :lawyer_3]

Và hash cache_identifiers:

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

Bạn có thể fetch, preload, và serialize tất cả đối tượng đó cùng lúc:

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

Vậy bạn có gì bây giờ?

  • Mảng tất cả cache key bạn muốn đối tượng ban đầu.
  • Hash cặp {key: value} cho mỗi đối tượng tìm thấy trong cache.
  • Danh sách key không trong cache.
  • Tất cả giá trị không tìm thấy trong cache.

Và bạn cần gì tiếp?

  • Cache tất cả giá trị bạn vừa fetch, vậy bạn không phải qua quá trình này lần sau.
  • Danh sách cuối cùng tất cả đối tượng, dù chúng từ cache hay không.

Cache giá trị uncached

Bạn có hai danh sách: một uncached key và một uncached value. Nhưng để cache chúng, dễ hơn nếu bạn có một danh sách cặp [key, value], để value ngay cạnh key. Đây là cớ dùng một phương thức yêu thích của tôi, zip:

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

Với zip, bạn có thể cache giá trị fetch dễ dàng:

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

Bạn có gì bây giờ?

  • Mảng tất cả cache key bạn muốn đối tượng ban đầu.
  • Hash cặp {key: value} cho mỗi đối tượng tìm thấy trong cache.
  • Danh sách giá trị trước đây uncached mà bạn vừa cache.

Và bạn vẫn cần gì?

  • Một danh sách lớn tất cả đối tượng, dù từ cache hay không.

Gộp lại với nhau

Bây giờ, bạn có danh sách có thứ tự cache key:

cache_keys = cache_identifiers.keys

Danh sách đối tượng bạn fetch từ cache:

cached_keys_with_values = Rails.cache.read_multi(cache_keys)

Và danh sách đối tượng bạn vừa lấy từ database:

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

Bây giờ bạn chỉ cần một vòng lặp cuối để gộp mọi thứ:

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

Đó là, cho mỗi cache key, bạn lấy đối tượng tìm thấy trong cache cho key đó. Nếu key đó không ban đầu trong cache, bạn lấy đối tượng tiếp theo bạn kéo từ database.

Sau đó, bạn xong!

Đây là toàn bộ trông như thế nào:

cache_identifiers = {lawyer_1: 1, lawyer_2: 2, lawyer_3: 3} cache_keys = cache_identifiers.keys uncached_keys = [] # Fetch cached values from cache cached_keys_with_values = Rails.cache.read_multi(cache_keys) # Create list of keys not in cache cache_keys.each do |key| uncached_keys << key unless cached_keys_with_values.has_key?(key) end # Fetch all 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 uncached values back to cache uncached_keys.zip(uncached_lawyers).each do |key, value| Rails.cache.write(key, value) end # Create final result set from cached and uncached values results = [] cache_keys.each do |key| results << cache_keys_with_values[key] || uncached_lawyers.shift end results

Có đáng giá không? Có lẽ. Đó là nhiều mã. Nhưng nếu bạn cache đối tượng với nhiều association, nó có thể tiết kiệm chục hoặc hàng trăm lời gọi SQL. Và đó có thể cắt giảm ton thời gian từ phản hồi API.

Tại Avvo, pattern này cực kỳ hữu ích: nhiều JSON API của chúng tôi dùng nó để trả phản hồi cached cực nhanh.

Pattern đã hữu ích đến mức tôi viết gem gói gọn nó gọi là bulk_cache_fetcher. Vì vậy nếu bạn bao giờ thấy bản thân cố cache mô hình dữ liệu lớn, phức tạp, hãy thử nó!