Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

Rails Hidden Gems:การเพิ่มและลดแคช ActiveSupport

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 เพื่ออัปเดตตัวนับที่ใช้งานได้ทั่วโลก โดยมีโครงสร้างดังนี้

  1. อย่างแรก ฉันเพิ่มคลาสใหม่ใน /app/models เพื่อสรุปข้อเท็จจริงที่ว่าตัวนับอาศัยอยู่ในแคช นี่คือที่ที่การเข้าถึงค่าทั้งหมดจะไหลผ่าน ส่วนหนึ่งคือการสร้างคีย์แคชเฉพาะที่เกี่ยวข้องกับงาน
  2. งานจะสร้างอินสแตนซ์ของโมเดลนี้และอัปเดตโมเดลนี้ขณะที่ประมวลผลรายการ
  3. ตำแหน่งข้อมูล JSON แบบธรรมดาจะสร้างอินสแตนซ์ของโมเดลนี้เพื่อดึงค่าปัจจุบัน
  4. ส่วนหน้าสำรวจจุดสิ้นสุดนี้ทุกสองสามวินาทีเพื่ออัปเดต UI แน่นอน คุณสามารถทำให้นักเล่นตัวนี้คลั่งไคล้ ActionCable และผลักดันการอัปเดตได้

บทสรุป

พูดตรงๆนะ Rails.cache.increment ไม่ใช่เครื่องมือที่ฉันจะเข้าถึงบ่อยๆ เนื่องจากไม่บ่อยนักที่ฉันต้องการอัปเดตข้อมูลที่เก็บไว้ในแคช (ซึ่งโดยธรรมชาติแล้วค่อนข้างชั่วคราว) ดังที่กล่าวไว้ข้างต้น เวลาที่ฉันเข้าถึงโดยทั่วไปจะเกี่ยวข้องกับงานพื้นหลังเนื่องจากงานนั้นจัดเก็บข้อมูลใน Redis แล้ว (อย่างน้อยก็ในแอปส่วนใหญ่ที่ฉันทำงานอยู่) และโดยทั่วไปมักเป็นการชั่วคราวเช่นกัน ในกรณีเช่นนี้ เป็นเรื่องปกติที่จะจัดเก็บข้อมูลที่เกี่ยวข้อง (เช่น เปอร์เซ็นต์ที่เสร็จสมบูรณ์) ไว้ในที่เดียวกันโดยมีความคงอยู่ระยะสั้นในระดับใกล้เคียงกัน

เช่นเดียวกับทุกสิ่ง "นอกเส้นทางหลัก" คุณควรระวังที่จะแนะนำสิ่งนี้ใน codebase ของคุณ อย่างน้อย ขอแนะนำให้เพิ่มความคิดเห็นเพื่ออธิบายให้นักพัฒนาในอนาคตทราบว่าเหตุใดคุณจึงใช้วิธีนี้ซึ่งพวกเขาอาจไม่คุ้นเคย