Rails เป็นเฟรมเวิร์กขนาดใหญ่พร้อมเครื่องมือที่มีประโยชน์มากมายในตัวสำหรับสถานการณ์เฉพาะ ในชุดนี้ เราจะมาดูเครื่องมือที่ไม่ค่อยมีใครรู้จักซึ่งซ่อนอยู่ในฐานโค้ดขนาดใหญ่ของ Rails
ในบทความนี้ เราจะอธิบายเกี่ยวกับ increment
และ decrement
เมธอดใน Rails.cache
.
ตัวช่วย Rails.cache
Rails.cache
เป็นทางเข้าเพื่อโต้ตอบกับแคชในแอปพลิเคชันของคุณ นอกจากนี้ยังเป็นนามธรรมโดยให้ API ทั่วไปเพื่อเรียกโดยไม่คำนึงถึงแคช "สโตร์" ที่แท้จริงที่ใช้ภายใต้ประทุน นอกกรอบ Rails รองรับสิ่งต่อไปนี้:
- FileStore
- MemoryStore
- MemCacheStore
- NullStore
- RedisCacheStore
กำลังตรวจสอบ Rails.cache
จะแสดงอันที่คุณกำลังเรียกใช้:
> Rails.cache
=> <#ActiveSupport::Cache::RedisCacheStore options={:namespace=>nil, ...
เราจะไม่ดูรายละเอียดทั้งหมด แต่เป็นการสรุปโดยย่อ:
- NullStore ไม่ได้เก็บข้อมูลใดๆ การอ่านกลับมาจากนี้จะส่งคืน
nil
. เสมอ . นี่เป็นค่าเริ่มต้นสำหรับแอป Rails ใหม่ - FileStore จัดเก็บแคชเป็นไฟล์บนฮาร์ดไดรฟ์ ดังนั้นมันจึงยังคงอยู่แม้ว่าคุณจะรีสตาร์ทเซิร์ฟเวอร์ Rails ของคุณ
- MemoryStore เก็บแคชไว้ใน RAM ดังนั้นหากคุณหยุดเซิร์ฟเวอร์ Rails คุณจะล้างแคชด้วย
- MemCacheStore และ RedisCacheStore ใช้โปรแกรมภายนอก (MemCache และ Redis ตามลำดับ) เพื่อรักษาแคช
ด้วยเหตุผลหลายประการ สามข้อแรกที่นี่มักถูกใช้เพื่อการพัฒนา/ทดสอบ ในการผลิต คุณอาจใช้ Redis หรือ MemCache
เพราะ Rails.cache
แยกแยะความแตกต่างระหว่างบริการเหล่านี้ ง่ายต่อการเรียกใช้เวอร์ชันต่างๆ ในสภาพแวดล้อมที่แตกต่างกันโดยไม่ต้องเปลี่ยนรหัส ตัวอย่างเช่น คุณสามารถใช้ NullStore
กำลังพัฒนา MemoryStore
ในการทดสอบและ RedisCacheStore
ในการผลิต
ข้อมูลแคช
ผ่าน Rails.cache.read
ของ Rails , Rails.cache.write
และ Rails.cache.fetch
เรามีวิธีง่ายๆ ในการจัดเก็บและดึงข้อมูลใดๆ จากแคชตามอำเภอใจ บทความก่อนหน้านี้ครอบคลุมรายละเอียดเพิ่มเติมเหล่านี้ สิ่งสำคัญที่ควรทราบสำหรับบทความนี้คือวิธีการเหล่านี้ไม่มีความปลอดภัยของเธรดในตัว สมมติว่าเรากำลังอัปเดตข้อมูลที่แคชไว้จากหลายเธรดเพื่อให้นับทำงานต่อไป เราจะต้องปิดการดำเนินการอ่าน/เขียนในการล็อกบางประเภทเพื่อหลีกเลี่ยงสภาวะการแข่งขัน พิจารณาตัวอย่างนี้ สมมติว่าเราได้ตั้งค่าให้ใช้ที่เก็บแคช Redis:
threads = []
# Set initial counter
Rails.cache.write(:test_counter, 0)
4.times do
threads << Thread.new do
100.times do
current_count = Rails.cache.read(:test_counter)
current_count += 1
Rails.cache.write(:test_counter, current_count)
end
end
end
threads.map(&:join)
puts Rails.cache.read(:test_counter)
ที่นี่เรามีสี่เธรด แต่ละเธรดเพิ่มค่าแคชของเราหนึ่งร้อยครั้ง ผลลัพธ์ควรเป็น 400 แต่โดยส่วนใหญ่แล้วจะน้อยกว่ามาก - 269 ในการทดสอบของฉัน สิ่งที่เกิดขึ้นที่นี่คือสภาพการแข่งขัน ฉันได้กล่าวถึงรายละเอียดเหล่านี้ในบทความที่แล้ว แต่โดยสรุปแล้ว เนื่องจากเธรดทั้งหมดทำงานบนข้อมูล "ที่ใช้ร่วมกัน" เดียวกัน จึงไม่ซิงค์กัน ตัวอย่างเช่น เธรดหนึ่งอาจอ่านค่า จากนั้นเธรดอื่นจะเข้ามาแทนที่และอ่านค่าด้วย เพิ่มขึ้น และจัดเก็บไว้ จากนั้นเธรดแรกจะกลับมาทำงานต่อโดยใช้ค่าที่ล้าสมัย
วิธีทั่วไปในการแก้ปัญหานี้คือ ล้อมโค้ดด้วยการล็อกที่ไม่เกิดร่วมกัน (หรือ Mutex) เพื่อให้มีเพียงเธรดเดียวเท่านั้นที่สามารถรันโค้ดภายในล็อกในแต่ละครั้ง ในกรณีของเรา Rails.cache มีวิธีการบางอย่างในการจัดการสถานการณ์นี้
Rails Cache การเพิ่มและลด
Rails.cache
วัตถุมีทั้ง increment
และ decrement
วิธีการสำหรับดำเนินการโดยตรงกับข้อมูลที่แคชไว้ เช่น สถานการณ์จำลองของเรา:
threads = []
# Set initial counter
Rails.cache.write(:test_counter, 0, raw: true)
4.times do
threads << Thread.new do
100.times do
Rails.cache.increment(:test_counter)
# repeating the increment just to highlight the thread safety
Rails.cache.decrement(:test_counter)
Rails.cache.increment(:test_counter)
end
end
end
threads.map(&:join)
puts Rails.cache.read(:test_counter, raw: true)
ในการใช้ increment
และ decrement
เราต้องบอกที่เก็บแคชว่าเป็นค่า 'ดิบ' (ผ่าน raw: true
). คุณต้องทำเช่นนี้เมื่ออ่านค่ากลับเช่นกัน มิฉะนั้น คุณจะได้รับข้อผิดพลาด โดยพื้นฐานแล้ว เรากำลังบอกแคชว่าเราต้องการเก็บค่านี้ไว้เป็นจำนวนเต็มเปลือย เพื่อให้เราสามารถเรียกการเพิ่ม/ลดค่าได้ แต่คุณยังสามารถใช้ expires_in
และแฟล็กแคชอื่นๆ พร้อมกัน
สิ่งสำคัญที่นี่คือ increment
และ decrement
ใช้การดำเนินการปรมาณู (อย่างน้อยสำหรับ Redis และ MemCache) ซึ่งหมายความว่าปลอดภัยสำหรับเธรด ไม่มีทางที่เธรดจะหยุดชั่วคราวระหว่างการทำงานของอะตอม
เป็นที่น่าสังเกตว่า แม้ว่าฉันจะไม่ได้ใช้มันในตัวอย่างของฉันที่นี่ แต่ทั้งสองวิธีก็คืนค่าใหม่เช่นกัน ดังนั้น หากคุณต้องการใช้ค่าตัวนับใหม่นอกเหนือจากการอัปเดต คุณสามารถทำได้โดยไม่ต้อง read
เพิ่มเติม โทร.
แอปพลิเคชันในโลกแห่งความเป็นจริง
ในทางกลับกัน increment
.เหล่านี้ และ decrement
วิธีการดูเหมือนเป็นวิธีการช่วยเหลือระดับต่ำ ชนิดที่คุณอาจสนใจเฉพาะในกรณีที่คุณกำลังใช้งานหรือบำรุงรักษาบางอย่างเช่นอัญมณีการประมวลผลงานพื้นหลัง เมื่อคุณรู้เกี่ยวกับมันแล้ว คุณอาจแปลกใจว่ามันมีประโยชน์ตรงไหน
ฉันได้ใช้สิ่งเหล่านี้ในแอปพลิเคชันที่ใช้งานจริงเพื่อหลีกเลี่ยงการทำงานพื้นหลังตามกำหนดเวลาที่ซ้ำกันซึ่งทำงานพร้อมกัน ในกรณีของเรา เรามีงานตามกำหนดเวลาต่างๆ เพื่ออัปเดตดัชนีการค้นหา ทำเครื่องหมายรถเข็นที่ถูกละทิ้ง และอื่นๆ โดยทั่วไปแล้ววิธีนี้ใช้ได้ดี สิ่งที่จับได้คืองานบางงาน (โดยเฉพาะดัชนีการค้นหา) ใช้หน่วยความจำมาก - เพียงพอที่หากทั้งสองทำงานร่วมกัน มันจะเกินขีดจำกัดของ Heroku dyno ของเรา และคนงานจะถูกฆ่าตาย เนื่องจากเรามีงานเหล่านี้อยู่สองสามงาน จึงไม่ง่ายเหมือนการทำเครื่องหมายว่าไม่มีการลองใหม่หรือบังคับงานที่ไม่ซ้ำ งานสองงานที่แตกต่างกัน (และไม่เหมือนใคร) อาจพยายามทำงานพร้อมกันและทำให้คนงานล้มลง
เพื่อป้องกันสิ่งนี้ เราได้สร้างคลาสพื้นฐานสำหรับงานตามกำหนดการที่คอยนับจำนวนที่กำลังทำงานอยู่ หากการนับสูงเกินไป งานก็จะเข้าคิวใหม่และรอ
อีกตัวอย่างหนึ่งคือในโปรเจ็กต์ด้านข้างของฉันที่งานพื้นหลัง (หรืองานหลายงาน) ดำเนินการบางอย่างในขณะที่ผู้ใช้ต้องรอ สิ่งนี้ทำให้เกิดปัญหาทั่วไปในการสื่อสารความคืบหน้าปัจจุบันให้กับผู้ใช้งานพื้นหลัง แม้ว่าจะมีวิธีแก้ปัญหามากมาย แต่ในการทดลอง ฉันได้ลองใช้ Rails.cache.increment
เพื่ออัปเดตตัวนับที่ใช้งานได้ทั่วโลก โดยมีโครงสร้างดังนี้
- อย่างแรก ฉันเพิ่มคลาสใหม่ใน
/app/models
เพื่อสรุปข้อเท็จจริงที่ว่าตัวนับอาศัยอยู่ในแคช นี่คือที่ที่การเข้าถึงค่าทั้งหมดจะไหลผ่าน ส่วนหนึ่งคือการสร้างคีย์แคชเฉพาะที่เกี่ยวข้องกับงาน - งานจะสร้างอินสแตนซ์ของโมเดลนี้และอัปเดตโมเดลนี้ขณะที่ประมวลผลรายการ
- ตำแหน่งข้อมูล JSON แบบธรรมดาจะสร้างอินสแตนซ์ของโมเดลนี้เพื่อดึงค่าปัจจุบัน
- ส่วนหน้าสำรวจจุดสิ้นสุดนี้ทุกสองสามวินาทีเพื่ออัปเดต UI แน่นอน คุณสามารถทำให้นักเล่นตัวนี้คลั่งไคล้ ActionCable และผลักดันการอัปเดตได้
บทสรุป
พูดตรงๆนะ Rails.cache.increment
ไม่ใช่เครื่องมือที่ฉันจะเข้าถึงบ่อยๆ เนื่องจากไม่บ่อยนักที่ฉันต้องการอัปเดตข้อมูลที่เก็บไว้ในแคช (ซึ่งโดยธรรมชาติแล้วค่อนข้างชั่วคราว) ดังที่กล่าวไว้ข้างต้น เวลาที่ฉันเข้าถึงโดยทั่วไปจะเกี่ยวข้องกับงานพื้นหลังเนื่องจากงานนั้นจัดเก็บข้อมูลใน Redis แล้ว (อย่างน้อยก็ในแอปส่วนใหญ่ที่ฉันทำงานอยู่) และโดยทั่วไปมักเป็นการชั่วคราวเช่นกัน ในกรณีเช่นนี้ เป็นเรื่องปกติที่จะจัดเก็บข้อมูลที่เกี่ยวข้อง (เช่น เปอร์เซ็นต์ที่เสร็จสมบูรณ์) ไว้ในที่เดียวกันโดยมีความคงอยู่ระยะสั้นในระดับใกล้เคียงกัน
เช่นเดียวกับทุกสิ่ง "นอกเส้นทางหลัก" คุณควรระวังที่จะแนะนำสิ่งนี้ใน codebase ของคุณ อย่างน้อย ขอแนะนำให้เพิ่มความคิดเห็นเพื่ออธิบายให้นักพัฒนาในอนาคตทราบว่าเหตุใดคุณจึงใช้วิธีนี้ซึ่งพวกเขาอาจไม่คุ้นเคย