ตอน data model ซับซ้อน และ APIs hit 1 second response time 悲しみ fix ง่าย: :includes Preload associations ลด SQL calls ประหยัดเวลามาก
แต site ช้าอีก คิด caching responses ปัญหา: ถ้าอยาก response จาก 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
สูญเสีย :includes ทั้งหมด มีทั้งสองอย่างได้ไหม? Response เร็วสำหรับ cached objects และ load uncached objects เร็ว?
อะไรทำเยอะ คิดยาก แบ่งปัญหาเป็นชิ้นเล็ก และหา simple next step
ทำอะไรก่อน? ต้องรู้ว่าอbjects ไหนใน cache และไหนยังต้องหา
แยก Cached และ Uncached
สมมติ cache keys:
cache_keys = [:key_1, :key_2, :key_3]
รู้ยังไงว่าอันไหนใน cache?
ActiveSupport::Cache มี read_multi handy:
# เมื่อแค่ lawyer_1 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 คืน hash {key: value} สำหรับ key ที่เจอใน cache แตหา keys ที่ ไม่อยู่ ใน cache ยังไง? ทางตรง: Loop ทุก cache keys หาว่าอันไหนไม่อยู่ใน hash ที่ 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
ตอนนี้มีอะไร?
- Array ของ cache keys ทั้งหมดที่อยาก objects
- Hash
{key: value}สำหรับ objects ที่เจอใน cache - List ของ keys ที่ไม่อยู่ใน cache
ต้องการอะไรต่อ?
- Values สำหรับ keys ที่ไม่อยู่ใน cache ดึงแบบ bulk
นั่นคือ next step
Preload Uncached Values
เร็วๆ นี้ต้องหา object ด้วย cache key ทำให้ง่าย เปลี่ยน code:
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 track cache key และ object id ที่จะ fetch
กับ uncached keys:
uncached_keys # => [:lawyer_2, :lawyer_3]
และ cache_identifiers hash:
cache_identifiers # => {lawyer_1: 1, lawyer_2: 2, lawyer_3: 3}
Fetch, preload, serialize ทั้งหมดครั้งเดียว:
uncached_ids = uncached_keys.map { |key| cache_identifiers[key] } uncached_lawyers = Lawyer.where(id: uncached_ids) .includes([:address, :practice_areas, :awards, ...]) .map(&:as_json))
ตอนนี้มีอะไร?
- Array cache keys ทั้งหมด
- Hash
{key: value}objects ใน cache - List keys ไม่อยู่ใน cache
- Values ที่ดึงจาก database
ต้องการต่อ?
- Cache values ที่ดึงมา เพื่อครั้งหน้าไม่ต้องทำซ้ำ
- Final list ของทั้งหมด objects ไม่ว่าจะจาก cache หรือไม่
Cache Uncached Values
มี 2 lists: uncached keys และ uncached values แตเพื่อ cache ง่ายกว่าถ้ามี หนึ่ง list ของ [key, value] pairs ให้ value อยู่ข้าง key โอกาส用 zip method โปรด:
[1, 2, 3].zip(["a", "b", "c"]) # => [[1, "a"], [2, "b"], [3, "c"]]
用 zip cache fetched values:
uncached_keys.zip(uncached_lawyers).each do |key, value| Rails.cache.write(key, value) end
ตอนนี้มีอะไร?
- Array cache keys ทั้งหมด
- Hash
{key: value}objects ใน cache - Formerly-uncached values ที่เพิ่ง cache
ยังต้องการ?
- One big list ทุก objects ไม่ว่าจะ cache หรือไม่
รวมทั้งหมด
ตอนนี้มี ordered cache keys:
cache_keys = cache_identifiers.keys
Objects จาก cache:
cached_keys_with_values = Rails.cache.read_multi(cache_keys)
Objects ที่เพิ่งดึงจาก 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))
Loop สุดท้ายรวมทั้งหมด:
results = [] cache_keys.each do |key| results << cache_keys_with_values[key] || uncached_lawyers.shift end
คือ สำหรับแต่ละ cache key grab object จาก cache ถ้า key ไม่อยู่ใน cache เดิม grab object ถัดไปจาก database
จบ!
Full code:
cache_identifiers = {lawyer_1: 1, lawyer_2: 2, lawyer_3: 3} cache_keys = cache_identifiers.keys uncached_keys = [] # Fetch cached values cached_keys_with_values = Rails.cache.read_multi(cache_keys) # List 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 # Final result set from cached and uncached results = [] cache_keys.each do |key| results << cache_keys_with_values[key] || uncached_lawyers.shift end results
คุ้มไหม? อาจจะ มัน code เยอะ แตถ้า caching objects ที่มี associations เยอะ อาจประหยัด SQL calls โหลหลายสิบหรือร้อย และลดเวลา API responses มาก
ที่ Avvo pattern นี้ useful มาก JSON APIs เยอะ用มันคืน cached responses เร็วมาก
Pattern useful จนผมเขียน gem encapsulate เรียก bulk_cache_fetcher ถ้าเจอ caching big, complex data models ลอง用ดู!