آیا متد find_each
را میشناسید؟ اگر با Ruby on Rails کار کرده باشید حتماً از این متد استفاده کردهاید. find_each
و متد مشابه آن each
، برای گرفتن رکوردها به صورت تکتک استفاده میشوند. به عنوان مثال:
People.find_each(&:party_all_night!) People.each(&:party_all_night!)
تفاوت بین find_each
و each
در نحوهی پیادهسازی آنهاست. متد each
تمام رکوردها را در یک آرایه ذخیره میکند و یک Enumerator
برای این آرایه خروجی میدهد. حالا فرض کنید تعداد رکوردها چند میلیون باشد. تمام این چند میلیون ActiveRecord
باید در حافظه ذخیره شوند. میزان مصرف حافظه سرسامآور خواهد شد. این میتواند مشکلساز باشد، مخصوصا برای پروسسهایی که عمر طولانی دارند چون که Ruby حافظهی گرفته شده را به سیستم پس نمیدهد (و در عوض در آینده از آن دوباره استفاده میکند).
در مقایسه با each
، متد find_each
هوشمندانهتر عمل میکند. این متد رکوردها را در چند مرحله و در دستههای کوچکتر از دیتابیس دریافت میکند. مثلاً دستههای هزارتایی. بنابراین حافظه مصرفی از میزان مورد نیاز برای نگهداری هزار رکورد فراتر نخواهد رفت. به عنوان مثال اگر ده هزار رکورد داشته باشیم، در مجموع ده درخواست به دیتابیس فرستاده میشود و هر کدام فقط هزار رکورد خروجی میدهد.
یکی از پروژههای Rails در سارینا احتیاج به بروزرسانی روزانه میلیونها رکورد دیتابیس دارد. روشهای مختلفی برای انجام این کار هست. واضحترین روش استفاده از update_all
است.
Message.update_all(status: 0)
اما این روش برای ما مناسب نیست. چون منابع سیستمی زیادی را برای مدت طولانی به خود اختصاص میدهد و دیسک IO زیاد میشود، به ویژه به این دلیل که table ما یک trigger دارد که هنگام بروزرسانی هر رکورد یک procedure را صدا میزند. هنگام اجرای این عملیات دیتابیس تقریباً غیر قابل استفاده میشود.
یک راه حل مناسب برای این مشکل این است که مانند find_in_batches
عمل کنیم. یعنی به جای بروزرسانی تمام رکوردها در یک مرحله، این کار را در چند مرحلهی کوچکتر انجام بدهیم. Rails متدی برای انجام این کار ندارد. این امکان را پیادهسازی کردم و برایش تست نوشتم. روی دیتابیس امتحان کردم. این روش مشکل ما را حل کرد. دیگر دیتابیس میتواند بین این بروزرسانیهای کوچک به کارهای دیگر هم رسیدگی کند. حتی میتوانیم این بروز رسانی را در ساعات پر ترافیک روز انجام دهیم.
ما در سارینا از پروژههای متنباز زیادی استفاده میکنیم و تا جایی که بتوانیم به این پروژهها کمک میکنیم. میدانستم برنامهنویسان دیگری هم هستند که به چنین امکانی نیاز دارند، به همین دلیل آن را برای تیم Rails ارسال کردم و پس از بررسیها و پیشنهادهای زیاد با Rails core ادغام شد. به پیشنهاد DHH آغازکنندهی Rails، یک API جدید به نام in_batches
اضافه کردیم که جایگزین find_each
و find_in_batches
خواهد شد. به عنوان نمونه، اگر قبلا با find_each
مینوشتیم:
People.find_each(&:party_all_night!)
حالا مینویسیم:
People.in_batches.each_record(&:party_all_night!)
و برای انجام بروزرسانی به صورت دستهای مینویسیم:
Message.in_batches.update_all(status: 0)
در نهایت استفاده از این روش باعث شد تا یک عملیات بزرگ بروزرسانی به چند عملیات کوچکتر با مصرف منابع ناچیز تبدیل شود.
برای اینکه شما هم این امکان را امتحان کنید Rails خود را به نسخهی پنجم ارتقاع بدهید. اگر از نسخهی چهارم استفاده میکنید میتوانید از این بکپورت برای Rails 4 استفاده کنید.