เป็นไปได้ว่า หากคุณกำลังเขียนโค้ด Ruby แสดงว่าคุณกำลังใช้Sidekiq เพื่อจัดการการประมวลผลเบื้องหลัง หากคุณมาจาก ActiveJob
หรือพื้นหลังอื่นๆ โปรดอดใจรอ เคล็ดลับบางส่วนที่กล่าวถึงก็สามารถนำมาใช้ที่นั่นได้เช่นกัน
ผู้คนใช้งานพื้นหลัง (Sidekiq) สำหรับกรณีต่างๆ ข้อมูลสำคัญบางประการ การส่งจดหมายต้อนรับบางส่วนไปยังผู้ใช้ และกำหนดการซิงค์ข้อมูลบางส่วน ไม่ว่ากรณีของคุณจะเป็นอย่างไร ในที่สุดคุณอาจพบกับข้อกำหนดเพื่อหลีกเลี่ยงงานซ้ำซ้อน โดยงานซ้ำซ้อน ฉันนึกภาพสองงานที่ทำสิ่งเดียวกันทุกประการ มาเจาะลึกเรื่องนั้นกันดีกว่า
เหตุใดจึงต้องยกเลิกการทำซ้ำงาน
ลองนึกภาพสถานการณ์ที่งานของคุณมีลักษณะดังนี้:
class BookSalesWorker
include Sidekiq::Worker
def perform(book_id)
crunch_some_numbers(book_id)
upload_to_s3
end
...
end
BookSalesWorker
ทำสิ่งเดียวกันเสมอ — ค้นหาฐานข้อมูลสำหรับหนังสือตาม book_id
และดึงข้อมูลการขายล่าสุดเพื่อคำนวณตัวเลขบางส่วน จากนั้นจะอัปโหลดไปยังบริการจัดเก็บข้อมูล โปรดทราบว่าทุกครั้งที่ขายหนังสือบนเว็บไซต์ คุณจะต้องจัดคิวงานนี้
แล้วถ้าคุณได้ยอดขาย 100 ครั้งในคราวเดียวล่ะ? คุณจะมีงานเหล่านี้ 100 งานที่ทำสิ่งเดียวกัน บางทีคุณอาจจะดีกับที่ คุณไม่สนใจ S3writes มากนัก และคิวของคุณก็ไม่แออัด ดังนั้นคุณจึงสามารถจัดการกับโหลดได้ แต่ "ขยายหรือไม่"™️
ไม่แน่นอน หากคุณเริ่มได้รับยอดขายเพิ่มขึ้นสำหรับหนังสือมากขึ้น คิวของคุณก็จะเต็มไปด้วยงานที่ไม่จำเป็นอย่างรวดเร็ว หากคุณมี 100 งานที่ทำสิ่งเดียวกันสำหรับหนังสือเล่มเดียว และคุณมีหนังสือ 10 เล่มขายไม่เท่ากัน ตอนนี้คุณอยู่ในคิวงาน 1000 ตำแหน่ง ซึ่งในความเป็นจริง คุณสามารถมี 10 งานสำหรับหนังสือแต่ละเล่ม
ตอนนี้ มาดูสองตัวเลือกเกี่ยวกับวิธีป้องกันไม่ให้งานซ้ำซ้อนคิวของคุณ
1. DIY วิธี
หากคุณไม่ได้ชื่นชอบการพึ่งพาภายนอกและตรรกะที่ซับซ้อน คุณสามารถดำเนินการและเพิ่มโซลูชันแบบกำหนดเองบางอย่างลงในฐานโค้ดของคุณได้ ฉันสร้างตัวอย่าง repo เพื่อลองใช้ตัวอย่างของเราโดยตรง จะมีลิงค์ไปยังตัวอย่างในแต่ละแนวทาง
1.1 วิธีธงเดียว
คุณสามารถเพิ่มหนึ่งแฟล็กที่ตัดสินใจว่าจะเข้าคิวงานหรือไม่ หนึ่งอาจเพิ่ม sales_enqueued_at
ในตารางหนังสือของพวกเขาและคงไว้ซึ่งสิ่งนั้น ตัวอย่างเช่น:
module BookSalesService
def schedule_with_one_flag(book)
# Check if the job was enqueued more than 10 minutes ago
if book.sales_enqueued_at < 10.minutes.ago
book.update(sales_enqueued_at: Time.current)
BookSalesWorker.perform_async(book.id)
end
end
end
นั่นหมายความว่าจะไม่มีการจัดคิวงานใหม่จนกว่าจะผ่านไป 10 นาทีนับจากเวลาที่งานสุดท้ายเข้าคิว หลังจากผ่านไป 10 นาที เราจะอัปเดตsales_enqueued_at
และจัดคิวงานใหม่
สิ่งที่คุณทำได้อีกอย่างคือตั้งค่าสถานะหนึ่งตัวที่เป็นบูลีน เช่นcrunching_sales
. คุณตั้งค่า crunching_sales
เป็นจริงก่อนที่จะเข้าคิวงานแรก จากนั้นคุณตั้งค่าเป็นเท็จเมื่องานเสร็จสมบูรณ์ งานอื่นๆ ทั้งหมดที่พยายามจัดกำหนดการจะถูกปฏิเสธจนถึง crunching_sales
เป็นเท็จ
คุณสามารถลองใช้แนวทางนี้ใน repoI ตัวอย่างที่สร้างขึ้น
1.2 วิธีสองธง
หากการ "ล็อก" งานจากการถูกเข้าคิวเป็นเวลา 10 นาทีฟังดูน่ากลัวเกินไป แต่คุณยังคงใช้แฟล็กเพิ่มเติมในโค้ดได้ ข้อเสนอแนะต่อไปอาจสนใจคุณ
คุณสามารถเพิ่มการตั้งค่าสถานะอื่นให้กับ sales_enqueued_at
. ที่มีอยู่ — sales_calculated_at
.จากนั้นโค้ดของเราจะมีลักษณะดังนี้:
module BookSalesService
def schedule_with_two_flags(book)
# Check if sales are being calculated right now
if book.sales_enqueued_at <= book.sales_calculated_at
book.update(sales_enqueued_at: Time.current)
BookSalesWorker.perform_async(book.id)
end
end
end
class BookSalesWorker
include Sidekiq::Worker
def perform(book_id)
crunch_some_numbers(book_id)
upload_to_s3
# New adition
book.update(sales_calculated_at: Time.current)
end
...
end
หากต้องการทดลองใช้ โปรดดูคำแนะนำในตัวอย่างที่เก็บ
ตอนนี้เราควบคุมช่วงเวลาระหว่างเวลาที่งานเข้าคิวและเสร็จสิ้น ในช่วงเวลานั้นไม่สามารถจัดคิวงานได้ ในขณะที่งานกำลังทำงาน sales_enqueued_at
จะมีขนาดใหญ่กว่า sales_calculated_at
. เมื่องานเสร็จสิ้น sales_calculated_at
จะใหญ่กว่า (ใหม่กว่า) กว่า sales_enqueued_at
และจะมีการเข้าคิวงานใหม่
การใช้สองแฟล็กอาจมีความน่าสนใจ คุณจึงสามารถแสดงเวลาล่าสุดที่ตัวเลขยอดขายได้รับการอัปเดตใน UI จากนั้นผู้ใช้ที่อ่านข้อมูลเหล่านี้สามารถทราบได้ว่าข้อมูลล่าสุดเป็นอย่างไร สถานการณ์แบบวิน-วิน
สรุปธง
มันอาจจะน่าดึงดูดใจที่จะสร้างวิธีแก้ปัญหาเช่นนี้ในยามจำเป็น แต่สำหรับฉัน พวกเขาดูงุ่มง่ามเล็กน้อย และพวกเขาเพิ่มค่าใช้จ่ายบางอย่าง ฉันขอแนะนำให้ใช้ตัวเลือกนี้หากกรณีการใช้งานของคุณเรียบง่าย แต่ทันทีที่พิสูจน์ได้ว่าซับซ้อนหรือไม่เพียงพอ เราขอแนะนำให้คุณลองใช้ตัวเลือกอื่นๆ
ข้อเสียเปรียบอย่างมากกับวิธีการตั้งค่าสถานะคือคุณจะสูญเสียงานทั้งหมดที่พยายามเข้าคิวในช่วง 10 นาทีนั้น ข้อดีอย่างมากคือคุณไม่ได้ทำให้เกิดการพึ่งพา และจะช่วยลดจำนวนงานใน Quespretty ได้อย่างรวดเร็ว
1.3 ลัดเลาะตามคิว
อีกวิธีหนึ่งที่คุณสามารถทำได้คือการสร้างกลไกการล็อคแบบกำหนดเองเพื่อป้องกันไม่ให้งานเดียวกันเข้าคิว เราจะตรวจสอบคิว Sidekiq ที่คุณสนใจและดูว่ามีงาน (คนงาน) แล้วหรือยัง รหัสจะมีลักษณะดังนี้:
module BookSalesService
def schedule_unique_across_queue(book)
queue = Sidekiq::Queue.new('default')
queue.each do |job|
return if job.klass == BookSalesWorker.to_s &&
job.args == [book.id]
end
BookSalesWorker.perform_async(book.id)
end
end
class BookSalesWorker
include Sidekiq::Worker
def perform(book_id)
crunch_some_numbers(book_id)
upload_to_s3
end
...
end
ในตัวอย่างข้างต้น เรากำลังตรวจสอบว่า'default'
คิวมีงานชื่อคลาส BookSalesWorker
. เรากำลังตรวจสอบว่าอาร์กิวเมนต์งานตรงกับรหัสหนังสือหรือไม่ ถ้า BookSalesWorker
งานที่มีรหัสหนังสือเดียวกันอยู่ในคิว เราจะคืนก่อนกำหนดและไม่กำหนดเวลาอื่น
โปรดทราบว่าบางงานอาจได้รับการกำหนดเวลาหากคุณกำหนดเวลางานเร็วเกินไปเนื่องจากคิวว่าง สิ่งที่แน่นอนเกิดขึ้นกับฉันเมื่อทำการทดสอบในพื้นที่ด้วย:
100.times { BookSalesService.schedule_unique_across_queue(book) }
คุณสามารถลองใช้ได้ในตัวอย่าง repo
ข้อดีของวิธีนี้คือคุณสามารถข้ามคิวทั้งหมดเพื่อค้นหางานที่มีอยู่ได้หากต้องการ ข้อเสียคือ คุณยังสามารถมีงานที่ซ้ำกันได้หากคิวของคุณว่างเปล่าและคุณกำหนดเวลางานจำนวนมากในคราวเดียว นอกจากนี้ คุณยังอาจสำรวจผ่านงานทั้งหมดในคิวก่อนที่จะจัดกำหนดการงาน ซึ่งอาจมีค่าใช้จ่ายสูงขึ้นอยู่กับขนาดของ คิวของคุณ
2. กำลังอัปเกรดเป็น Sidekiq Enterprise
หากคุณหรือองค์กรของคุณมีเงินอยู่บ้าง คุณสามารถอัปเกรดเป็น Sidekiq เวอร์ชัน Enterprise ได้ เริ่มต้นที่ $179 ต่อเดือน และมีคุณสมบัติเจ๋งๆ ที่ช่วยให้คุณหลีกเลี่ยงงานซ้ำซ้อนได้ ขออภัย ฉันไม่มี SidekiqEnterprise แต่ฉันเชื่อว่าเอกสารประกอบนั้นเพียงพอ คุณสามารถมีงานที่ไม่ซ้ำ (ไม่ซ้ำกัน) ได้อย่างง่ายดายด้วยรหัสต่อไปนี้:
class BookSalesWorker
include Sidekiq::Worker
sidekiq_options unique_for: 10.minutes
def perform(book_id)
crunch_some_numbers(book_id)
upload_to_s3
end
...
end
และนั่นแหล่ะ คุณมีการใช้งานที่คล้ายกับที่เราอธิบายไว้ในส่วน 'แนวทางเดียว' งานจะไม่ซ้ำกันเป็นเวลา 10 นาที หมายความว่างานอื่นที่มีข้อโต้แย้งเดียวกันไม่สามารถกำหนดเวลาได้ในช่วงเวลานั้น
หนึ่งซับสวยดีฮะ? ถ้าคุณมี Enterprise Sidekiq และเพิ่งทราบเกี่ยวกับคุณลักษณะนี้ ฉันดีใจจริงๆ ที่ได้ช่วย พวกเราส่วนใหญ่จะไม่ใช้มัน ดังนั้นเรามาเข้าสู่แนวทางแก้ไขปัญหาถัดไปกัน
3. sidekiq-unique-jobs To The Rescue
ใช่ ฉันรู้ว่าเรากำลังจะพูดถึงอัญมณี และใช่ มันมีไฟล์ Lua อยู่ด้วยซึ่งอาจทำให้บางคนหยุดทำงาน แต่อดทนกับฉัน มันเป็นข้อตกลงที่น่ารักจริงๆ ที่คุณได้รับกับมัน sidekiq-unique-jobgem มาพร้อมกับตัวเลือกการล็อคและการกำหนดค่าอื่นๆ มากมาย — อาจมากกว่าที่คุณต้องการ
หากต้องการเริ่มต้นอย่างรวดเร็ว ให้ใส่ sidekiq-unique-jobs
อัญมณีลงใน Gemfile ของคุณ ทำbundle
และกำหนดค่าคนงานของคุณตามที่แสดง:
class UniqueBookSalesWorker
include Sidekiq::Worker
sidekiq_options lock: :until_executed,
on_conflict: :reject
def perform(book_id)
book = Book.find(book_id)
logger.info "I am a Sidekiq Book Sales worker - I started"
sleep 2
logger.info "I am a Sidekiq Book Sales worker - I finished"
book.update(sales_calculated_at: Time.current)
book.update(crunching_sales: false)
end
end
มีตัวเลือกมากมาย แต่ฉันตัดสินใจที่จะลดความซับซ้อนและใช้ตัวเลือกนี้:
sidekiq_options lock: :until_executed, on_conflict: :reject
lock: :until_executed
จะล็อก UniqueBookSalesWorker
ตัวแรก jobunt จนกว่ามันจะดำเนินการ ด้วย on_conflict: :reject
เรากำลังบอกว่าเราต้องการงานอื่นๆ ทั้งหมดที่พยายามดำเนินการให้ถูกปฏิเสธในคิวที่มีปัญหา สิ่งที่ทำให้ที่นี่มีความคล้ายคลึงกับสิ่งที่เราทำในตัวอย่าง DIY ในหัวข้อด้านบน
การปรับปรุงเล็กน้อยจากตัวอย่าง DIY เหล่านั้นคือการที่เราออกจากระบบว่าเกิดอะไรขึ้น เพื่อให้เข้าใจถึงรูปลักษณ์ ให้ลองทำดังนี้:
5.times { UniqueBookSalesWorker.perform_async(Book.last.id) }
มีเพียงหนึ่งงานเท่านั้นที่จะดำเนินการอย่างสมบูรณ์ และอีกสี่งานจะถูกส่งไปที่คิวที่ไม่ทำงาน ซึ่งคุณสามารถลองใหม่ได้ วิธีการนี้แตกต่างจากตัวอย่างของเราที่งานที่ซ้ำกันถูกละเลย
มีตัวเลือกมากมายให้เลือกเมื่อต้องการแก้ไขการล็อกและข้อขัดแย้ง เราขอแนะนำให้คุณศึกษาเอกสารประกอบของ gem สำหรับกรณีการใช้งานเฉพาะของคุณ
ข้อมูลเชิงลึกที่ยอดเยี่ยม
สิ่งที่ยอดเยี่ยมเกี่ยวกับอัญมณีนี้คือ คุณสามารถดูล็อคและประวัติของสิ่งที่ค้างอยู่ในคิวของคุณ สิ่งที่คุณต้องทำคือเพิ่มบรรทัดต่อไปนี้ใน config/routes.rb
ของคุณ :
# config/routes.rb
require 'sidekiq_unique_jobs/web'
Rails.application.routes.draw do
mount Sidekiq::Web, at: '/sidekiq'
end
มันจะรวมไคลเอนต์ Sidekiq ดั้งเดิมไว้ด้วย แต่จะให้หน้าเพิ่มเติมสองหน้าแก่คุณ หน้าหนึ่งสำหรับการล็อคงานและอีกหน้าสำหรับบันทึกการเปลี่ยนแปลง นี่คือลักษณะ:
ขอให้สังเกตว่าเรามีหน้าใหม่สองหน้า "ล็อก" และ "บันทึกการเปลี่ยนแปลง" ฟีเจอร์เด็ดมาก
คุณสามารถลองทั้งหมดนี้ได้ในตัวอย่างโครงการที่มีการติดตั้งอัญมณีและพร้อมใช้งาน
ทำไมต้องลัวะ
ก่อนอื่น ฉันไม่ใช่ผู้เขียนอัญมณี ดังนั้นฉันจึงสมมติว่าที่นี่ ครั้งแรกที่ฉันเห็นอัญมณีฉันสงสัยว่าทำไมต้องใช้ Lua ใน Ruby gem? อาจดูแปลกในตอนแรก แต่ Redis รองรับการรันสคริปต์ Lua ฉันเดาว่าผู้เขียนอัญมณีมีความคิดนี้และต้องการใช้ตรรกะที่คล่องแคล่วมากขึ้นใน Lua
หากคุณดูไฟล์ theLua ใน repo ของ gem ไฟล์เหล่านั้นไม่ได้ซับซ้อนขนาดนั้น สคริปต์ Lua ทั้งหมดถูกเรียกในภายหลังจากรหัส Ruby ใน SidekiqUniqueJobs::Script::Caller
ที่นี่โปรดดูที่ซอร์สโค้ด เป็นที่น่าสนใจที่จะอ่านและทำความเข้าใจว่าสิ่งต่างๆ ทำงานอย่างไร
อัญมณีทางเลือก
หากคุณใช้ ActiveJob
อย่างกว้างขวาง คุณสามารถลองใช้ active-job-uniqueness
อัญมณีที่นี่ แนวคิดคล้ายกัน แต่แทนที่จะใช้สคริปต์ Lua แบบกำหนดเอง มันใช้ [Redlock] เพื่อล็อกรายการใน Redis
หากต้องการมีงานที่ไม่เหมือนใครโดยใช้อัญมณีนี้ คุณสามารถจินตนาการถึงงานแบบนี้:
class BookSalesJob < ActiveJob::Base
unique :until_executed
def perform
...
end
end
ไวยากรณ์นั้นละเอียดน้อยกว่า แต่คล้ายกับ sidekiq-unique-jobs
มาก อัญมณี. อาจแก้ปัญหากรณีของคุณได้หากคุณพึ่งพา ActiveJob
.
ความคิดสุดท้าย
ฉันหวังว่าคุณจะได้รับความรู้เกี่ยวกับวิธีการจัดการกับงานที่ซ้ำกันในแอปของคุณ ฉันสนุกกับการค้นคว้าและลองใช้วิธีแก้ปัญหาต่างๆ อย่างสนุกสนาน หากคุณไม่พบสิ่งที่ต้องการ ฉันหวังว่าบางตัวอย่างจะเป็นแรงบันดาลใจให้คุณสร้างสรรค์สิ่งที่คุณต้องการ
นี่คือตัวอย่างโปรเจ็กต์ที่มีข้อมูลโค้ดทั้งหมด
แล้วเจอกันใหม่ตอนหน้า ไชโย
ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!