Khi mô hình dữ liệu của bạn trở nên phức tạp và các API của bạn đạt đến thời gian phản hồi 1 giây đáng tiếc đó, thường có một cách khắc phục dễ dàng::includes
. Khi bạn tải trước các liên kết mô hình của mình, bạn sẽ không thực hiện nhiều lệnh gọi SQL. Và điều đó có thể giúp bạn tiết kiệm rất nhiều thời gian.
Nhưng sau đó trang web của bạn lại chậm đi và bạn nghĩ đến việc lưu các phản hồi vào bộ nhớ đệm. Và bây giờ bạn có một vấn đề. Bởi vì nếu bạn muốn nhận phản hồi từ bộ nhớ 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
của mình . Bạn có thể có cả hai? Làm cách nào để bạn nhận được phản hồi nhanh cho các đối tượng được lưu trong bộ nhớ cache của mình và vẫn tải các đối tượng không có trong bộ nhớ cache một cách nhanh chóng?
Có rất nhiều việc phải làm, vì vậy việc suy nghĩ về nó thật khó khăn. Sẽ dễ dàng hơn khi bạn chia nhỏ vấn đề thành nhiều phần nhỏ hơn và thực hiện một bước đơn giản tiếp theo.
Vậy điều đầu tiên bạn có thể làm là gì? Để làm được nhiều việc, bạn cần biết những đối tượng nào nằm trong bộ nhớ cache của mình và những đối tượng nào bạn vẫn cần tìm.
Tách phần đã lưu trong bộ nhớ cache khỏi phần chưa được lưu trong bộ nhớ cache
Vì vậy, giả sử bạn có một loạt các khóa bộ nhớ cache:
cache_keys = [:key_1, :key_2, :key_3]
Làm thế nào bạn có thể biết cái nào trong số này nằm trong bộ nhớ cache?
ActiveSupport::Cache
có một phương thức tiện dụng được gọi là 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
trả về một hàm băm của {key: value}
cho mỗi khóa được tìm thấy trong bộ nhớ cache. Nhưng làm thế nào để bạn tìm thấy tất cả các chìa khóa không trong bộ nhớ cache? Bạn có thể thực hiện theo cách đơn giản: Lặp lại tất cả các khóa bộ nhớ đệm và tìm ra khóa nào không có trong hàm băm read_multi
lợi nhuận:
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ột mảng gồm tất cả các khóa bộ nhớ đệm mà bạn muốn có các đối tượng.
- Hàm băm của
{key: value}
các cặp cho từng đối tượng bạn tìm thấy trong bộ nhớ cache. - Danh sách các khóa không có trong bộ nhớ cache.
Và bạn cần gì tiếp theo?
- Các giá trị cho các khóa không có trong bộ nhớ cache. Tốt nhất là tìm nạp tất cả cùng một lúc.
Đó là bước tiếp theo của bạn.
Tải trước các giá trị chưa được lưu trữ
Ngay sau đó, bạn sẽ phải tìm một đối tượng bằng khóa bộ nhớ cache. Để làm cho mọi thứ dễ dàng hơn, bạn có thể thay đổi mã thành một cái 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
hiện theo dõi khóa bộ nhớ cache và id đối tượng để tìm nạp.
Bây giờ, với các khóa chưa được lưu của bạn:
uncached_keys # => [:lawyer_2, :lawyer_3]
Và cache_identifiers
của bạn băm:
cache_identifiers # => {lawyer_1: 1, lawyer_2: 2, lawyer_3: 3}
Bạn có thể tìm nạp, tải trước và tuần tự hóa tất cả các đối tượng đó cùng một 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ột mảng gồm tất cả các khóa bộ nhớ đệm mà bạn muốn các đối tượng bắt đầu.
- Hàm băm của
{key: value}
các cặp cho mỗi đối tượng được tìm thấy trong bộ nhớ cache. - Danh sách các khóa không có trong bộ nhớ đệm.
- Tất cả các giá trị không được tìm thấy trong bộ nhớ cache.
Và bạn cần gì tiếp theo?
- Để lưu vào bộ nhớ cache tất cả các giá trị bạn vừa tìm nạp, vì vậy, bạn không phải thực hiện toàn bộ quá trình này vào lần sau.
- Danh sách cuối cùng của tất cả các đối tượng của bạn, cho dù chúng đến từ bộ nhớ cache hay không.
Lưu vào bộ nhớ cache các giá trị chưa được lưu trữ
Bạn có hai danh sách:một danh sách các khóa chưa được lưu trữ và một danh sách các giá trị chưa được lưu trữ. Nhưng để lưu chúng vào bộ nhớ cache, sẽ dễ dàng hơn nếu bạn có một danh sách [key, value]
ghép nối, để value
của bạn nằm ngay bên cạnh key
của nó . Đây là lý do để sử dụng một trong những phương pháp 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ể lưu vào bộ nhớ cache các giá trị đã tìm nạp của mình một cách 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ột mảng gồm tất cả các khóa bộ nhớ đệm mà bạn muốn các đối tượng bắt đầu.
- Hàm băm của
{key: value}
các cặp cho mỗi đối tượng được tìm thấy trong bộ nhớ cache. - Danh sách các giá trị trước đây chưa được lưu vào bộ nhớ cache mà bạn vừa mới lưu vào bộ nhớ cache.
Và bạn vẫn cần gì?
- Một danh sách lớn gồm tất cả các đối tượng của bạn, cho dù chúng đến từ bộ nhớ đệm hay không.
Mang tất cả lại với nhau
Bây giờ, bạn có một danh sách có thứ tự các khóa bộ nhớ cache:
cache_keys = cache_identifiers.keys
Danh sách các đối tượng bạn đã tìm nạp từ bộ nhớ cache:
cached_keys_with_values = Rails.cache.read_multi(cache_keys)
Và danh sách các đối tượng mà bạn vừa lấy từ cơ sở dữ liệu:
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 cùng để kết hợp mọi thứ lại với nhau:
results = []
cache_keys.each do |key|
results << cache_keys_with_values[key] || uncached_lawyers.shift
end
Có nghĩa là, đối với mỗi khóa bộ nhớ cache, bạn lấy đối tượng bạn tìm thấy trong bộ nhớ cache cho khóa đó. Nếu khóa đó ban đầu không có trong bộ nhớ cache, bạn lấy đối tượng tiếp theo mà bạn lấy từ cơ sở dữ liệu.
Sau đó, bạn đã hoàn tất!
Đây là toàn bộ mọi thứ 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 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
Nó có đáng không? Có lẽ. Đó là rất nhiều mã. Nhưng nếu bạn đang lưu vào bộ nhớ đệm các đối tượng có nhiều liên kết, điều đó có thể giúp bạn tiết kiệm hàng chục hoặc hàng trăm lệnh gọi SQL. Và điều đó có thể làm mất đi rất nhiều thời gian đối với các phản hồi API của bạn.
Tại Avvo, mẫu này cực kỳ hữu ích:rất nhiều API JSON của chúng tôi sử dụng nó để trả về các phản hồi được lưu trong bộ nhớ cache một cách nhanh chóng đến khó tin.
Mẫu này hữu ích đến mức tôi đã viết một viên đá quý để đóng gói nó có tên là số lượng lớn_cache_fetcher. Vì vậy, nếu bạn từng thấy mình đang cố gắng lưu vào bộ nhớ cache các mô hình dữ liệu lớn, phức tạp, hãy thử!