<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
    <channel>
        <title>نوشته های محمد حسینی راد</title>
        <link>https://virgool.io/feed/@mhrlife</link>
        <description>یک مهندس نرم‌افزار در دیوار.</description>
        <language>fa</language>
        <pubDate>2026-04-15 08:10:35</pubDate>
        <image>
            <url>https://files.virgool.io/upload/users/316150/avatar/8sz7Rs.jpeg?height=120&amp;width=120</url>
            <title>محمد حسینی راد</title>
            <link>https://virgool.io/@mhrlife</link>
        </image>

                    <item>
                <title>چقدر دانشگاه در تعیین آینده منِ برنامه‌نویس تاثیر داره؟</title>
                <link>https://virgool.io/7Learn/%DA%86%D9%82%D8%AF%D8%B1-%D8%AF%D8%A7%D9%86%D8%B4%DA%AF%D8%A7%D9%87-%D8%AF%D8%B1-%D8%AA%D8%B9%DB%8C%DB%8C%D9%86-%D8%A2%DB%8C%D9%86%D8%AF%D9%87-%D9%85%D9%86%D9%90-%D8%A8%D8%B1%D9%86%D8%A7%D9%85%D9%87-%D9%86%D9%88%DB%8C%D8%B3-%D8%AA%D8%A7%D8%AB%DB%8C%D8%B1-%D8%AF%D8%A7%D8%B1%D9%87-k95cmiidtl39</link>
                <description>این چند وقته سردرگمی زیادی در میان کسایی که تازه میخوان مسیرشون رو توی دنیای برنامه‌نویسی شروع کنن دیدم و دوس داشتم کمی درباره این مسئله صحبت کنم. واقعیتش این چند ساله، برنامه‌نویسی طرفدار های زیادی پیدا کرده و بخاطر همین هم بهای بیشتری بهش داده میشه و خب کسی که تازه میخواد وارد این مسیر بشه با تعداد زیادی زبون برنامه‌نویسی، تکنولوژی ، دوره‌های برنامه نویسی، بوت کمپ، و حتی رشته‌های دانشگاهی مواجه میشه و خب آدم‌هایی رو هم می‌بینه که بدون گذروندن حتی یکی از اینها یا با گذروندن همه اینجا به چیزی که میخواستن رسیدن! و این سوال خب چیکار کنم؟ کدوم برای من بهتره؟ شروع تمام سردرگمی ها میشه. قبل از اینکه ادامه مقاله رو بخونی پیشنهاد می‌کنم اول این ویدیو که به همکاری مجموعه آموزش برنامه نویسی سون لرن تهیه شده رو ببینی . https://www.aparat.com/v/WNw0J کسی که تازه 18 سالشه، 5 سال آیندشو چطور می‌بینه؟دنیای کامپیوتر و برنامه‌نویسی، برعکس بقیه رشته‌های دیگه بخصوص مهندسی، نیاز به سخت‌افزار یا ابزارهای مخصوص و گرون قیمت برای شروع کار نداره. با ساده ترین لپ‌تاپ یا کامپیوتر که ارزون‌تر از یک تلفن‌همراه هوشمند هستند میتونید مسیر خودتون رو شروع کنید. پس چیزی که مهمه محتوا و زمانی هست که برای تمرین و یادگیری میذاریم. پس مهم ترین چیز برای کسی که میخواد تازه شروع کنه محتوایی هست که یاد میگیره و زمانی هست که برای یادگیری و تمرین میذاره.آدم بخواد واقع‌بین باشه، ما هرچقدر هم تلاش کنیم؛ کنترلی بر روی آینده خودمون نداریم چراکه پارامتر های زیادی روی اتفاقات آینده تاثیرگذار هست. یعنی حتی 20درصد یا 50درصدشو بخوایم کنترل کنیم هیچ وقت 100 درصد نمیشه.آدم نمیدونه فرداش قراره چطور بشه و من نمیتونم هیچ وقت بگم حتما تا دوسال آینده این مباحث رو یادمیگیرم، نه کمتر نه بیشتر. برای همین همیشه یک برنامه کوتاه مدت داریم و میگیم به احتمال زیاد به اکثر اون چیزی که میخوایم میرسیم و در کنارش یک برنامه بلند مدت داریم که صرفا کلیت مسیر ما رو مشخص میکنه و چیزی که در اون مسیر اتفاق میوفته اکثرا واکنش ما به اتفاق های محیطه، نه چیزی که برنامه‌ریزی کردیم.حالا کسی که 18 سالشه چطور میتونه مطمئن باشه و به خودش بگه اگه من توی 23 سالگی بخوام به فلان سطح برسم حتما حتما باید این مسیر رو برم؟متغیرهای بیرونی و درونیباید قبول کنیم آدم‌ها با هم متفاوت هستن. درسته که شباهت‌های زیادی رو میتونیم توی همدیگه ببینیم اما کوچکترین تفاوت‌ها خیلی وقت‌ها میتونن مسیر دونفر رو کاملا متفاوت کنن. این تفاوت‌ها فقط شامل متغیرهای درونی ما نمیشوند، خانواده‌ایی که توش بدنیا اومدیم، شهری‌ که توش زندگی میکنیم، اگر سر کنکور مریض شده بودیم! و خیلی مثال های دیگه که اون‌ها هم کنترلشون دست‌ ما نیست و توی زندگی همه پیش میان. همه‌ی اینها دست به دست هم میدن که یک طورایی خیال مارو راحت کنن که اگر یک شخصی با یک روشی به چیزی که میخواست رسید، لزوما اگه اون مسیر رو ما نرفتیم دلیل بر این نیست که اشتباه کردیم. پیشبینی آِینده تقریبا غیرممکنه. پس بیایم به اون مسیری که در لحظه داریم طی میکنیم و با تحقیق و تجربه‌های قبلی بهش رسیدیم ایمان داشته باشیم و دلسرد نشیم.آیا برای برنامه‌نویس خوب شدن، به دانشگاه، آموزشگاه، بوت‌کمپ و .. نیاز هست؟جواب این سوال بله و خیر نیست. بیاید یک مثال بزنم. آدمی که بره بهترین دانشگاه ولی به 100% درس‌ها توجه نکنه توی مسیر کاریش پیشرفت میکنه یا آدمی که دانشگاه نرفته ولی حالا بخاطر تجربه‌هایی که داشته یا تلاش خودش برای یادگیری اکثر اون مباحث رو یادگرفته و باهاشون تجربه داشته؟ قطعا جواب دومی هستش. دانشگاه، بوت‌کمپ، آموزشگاه، کارآموزی، ... همه نقش کاتالیزگر رو دارن. یعنی شما وارد محیطی میشید که از قبل افرادی انرژی مصرف میکنند که شما برای درک یک سری مطالب انرژی کمتری مصرف کنید.بیایم واقع‌بین باشیم، توی دانشگاه مفاهیم بسیار ارزشمندی آموزش داده میشه که حتی طرح خود سرفصل‌ها باعث میشه با مفاهیمی که قبلا هیچ ایده‌ایی در موردشون نداشتیم آشنا بشیم و حتی اگر بدترین اساتید رو هم داشته باشیم خودمون میتونیم از این فرصت برای یادگیری استفاده کنیم، اگر استاد خوبی هم داشته باشیم چه عالی! اما باز هم چیزی که این وسط تاثیر بیشتری میذاره اینه ما باید تلاش کنیم و انرژی مصرف کنیم چون دانشگاه به تنهایی کافی نیست. بوت‌کمپ چطور؟ پکیج‌های آموزشی چطور؟ واقعیت اینه این‌ها هم تفاوت آنچنانی ایجاد نمیکنند مگر همراه باشند با تلاش و تمرین ما.چیزی که دانشگاه رو با بقیه متمایز میکنه این هست توی محیط آکادمیک توسط اساتیدی که دانش آکادمیک دارند یاد میگیریم و هضم کردن مطالب تئوری، که پیش‌نیاز یک مهندس نرم‌افزار هستند، برای ما راحت تر میشود. ( البته سربازی رو هم نباید نادیده گرفت، بخصوص برای پسرها)چیزی که پکیج‌های آموزشی رو با بقیه متمایز میکند این هست که یک دوره برای مثال پروژه محور با تمرکز روی مهارت های برنامه نویسی آشنایی ما با تکنولوژی‌های مورد استفاده در بازار کار را بیشتر میکند و آشنایی و استفاده از این تکنولوژی‌ها برای ما راحت تر میشود.چیزی که بوت‌کمپ رو با بقیه متمایز میکند این هست علاوه‌ بر یادگیری تکنولوژی‌های روز بازار، در محیطی مشابه محیط کاری soft skills خودمون رو تقویت میکنیم و آماده برای کارکردن به عنوان یک نیروی تازه‌کار، در شرکت‌ها میشویم.تاثیر دانشگاه بر مهاجرت و استخدامبرای استخدام شدن در شغل‌های رایج توسعه وب، موبایل و نرم‌افزار، اکثرا شرکت‌ها مدرک دانشگاهی یا معادل آن تجربه کاری میخواهند. حالا هستند شرکت‌هایی که مدرک برای آنها الزامی هست. شما اگر تجربه و مهارت کافی داشته باشید میتونید بدون مدرک چه در ایران و چه در خارج ایران (اگر مسئله خدمت حل شده باشد) کار کنید چون در نهایت دانشگاه نقش کاتالیزوری دارد که به افزایش دانش و مهارت شما میخواهد کمک کند.باید واقع بین باشیم، دانشگاه و ادامه تحصیل میتواند ساده‌ترین روش برای مهاجرت باشد، بخصوص برای کسایی که به ادامه تحصیل کشورهایی مثل کانادا و آمریکا علاقه‌مند هستند. مهاجرت کاری در کشورهای اروپایی اتفاقا بسیار شدنی هست اما در کشورهای آمریکای شمالی مقداری سخت‌تر.سخن آخرهمه این‌ها را گفتم که بگم اگر هدفی دارید براش تلاش کنید، تجربه دیگران بسیار ارزشمند هست اما اگر تجربه آنها متفاوت از مسیر شما بود دلسرد نشوید، خیلی منطقی هست که آدم‌ها تجربیات مختلفی داشته باشند. و به این نکته نیز توجه کنید که هرگز خودتان را با فرد دیگری مقایسه نکنید، شما قرار است ورژن ارزشمندی از خودتان باشید نه افراد دیگر.</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Mon, 25 Dec 2023 21:38:27 +0330</pubDate>
            </item>
                    <item>
                <title>آشنایی با Long polling و پیاده‌سازی آن در Go</title>
                <link>https://virgool.io/@mhrlife/%D8%A2%D8%B4%D9%86%D8%A7%DB%8C%DB%8C-%D8%A8%D8%A7-long-polling-%D9%88-%D9%BE%DB%8C%D8%A7%D8%AF%D9%87-%D8%B3%D8%A7%D8%B2%DB%8C-%D8%A2%D9%86-%D8%AF%D8%B1-go-aqpb4stwt0qq</link>
                <description>با یک مثال ساده شروع کنم. فرض کنید میخوایم بک‌اند یک بازی رو پیاده سازی کنیم که نوبتی باشه. مثلا منچ! شطرنج یا حتی دوز! خلاصش اینه من وقتی یک حرکتی انجام میدم باید منتظر بمونم که حریفم حرکت انجام بده تا تخته رو آپدیت کنم. اما چطور بفهمم حریفم حرکتش رو انجام داده؟ بیاید راه حل های مختلف رو بررسی کنیم.هی ریکوئست بزنیم!یک راه حل ساده ایی که به ذهنمون میاد اینه هی ریکوئست بزنیم! چطور؟ یک endpoint پیاده سازی کنیم که اطلاعات تخته رو میده. وقتی حرکتی رو انجام میدیم هر ۲ ثانیه یه بار ریکوئست میزنیم به این اندپوینت تا اگر حریفمون حرکتی انجام داد متوجه بشیم. بیاید کدش رو بزنیم:کد ما یک منطق خیلی ساده رو پیاده سازی میکنه. یک متغیر Boolean داریم به اسم didTheMove که مشخص میکنه حریف ما حرکتش رو انجام داده یا نه. حریفمون با صدا زدن اندپوینت /move میتونه حرکتش رو ثبت کنه و وقتی این کارو میکنه متغیر ما true میشه. این وسط سیستم من هر ۱ دقیقه یک بار داشته /status رو صدا میزده و بلاخره وقتی حریف حرکتشو انجام بده با true شدن متغیر من متوجه این اتفاق میوفتم.امااا! این حرکت یک سری مشکل دارهاگر ما هر یک ثانیه یک بار ریکوئست بزنیم داریم برای هر بازی فعال ۲ ریکوئست برثانیه به سرور میزنه که هم سمت موبایل کاربر باز کردن I/O برای زدن ریکوئست هرثانیه عملیات سنگینیه و اگه گوشیش قدیمی باشه فشار میاد روش هم برای سرور ما سنگین میشه چون اگه ۲۰۰ بازی همزمان داشته باشیم ۴۰۰ بار در ثانیه باید I/O فقط برای هندل کردن HTTP باز کنیم.مشکل اساسی دوم کد ما  اینه که real-time نیست. ما داریم هر یک ثانیه یک بار ریکوئست میزنیم و با یک دیلی یک ثانیه ایی اتفاق ها رو نشون کاربر میدیم. حالا اگه فشار بیاد روی سرور بخاطر مشکل قبلی میگیم عیب نداره ۲ش کنیم و این اتفاق هی میوفته و ریسپانسیو بودن بازی کمتر و کمتر میشه.یک شبیه سازی کنیمفرض کنید یک سشن بازی اینطور داریم که:بازی ۵ دقیقه طول میکشه.هر ۲۰ ثانیه یک بار بازیکن ها حرکتشون رو انجام میدن.یعنی توی این ۵ دقیقه ما ۱۵ بار تختمون آپدیت میشه. اما با این حرکت داریم ۶۰۰ بار به بک‌اند ریکوئست میزنیم. عدد ۶۰۰ یادمون باشه به عنوان بنچ مارک که جلوتر بتونیم راه حل های دیگه رو با هم مقایسه کنیم.استفاده از سوکت - یک کانکشن یک بار برای همیشهبا استفاده از سوکت ما یک کانکشن TCP با سرور باز میکنیم و هر اتفاقی که بیوفته سرور از طریق همون کانکشن بهمون اطلاع میده. این یعنی توی شبیه سازیمون ۲ تا کانکشن باز میکنیم و اکشن ها از طریق همین کانکشن ها بین کلاینت و بک‌اند رد و بدن میشن. این خیلی خوبه! اما مشکلاش چیه؟!سوکت پروگرمینگ تجربه میخواد، باز نگه داشتن کانکشن‌ها، پینگ کردن و ... یک دنیای جدیدیه که تجربه کردنش نیاز به تجربه دارهداریم یک تکنولوژی جدید رو به کلاینت و فرانت و بک‌اند فورس میکنیم. اگر کدمون زده شده نیاز به یک ریفکتور اساسی توی لایه presentation داریم که بتونیم سوکت رو ارائه بدیم.خلاصه بگم. سوکت سخته! آدم ترجیح میده سمتش نره مگه اینکه واقعا نیازی باشه که استفاده ازش به دردسرهاش بچربه!لانگ پولینگ! همون کد HTTP ولی هوشمندانه تربیاید حرکتی که قبلا زدیم رو مرور کنیم. ما یک اندپوینت داشتیم که کلاینت هر ثانیه بهش ریکوئست میزد تا بفهمه وضعیت تخته چطوره. بیاید یکم منطقش رو تغییر بدیم. وقتی کلاینت ریکوئست زد و تخته آپدیت نشده بود کاری نکنیم! کانکشنش رو باز نگه داریم. برای این کار باید به کلاینت بگیم تایم اوت رو مثلا بذار روی ۱ دقیقه! ریکوئستی که زده همینطوری بازه تا وقتی که حریف حرکتی انجام بده. حالا وقتی حریف حرکتشو انجام داد از طریق سیگنال یا ایونتی به ریکوئست status ایی که زده بودیم میگیم حریف حرکتشو انجام داده و اونجا آپدیت جدید تخته رو نشون میدیم.یعنی اگر من یک حرکتی انجام بدم و ۱۵ ثانیه بعد حریفم حرکتشو انجام بده اینطور میشه که بعد از انجام حرکتم یک ریکوئست میزنم به /status و این ریکوئست باز میمونه و منتظر جوابه و ثانیه ۱۵ که حریفم حرکتشو انجام میده بعد از ۱۵ ثانیه ریکوئستم جواب میگیره که تخته‌ی اپدیت شده هست.ببریم توی شبیه‌سازی ایی که کردیمهمون طوری که گفتیم ۱۵ اکشن توی ۵ دقیقه داریم. یعنی توی این ۵ دقیقه ما در کل ۳۰ کانکشن جدید باز میکنیم (که I/O سرور و کلاینت نفس راحت میکشن). اما بازم مثل اگه ۲۰۰ بازی همزمان داشته باشیم ۴۰۰ کانکشن باز داریم در ثانیه. ولی این عدد اصلا زیاد نیست! توجه کنید توی سوکت هم عدد همینه. ما یک کانکشن باز داریم نه اینکه کانکشن جدید باز کنیم!حالا برای همون ۲۰۰ بازی همزمان چند کانکشن جدید (ریکوئست) برثانیه داریم؟ 15/300*400 که میشه ۲۰ ریکوئست برثانیه جای ۴۰۰ ریکوئست برثانیه حالت اولیه. I/O ما نفس راحت میکشه!  برای سوکت این عدد میشه ۲.۵.درسته که ۲.۵ خیلی کمتر از ۲۰ هست ولی این عدد ها برای ۲۰۰ بازی همزمان برثانیه خیلی عدد خوبیه. کد ما اسکیل پذیره و خیلی راحت با یک سری تغییر ساده توی کدبیس قبلیمون میتونیم بهش برسیم جای اینکه کلی دردسر سر سوکت بکشیم! حالا چطور کدشو بزنیم؟ بریم مثال قبلی رو ادیت کنیم:خلاصه بخوام بگم ما یک چنل توی Go ساختم که منتظر میمونیم تا وقتی حرکت انجام بشه یک دیتای خالی توش ریخته میشه و بعد به کاربر اطلاع میدیم. نکته ایی که هست توی select case که منتظر میمونیم دیتایی توی channel ریخته شه ۲ تا حالت دیگه رو هم چک میکنیم. اگه بیشتر از یک دقیقه گذشته بود یا اگر کاربر کانکشن رو کنسل کرد در هردوحالت دیگه گوش دادن به چنل رو متوقف میکنیم چون دیگه کاربر روی اون goroutine نیست و اگه این کارو نکنیم goroutine leak میدیم.مشاهده کد در گیتحرف آخرلانگ پولینگ حرکت جالبیه. تا جایی که من میدونم یک سری از استارتاپ های بزرگ ایرانی برای چتشون و حتی تلگرام برای دادن آپدیت‌های ربات به وب سرورهاشون از این روش استفاده میکنه که نه تنها ساده هست مشکل Real-time بودن رو حل میکنه و پیاده سازی راحتش توی Go هم یک دلیل دیگه برای عاشق این زبان بودنه :)</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Tue, 10 Jan 2023 14:20:51 +0330</pubDate>
            </item>
                    <item>
                <title>آشنایی با Heap و مقایسه آن در یک شبیه سازی</title>
                <link>https://virgool.io/Rocket/%D8%A2%D8%B4%D9%86%D8%A7%DB%8C%DB%8C-%D8%A8%D8%A7-heap-%D8%B3%D8%A7%D8%AE%D8%AA%D9%85%D8%A7%D9%86-%D8%AF%D8%A7%D8%AF%D9%87-%D9%88-%D8%A7%D9%84%DA%AF%D9%88%D8%B1%DB%8C%D8%AA%D9%85-af9lelnvtdck</link>
                <description>هیپ چیه؟ خلاصه بخوایم بگیم یک درخت باینری تقریبا کامل. چه کمکی بهمون میکنه؟ فرض کنید یک لیست از اعداد داریم و میخوایم دنبال minimum ها بگردیم. توی این مقاله در مورد راه حل معمولی و مقایسش با Heap میپردازیم و با شبیه سازی و بنچ مارک نتیجه و تاثیر رو به چشم می بینیم.بیاید یک مسئله ساده حل کنیم. یک لیست از اعداد داریم. میخوایم دنبال کوچیکترین عدد توی اون لیست بگردیم. چطور این کارو انجام میدیم؟ یک دور تک تک عددا رو چک میکنیم و کوچیکترین عدد رو نگه میداریم. یعنی اگر N تا عدد داشته باشیم با N بار مقایسه ما کوچیکترین عدد رو پیدا میکنیم. (توی این مثال ها فرض میکنیم تمام عدد ها از 0 بزرگتر هستند)// FindMin returns minimum number or -1 if numbers is empty
func FindMin(numbers []int) int {
   min := -1
   for _, number := range numbers {
      if min == -1 || number &lt; min {
         min = number
      }
   }
   return min
}خوب حالا یکم مسئله رو متفاوت کنیم. فرض کنید یک جریانی از ورودی ها داریم طوری که در ثانیه K عدد جدید باید به لیستمون اضافه کنیم و M بار مینیمم رو از لیست بگیریم و حذف کنیم (عملیات pop). با این الگوریتمی که داریم چقدر قراره ازمون وقت بگیره؟برای اضافه کردن یک آیتم به لیستمون با یک عملیات o(1) - اضافه کردن یک عدد به آخر آرایه - این کار رو انجام میدیم. اما عملیات pop چقدر طول میکشه؟ اول باید عدد مینیمم رو پیدا کنیم که توی N عملیات (N طول آرایه هست) پیداش میکنیم و بعد از حذفش بازم باید در بدترین حالت N عملیات انجام بدیم (اینطور که یک عدد وسط ارایه رو بخوایم حذف کنیم باید تمام عدد های سمت راستش رو یکی بیاریم سمت چپ)پیچیدگی زمانی این کار ما o(1)*K + o(N)*M هست که میتونیم بگیم o(N)*M. شاید بگید M*o(N) بد نیست؛ اگه عدد های N و M کم باشن چون سرورها خیلی قوی هستن طبیعتا فشاری روشون نمیاد. اما اگر این عدد ها زیاد شن چی؟فرض کنید لیست ما یک میلیون عدد داشته باشه و M (چند بار مینیمم بگیریم) هم 100 باشه. اون موقع برای این سیستم باید در ثانیه 100 میلیون عملیات انجام بدیم.توی الگوریتم ها خیلی سعی میشه اون O(N) تبدیل بشه به O(logN). تاثیر بسیار بسیار زیادی روی سیستم میذاره. چطور حالا؟ فرض کنید پیچیدگی زمانی مون M*O(logN) بود. اون موقع هرثانیه کافی بود 1900 عملیات انجام بدیم! یعنی 52هزار برابر سریع تر! اگر بتونیم چنین کاری بکنیم سیستم ما مقیاس پذیر هم خواهد بود. تاثیر افزایش مقدار دیتا روی پرفورمنس سیستم کمتر میشه.اما چطور O(N) رو به O(LogN) تبدیل کنیم؟یکی از ساختمان داده های جذاب دنیای کامپیوتر درخت Heap هست. هیپ یک درخت باینری و تقریبا کامله. حالا این یعنی چی؟درخت باینری یعنی درختی که هر نود اش حداکثر 2 فرزند داره. یعنی میتونه هیچ فرزندی نداشته باشه؛ میتونه یک فرزند داشته باشه یا 2 فرزند. دیگه سه تا نمیتونه. مثل:Binary Treeحالا هیپ یک درخت تقریبا کامل و باینری ایی هست که توش هر ند از فرزنداش یا حتما کوچیک تره یا حتما بزرگ تره. اگر یک هیپ هر نود از فرزنداش کوچیکتر باشه بهش میگیم min-heap و اگر بزرگتر باشه بهش میگیم max-heap.حالا اگر یک درخت min-heap داشته باشیم همیشه ریشه مینیمم داده هامون هست. ولی اگر بخوایم یک داده جدید بهش اضافه کنیم چقدر قراره ازمون وقت بگیره؟ بهتره الگوریتمش رو ببینیم.اضافه کردن داده به Heapالگوریتم این کار اینطوره که به آخرین برگ درخت داده جدیدمون رو اضافه میکنیم. حالا اون Node رو با پدرش مقایسه میکنیم؛ اگر کوچیکتر باشه یعنی جاش اونجا نیست چون هر node باید از فرزنداش کوچیک تر باشه، پس جای پدر و فرزند رو عوض میکنیم. این عملیات رو اونقدر تکرار میکنیم تا دیگه برسیم به جایی که منطق درخت حفظ بشه و نیاز به Swap کردن نداشته باشیم.افزودن به Heapخب این کار چقدر طول میکشه؟ بیاید ساده یه حساب کتاب کنیم. یک درخت کامل به عمق K توش 2^k-1 عدد داریم.  یعنی اگر N تا عدد داشته باشیم نیازه در بدترین حالت log(N) بار جا به جایی انجام بدیم ( چون اگه log(N) بار انجام بدیم میرسیم به ریشه ی درخت و اون عدد قطعا مینیمم هست).بخوایم جمع بندی کنیم تا الان فهمیدیم اگر بخوایم یک عدد به درختمون اضافه کنیم log(N) بار طول میکشه. اما اگه یادتون باشه توی حالت ساده با O(1) یعنی یک عملیات این کار انجام میشد! اینکه داره بیشتر طول میکشه؟ اره ولی نه :) چون قراره توی pop جبران کنه.پیدا کردن مینیمم در min-heapتوضیح خاصی نداره :) ریشه ما همیشه مینیمم هست. یعنی O(1). حالت ساده این کار O(N) بود. یعنی جای N عملیات داریم یک عملیات انجام میدیم.برداشت (pop) عدد مینیمم از min-heapحالا فرض کنید میخوایم مینیمم رو از درخت حذف کنیم. برای این کار کافیه عدد مینیمم رو حذف کنیم و جاش آخرین عددی که به درخت اضافه کردیم رو بذاریم. حالا اون عدد احتمالا از فرزنداش بزرگ تره. اون رو اونقدر میاریم پایین تا برسه به جایی که منطق درخت حفظ بشه.پیچیدگی زمانی این عملیات چقدره؟ درست حدس زدید. log(n).حالا بیایم یک حساب کتاب کنیم اگه جای اون روش ساده از Heap استفاده میکردیم چقدر کارمون طول میکشید؟ میدونیم که K تا دیتای جدید اضافه میشه پس اضافه کردن به هیپ K*O(LogN) طول میکشه. از اون طرف هم قراره M تا مینیمم پیدا کنیم که M*O(1) طول میکشه و در نهایت M تا pop که M*O(logN) تا طول میکشه. همه اینا با هم میشه چقدر؟ (M+K) * O(LogN) که دقیقا همون چیزی بود که میخواستیم! بریم پیاده سازیش کنیم و در عمل ببینیم تفاوت چقدره!پیاده سازی Heapبرای پیاده سازی Heap من از آرایه استفاده میکنم. اینطور که با این فرمول میتونیم با استفاده از ایندکس هر Node ایندکس فرزنداش رو بدست بیاریم.Left child index = parent index * 2 + 1
Right child index = parent index * 2 + 2پیاده سازی Min-Heap در گو:const MaxInt = int(^uint(0) &gt;&gt; 1)

func leftChildIndex(index int) int {
   return index*2 + 1
}
func rightChildIndex(index int) int {
   return index*2 + 2
}
func parentIndex(index int) int {
   if index%2 == 1 {
      index -= 1
   } else {
      index -= 2
   }
   return index / 2
}

type MinHeap struct {
   items []int
}

func NewMinHeap() *MinHeap {
   return &amp;MinHeap{items: make([]int, 0, 0)}
}

func (m *MinHeap) HasLeftChild(index int) bool {
   return leftChildIndex(index) &lt; len(m.items)
}
func (m *MinHeap) HasRightChild(index int) bool {
   return rightChildIndex(index) &lt; len(m.items)
}
func (m *MinHeap) Swap(index1, index2 int) {
   m.items[index1], m.items[index2] = m.items[index2], m.items[index1]
}
func (m *MinHeap) moveUp(index int) {
   if index == 0 {
      return
   }
   parent := parentIndex(index)
   if m.items[parent] &gt; m.items[index] {
      m.Swap(parent, index)
      m.moveUp(parent)
   }
}
func (m *MinHeap) moveDown(index int) {
   if !m.HasLeftChild(index) &amp;&amp; !m.HasRightChild(index) {
      return
   }
   value := m.items[index]
   left := MaxInt
   right := MaxInt
   if m.HasLeftChild(index) {
      left = m.items[leftChildIndex(index)]
   }
   if m.HasRightChild(index) {
      right = m.items[rightChildIndex(index)]
   }
   swapIndex := -1
   if value &gt; left &amp;&amp; value &gt; right {
      if left &lt; right {
         swapIndex = leftChildIndex(index)
      } else {
         swapIndex = rightChildIndex(index)
      }
   } else if value &gt; left {
      swapIndex = leftChildIndex(index)
   } else if value &gt; right {
      swapIndex = rightChildIndex(index)
   }
   if swapIndex != -1 {
      m.Swap(index, swapIndex)
      m.moveDown(swapIndex)
   }
}

// Pop returns min value or -1 if items is empty
func (m *MinHeap) Pop() int {
   if len(m.items) == 0 {
      return -1
   }
   v := m.items[0]
   m.items[0] = m.items[len(m.items)-1]
   m.items = m.items[:len(m.items)-1] // delete last element of slice
   m.moveDown(0)
   return v
}
func (m *MinHeap) Add(values ...int) {
   for _, value := range values {
      m.add(value)
   }
}
func (m *MinHeap) add(value int) {
   m.items = append(m.items, value)
   m.moveUp(len(m.items) - 1)
}خب این همه کد زدیم من راه حل معمولی و هم کدشو میزنم که بتونیم واقعا یک مقایسه ببینیم :)type BadSolution struct {
   items []int
}

func NewBadSolution() *BadSolution {
   return &amp;BadSolution{items: make([]int, 0)}
}
func (b *BadSolution) Add(values ...int) {
   for _, val := range values {
      b.add(val)
   }
}
func (b *BadSolution) add(value int) {
   b.items = append(b.items, value)
}
func (b *BadSolution) Pop() int {
   minIndex := -1
   minValue := -1
   for i, item := range b.items {
      if minValue == -1 || item &lt; minValue {
         minValue = item
         minIndex = i
      }
   }
   b.items = append(b.items[:minIndex], b.items[minIndex+1:]...)
   return minValue
}حالا بخش جذاب مقایسه پرفورمنس این دو تا کد :) برای این کار میدونم فقط به دو متد Pop و Add نیاز دارم برای همین یک اینترفیس میسازم به اسم Holder و تست رو برای اون اینترفیس مینویسم. بعد یک بار بهش جواب معمولی و یک بار هیپ رو میدم که ببینم چقدر طول میکشه:func main() {
   Test(NewBadSolution())
   Test(NewMinHeap())
}

type Holder interface {
   Add(values ...int)
   Pop() int
}

func Test(holder Holder) {
   rand.Seed(time.Now().Unix())
   k := 10000
   m := 1000
   iterations := 10
   startTime := time.Now()

   for itr := 0; itr &lt; iterations; itr++ {
      // adding items
      for i := 0; i &lt; k; i++ {
         holder.Add(rand.Int())
      }
      for i := 0; i &lt; m; i++ {
         holder.Pop()
      }
   }
   fmt.Println(&amp;quotFor &amp;quot, reflect.TypeOf(holder), &amp;quotTook &amp;quot, time.Since(startTime))
}نتیجه تستاگه تست رو اجرا کنیم نتیجه تست اینطور خواهد بود:For  *main.BadSolution Took  412.2844ms
For  *main.MinHeap Took  4.9997msشاید بگید تفاوت 4 میلی ثانیه با 400 میلی ثانیه خیلی زیاد نیست. فرض کنید درخواست ها زیاد بشه و جای 10 ایتریشن بخوایم 100 ایتریشن داشته باشیم. بنظرتون عددا چطور میشه؟For  *main.BadSolution Took  42.0210378s
For  *main.MinHeap Took  62.9997msمی بینیم که 400 میلی ثانیه رسید به 40 ثانیه اما 4 میلی ثانیه رسید به 62 میلی ثانیه. </description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Thu, 28 Apr 2022 00:51:22 +0430</pubDate>
            </item>
                    <item>
                <title>افزایش پرفورمنس سیستم با دسته دسته کردن درخواست ها</title>
                <link>https://virgool.io/@mhrlife/%D8%A7%D9%81%D8%B2%D8%A7%DB%8C%D8%B4-%D9%BE%D8%B1%D9%81%D9%88%D8%B1%D9%85%D9%86%D8%B3-%D8%B3%DB%8C%D8%B3%D8%AA%D9%85-%D8%A8%D8%A7-%D8%AF%D8%B3%D8%AA%D9%87-%D8%AF%D8%B3%D8%AA%D9%87-%DA%A9%D8%B1%D8%AF%D9%86-%D8%AF%D8%B1%D8%AE%D9%88%D8%A7%D8%B3%D8%AA-%D9%87%D8%A7-cryx9upuzbhm</link>
                <description>توی این مقاله در مورد batch select ها صحبت کردیم. اما واقعیت اینه نیاز سیستم ما فقط select ها نیستن که در بعضی شرایط نیاز به batch شدن دارن. تاثیر این روش در insert ها به مراتب بیشتر خواهد بود. بیاید یک سناریو رو با هم فرض کنیم و مقاله رو طبق اون جلو ببریممقالمون خیلی ربطی به گو نداره ولی عکسشو دوس داشتم:)یک مثال سادهفرض کنید با دانش مقاله قبلی و این مقاله میخوایم یک سیستم کوتاه کننده لینک طراحی کنیم که بتونه لود بالا رو پشتیبانی کنه. برای این کار دو بخش رو باید در نظر بگیریم که روی دیتابیسمون لود میاره. توی یوز کیس میخوایم دیتاهایی که توی دیتابیس هست رو توی یک کش key/value بریزیم که بتونیم راحت تر scale کنیم و ریسپانس تایممون به کاربرایی که روی لینکامون کلیک کردن کمتر شه. این کار رو توی مقاله قبلی انجام دادیمتوی بخش دوم که مربوط به این مقاله هست میخوایم روی یوز کیسی تمرکز کنیم که مربوط به کاربرایی هستن که میخوان لینک بسازن یا لینکاشون رو آپدیت کننمن دارم روی یک مقاله کار میکنم که با هم یک سیستم شبیه توییتر که توزیع پذیر باشه طراحی کنیم. برای همین از مثال توییتر استفاده میکنم. فرض کنید داریم یک سیستم شبیه توییتر طراحی میکنیم و میخوایم سرویس هش تگ ها رو پیاده سازی کنیم. فرض کنید در ثانیه ۵ هزار هشتگ درحال ساخته شدن هست. میخوایم طوری سیستممون رو طراحی کنیم که بتونه این عدد رو پشتیبانی کنه. فرض کنید طراحی دیتابیسمون اینطوره ( من برای تست از پستگرس استفاده کردم)CREATE SEQUENCE hashtags_id_seq;
CREATE TABLE hashtags
(
    id int default nextval(&#039;hashtags_id_seq&#039;::regclass),
    hashtag text NOT NULL,
    PRIMARY KEY (id),
    CONSTRAINT hashtag UNIQUE (hashtag)
);یک جدول به اسم hashtags داریم که دو ستون id و hashtag داره و id کلید اصلی ما هست و hashtag هم unique هست.راه حل ساده! به ازای هر درخواست یک INSERTساده ترین راه حل که به ذهن میاد اینه به ازای هر درخواستی که برامون میاد یک INSERT توی دیتابیس انجام بدیم. بیاید با هم ببینیم چقدر این راه بده! من یک تابع توی Go نوشتم که ورودیش تعدادی هشتگ و کانکشن اتصال به دیتابیس هست.func CreateHashtagsOneByOne(db *sql.DB, hashtags []string) error {
   var placeholder []string
   for _, _ = range hashtags {
      placeholder = append(placeholder, &amp;quot(?)&amp;quot)
   }
   for _, hashtag := range hashtags {
      query := fmt.Sprintf(&amp;quotINSERT INTO hashtags (hashtag) VALUES ($1)&amp;quot)
      if _, err := db.Exec&#40;query, hashtag&#41;; err != nil {
         return err
      }
   }
   return nil
}همون طوری که توی کد معلوم هست به ازای هر هشتگ یک کوئری INSERT INTO hashtags (hashtag) VALUES ($1) اجرا میشود. بیاید اول تست کنیم اگه خواستیم فقط ۵ تا هشتگ با این روش بسازیم چقدر طول میکشه. کد تست اولمون اینطوره:var hashtags []string
for i := 0; i &lt; 5; i++ {
   hashtags = append(hashtags, fmt.Sprintf(&amp;quothashtag_%d&amp;quot, i))
}
s := time.Now()
if err := CreateHashtagsOneByOne(db, hashtags); err != nil {
   panic(err)
}
fmt.Println(time.Since(s))اگه این کد رو اجرا کنیم خروجی ما ۴ میلی ثانیه خواهد بود (یا یه چیزی همین حدودا). عدد خیلی کمیه. حالا اگه جای ۵ تا خواستیم ۵۰ تا هشتگ جدید بسازیم چی ؟ خروجی ما میشه ۷۰ میلی ثانیه! اگه بخوایم ۵۰۰ تا رو ذخیره کنیم چی؟ خروجیمون میشه ۷۰۰ میلی ثانیه. با توجه به اسکیلی که سیستم ما داره میتونه این عددا برای ما قابل قبول باشن. اما چون میدونیم قراره ۵ هزار ریکوئست برثانیه بگیریم با توجه به عددا حس میکنیم ۷ ثانیه طول بکشه! اگه کد رو برای ۵ هزار هشتگ اجرا کنیم عددی حدود ۷ ثانیه میگیریم و این برای سیستم ما اصلا قابل قبول نخواهد بود.حالا میتونیم یک سری گیر به بنچ مارک من بدیم که قبول هم دارم درست هستنیک) ما تمام insert ها رو داریم توی یک ترد انجام میدیم و این باعث میشه هر کوئری منتظر کوئری بعدی بمونه تا تموم شه در صورتی که توی دنیای واقعی اینطور نیست. هر ریکوئست کاربرا میره روی یک گوروتین و هیج وقت منتظر ریکوئست کاربر دیگه ایی نمیمونیم که تموم شه و نوبت کوئری ما برسه.دو) ما فقط از یک کانکشن به دیتابیس داریم استفاده میکنیم. در صورتی که برای بهبود پرفورمنس میتونیم چند تا کانکشن باز کنیممن اول از همه سراغ اینا نرفتم تا یکم مسئله رو باز تر کنم. برای الان بهتره واقعی تر کنیم بنچ مارک رو:برای حل مشکل اول من تک تک INSERT ها رو توی یک گوروتین جدا انجام میدم و در نهایت منتظر میمونم تا همشون انجام بشنبرای حل مشکل دوم هم تنظیمات کانکشنمون رو توی Go اینطور تنظیم میکنم که ۱۰۰ کانکشن باز و IDLE بتونیم داشته باشیم. فقط برای اینکه مطمئن شیم بیشتر از ۱۰۰ کانکشن باز نمیکنیم من یک queue با چنل ها ساختم که اجازه نمیده همزمان بیشتر از ۹۰ تا گوروتین INSERT بزنن.کد ما اینطور میشه:db.SetMaxOpenConns(100) 
db.SetMaxIdleConns(100)
...
func CreateHashtagsOneByOne(db *sql.DB, hashtags []string) error {
   var placeholder []string
   for _, _ = range hashtags {
      placeholder = append(placeholder, &amp;quot(?)&amp;quot)
   }
   queue := make(chan struct{}, 90)
   var wg sync.WaitGroup
   for _, hashtag := range hashtags {
      wg.Add(1)
      go func(hashtag string) {
         queue &lt;- struct{}{}
         query := fmt.Sprintf(&amp;quotINSERT INTO hashtags (hashtag) VALUES ($1)&amp;quot)
         if _, err := db.Exec&#40;query, hashtag&#41;; err != nil {
            panic(err)
         }
         wg.Done()
         &lt;-queue
      }(hashtag)
   }
   wg.Wait()
   return nil
}خب ببینیم خروجی چطور شد؟ توی رنج ۲۵۰ میلی ثانیه کدمون اجرا میشه که قابل قبوله. حالا اگه جای ۵ هزار تا خواستیم ۵۰ هزار تا insert کنیم چطور؟ حدود دو ثانیه.با استفاده از SELECT sum(numbackends) FROM pg_stat_database; هم میتونیم تعداد کانکشن های باز رو ببینیم که بیشتر از ۱۰۰ تا نمیشه.اما چطور این عددا رو بهتر کنیم؟راه حل دوم) چندین درخواست رو با هم بزنیمراه حل دوم ما مثل مقاله قبل اینه یک پنجره داشته باشیم و تمام درخواست های insert توی اون پنجره رو با هم بزنیم. مثلا میتونیم تمام درخواست ها رو توی مموری ذخیره کنیم و هر ۵۰ میلی ثانیه یک بار همشون رو با هم بزنیم. برای این کار از bulk insert استفاده میکنیم که احتمالا باهاش آشنایی دارید و خیلی ازشم استفاده کردید.func CreateHashtagsBatch(db *sql.DB, hashtags []string) error {
   var (
      placeholder []string
      values      []interface{}
   )
   for i, hashtag := range hashtags {
      placeholder = append(placeholder, fmt.Sprintf(&amp;quot($%d)&amp;quot, i+1))
      values = append(values, hashtag)
   }
   query := fmt.Sprintf(&amp;quotINSERT INTO hashtags (hashtag) VALUES %s&amp;quot, strings.Join(placeholder, &amp;quot,&amp;quot))
   _, err := db.Exec&#40;query, values...&#41;
   return err
}با استفاده از کوئری INSERT INTO hashtags (hashtag) VALUES ($1) , ($2),($3)و... میتونیم همزمان چندین هشتگ رو insert کنیم. ببینیم پرفورمنس این کد چطوره.برای ۵ هزار هشتگ با یک کانکشن تونستیم توی ۵۰ میلی ثانیه INSERT کنیم. تا همین الان بهبود خیلی خوبی دادیم به سیستم. حالا اگه بخوایم ۵۰ هزار تا بسازیم چی؟ حدود ۵۰۰ میلی ثانیه. همونطوری که می بینید عددا تا همین الان کلی بهبود پیدا کردن و نیازیم به باز کردن کانکشن جدید و ساختن گوروتین ها نبوده.حالا بیایم یکم عدالت رو رعایت کنیم :)) اینبار با ۱۰ تا کانکشن همزمان INSERT رو برای ۵۰ هزار هشتگ انجام بدیم.func CreateHashtagsBatch(db *sql.DB, hashtags []string) error {
   var (
      concurrentRequests = 10
      eachBatchSize      = len(hashtags) / concurrentRequests
   )
   var batches [][]string
   for i := 0; i &lt; concurrentRequests; i++ {
      startIndex := i * eachBatchSize
      endIndex := (i + 1) * eachBatchSize
      var batchHashtags []string
      if i == 0 {
         batchHashtags = hashtags[:endIndex]
      } else if i == concurrentRequests-1 {
         batchHashtags = hashtags[startIndex:]
      } else {
         batchHashtags = hashtags[startIndex:endIndex]
      }
      batches = append(batches, batchHashtags)
   }

   var wg sync.WaitGroup
   for _, batchHashtags := range batches {
      wg.Add(1)
      go func(hashtags []string) {
         var (
            placeholder []string
            values      []interface{}
         )
         for i, hashtag := range hashtags {
            placeholder = append(placeholder, fmt.Sprintf(&amp;quot($%d)&amp;quot, i+1))
            values = append(values, hashtag)
         }
         query := fmt.Sprintf(&amp;quotINSERT INTO hashtags (hashtag) VALUES %s&amp;quot, strings.Join(placeholder, &amp;quot,&amp;quot))
         if _, err := db.Exec&#40;query, values...&#41;; err != nil {
            panic(err)
         }
         wg.Done()
      }(batchHashtags)
   }
   wg.Wait()
   return nil
}اگر کد رو اجرا کنیم می بینیم که ۵۰هزار تا در حدود ۲۵۰ میلی ثانیه INSERT میشوند که به نسبت ۲ ثانیه پیشرفت زیادی هست. اما یک مسئله مهم رو تا الان در نظر نگرفتیم.اگر بین درخواست ها هشتگی بود که از قبل وجود داره چطور؟فرض کنید طراحی سیستم ما طوریه که نمیدونیم یک هشتگ وجود خارجی داره یا نه. و انبوهی از درخواست ها سمت سیستم ما میاد که نمیدونیم کدوم هشتگ از قبل وجود داره و کدوم رو باید ذخیره کنیم. روش اول و کثیف اینطوره که کل درخواست ها رو بگیریم و یک کوئری بزنیم ببینیم کدوم هشتگ ها وجود دارن و اونایی که وجود ندارن رو جدا کنیم و ذخیره کنیم. اول ۵ میلیون هشتگ برای واقعی تر کردن تست ریختم توی دیتابیس.من کد رو تا جایی که تونستم بهینه زدم. در مرحله اول تمام هشتگ ها رو با هم سعی میکنیم از دیتابیس بگیریم و میریزیمشون توی یک set. حالا یکی یکی چک میکنیم هشتگ هایی که توی ست نیستن رو توی یک اسلایس جدید میریزیم و میدیم همه رو با هم کد batch مون ذخیره کنه.func UpsertOneByOne(db *sql.DB, hashtags []string) {
   var (
      placeholder []string
      data        []interface{}
   )
   for i, hashtag := range hashtags {
      placeholder = append(placeholder, fmt.Sprintf(&amp;quot$%d&amp;quot, i+1))
      data = append(data, hashtag)
   }
   rows, err := db.Query(fmt.Sprintf(&amp;quotSELECT hashtag FROM hashtags WHERE hashtag IN (%s)&amp;quot,
      strings.Join(placeholder, &amp;quot,&amp;quot)), data...)
   if err != nil {
      panic(err)
   }
   availableSet := make(map[string]struct{})
   for rows.Next() {
      var hashtag string
      if err := rows.Scan(&amp;hashtag); err != nil {
         panic(err)
      }
      availableSet[hashtag] = struct{}{}
   }
   var newHashtags []string
   for _, hashtag := range hashtags {
      if _, ok := availableSet[hashtag]; !ok {
         newHashtags = append(newHashtags, hashtag)
      }
   }
   CreateHashtagsBatch(db, newHashtags)
}کد رو برای ۵۰هزار هشتگ (۲۵۰۰۰ تا جدید و ۲۵۰۰۰ قدیمی) اجرا کردم. فرایند پیدا کردن هشتگ های قدیمی ۳۰۰ میلی ثانیه طول کشید و فرایند ذخیره سازی نیست ۱۵۰ میلی ثانیه که در کل میشه ۴۵۰ میلی ثانیه.احتمالا الان براتون سواله خب چرا؟ از قابلیت های دیتابیس استفاده کنیم و کار خودمون رو راحت کنیم دیگه :)) منم باهاتون موافقم.استفاده از ON CONFLICTتوی کوئریمون میتونیم تعریف کنیم که شما بیا INSERT رو انجام بده حالا اگر یک هشتگی از قبل وجود داشت کاری نکن. برای این کار از ON CONFLICTاستفاده میکنیم که خیلی راحت با اضافه کردن کد زیر به تابع Bulk insert مون میتونیم از همون تابع برای این منظور هم استفاده کنیم:query += &amp;quotON CONFLICT (hashtag) DO NOTHING&amp;quotشکل جدید تابعمون مثال قبل رو توی ۲۵۰ میلی ثانیه اجرا میشه که بهبود تقریبا ۲ برابری نسبت به حالت قبل و کد بسیار بسیار تمیز تر هست.حالا اگر خواستیم آپدیت کنیم چطور؟فرض کنید اطلاعات کاربرها همراه هر ریکوئست براتون ارسال میشه. میخواد اگر کاربر وجود داشت اون رو با اطلاعات جدید آپدیت کنید  و اگر وجود نداشت اون رو بسازیم. با استفاده از ON CONFLICT به راحتی میتونیم همین کارو کنیم.بیایم اول یک ستون جدید به دیتابیس به اسم description اضافه کنیم:ALTER TABLE hashtags
    ADD COLUMN description text;حالا میخوایم تعدادی هشتگ با description بسازیم. میخوایم اگر هشتگ از قبل وجود نداشت ذخیره شه و اگر وجود داشت ستون description (فقط) آپدیت بشه. برای این کار میتونیم از کوئری زیر استفاده کنیم:query := fmt.Sprintf(&amp;quotINSERT INTO hashtags (hashtag,description) VALUES %s&amp;quot, strings.Join(placeholder, &amp;quot,&amp;quot))
query += &amp;quot ON CONFLICT (hashtag) DO UPDATE SET description = EXCLUDED.description&amp;quotبرای ۵۰هزار تا مثل مثال های قبل کد رو اجرا کردم و ۵۰۰ میلی ثانیه طول کشید.خب الان با ترکیب این مقاله و مقاله قبل به چی میرسیم؟توی مقاله قبل من سعی ام بر روی این بود که با شو آف کردن قدرت گو :)) یکم در مورد batch کردن درخواست و کم کردن تاثیر شبکه برروی سیستم کیفیت سیستمون صحبت کنیم. با ترکیب این مقاله و مقاله قبلی میتونیم درخواست های SELECT, INSERT و UPDATE سیستممون رو Batch کنیم و فشار روی سیستم رو کمتر کنیم و کیفیتش رو بالا تر ببریم.</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Tue, 15 Feb 2022 20:02:12 +0330</pubDate>
            </item>
                    <item>
                <title>جنریک ها در گو و بررسی پرفورمنس آنها با گرفتن بنچ مارک</title>
                <link>https://virgool.io/golangpub/%D8%AC%D9%86%D8%B1%DB%8C%DA%A9-%D9%87%D8%A7-%D8%AF%D8%B1-%DA%AF%D9%88-%D9%88-%D8%A8%D8%B1%D8%B1%D8%B3%DB%8C-%D9%BE%D8%B1%D9%81%D9%88%D8%B1%D9%85%D9%86%D8%B3-%D8%A2%D9%86%D9%87%D8%A7-%D8%A8%D8%A7-%DA%AF%D8%B1%D9%81%D8%AA%D9%86-%D8%A8%D9%86%DA%86-%D9%85%D8%A7%D8%B1%DA%A9-r8rhw88uh1xh</link>
                <description>در ماه فبریه 2022 به صورت رسمی ورژن 1.18 گو ریلیز میشه و در این ورژن اولین بار در یک ورژن stable از گو ما generics رو داریم و میتونیم ازش استفاده کنیم؛ فیچری که گوفر ها خیلی وقته منتظرش هستن.یک مثال از جنریک! جمع اعدادفرض کن در لحظه بدون جنریک میخوایم توی زبون Go آیتم های یک سری اسلایس رو با هم جمع کنیم و خروجی رو برگردونیم. جواب ساده اینه که نمیتونیم یک تابع داشته باشیم که جامع باشه. باید برای هر تایپ یک تابع بنویسیم. برای مثال:func SumInts(s []int) int {
   var sum int
   for _, num := range s {
      sum += num
   }
   return sum
}
func SumInt64s(s []int64) int64 {
   var sum int64
   for _, num := range s {
      sum += num
   }
   return sum
}

func main() {
   fmt.Println(SumInts([]int{1, 2, 3}))
   fmt.Println(SumInt64s([]int64{1, 2, 3}))
}همونطوری که توی کد بالا می بینید ما برای int64 ها باید یک تابع جدید میزدیم و دقیقا همون لاجیک جمع کردن آیتم های int رو کپی میکردیم. حالا ما چه تایپ های عددی ایی داریم؟ int, int8, int32, int64, uint, uint32, float ... . اگه بخوایم با این روش جلو بریم باید برای تک تک این تایپ ها یک تابع جمع بنویسیم.البته میتونیم با استفاده از interface{} و مقداری تف زدن :) یک تابع داشته باشیم که کار Sum رو برامون انجام بده:func Sum(s interface{}) float64 {
   switch s.(type) {
   case []int:
      return float64(SumInts(s.([]int)))
   case []int64:
      return float64(SumInt64s(s.([]int64)))
   }
   return -1
}ولی خب همونطوری که معلومه فقط در ظاهر کسی که از پکیج استفاده میکنه کارش راحت شده (جدا از اینکه چون داریم float64 میکنیم overhead داریم) اما کدمون پیچیده تر میشه و بعدا برای تعریف تایپ جدید علاوه بر تابع جدید و کپی کردن لاجیک باید case جدید هم اضافه کنیم.نصب ورژن go1.18beta1روزی که این مقاله رو مینویسم آخرین ورژن گو 1.18 بتا هست و هنوز 1.18 ریلیز نشده. برای نصب این ورژن ابتدا دستور زیر را اجرا کنید:go install golang.org/dl/go1.18beta1@latestسپس با اجرای دستور زیر منتظر بمانید تا نصب تکمیل گرددgo1.18beta1 downloadجنریک ها توی گو چطور هستن؟در مرحله اول فرض کنید میخوایم تابع Sum رو تمیز کنیم طوری که بگیم ورودی ما یک آرایه از int یا int64 هست و چون هردو خاصیت عددی دارن گو به ما اجازه میده فرایند جمع رو انجام بدیم و خروجی هم براساس تایپ ورودی برمیگردونیم که overhead نداشته باشیم.func Sum[T int | int64](s []T) T {
   var sum T
   for _, item := range s {
      sum += item
   }
   return sum
}

func main() {
   s1 := Sum([]int{1, 2, 3})
   fmt.Printf(&amp;quot%T %d\n&amp;quot, s1, s1) // int 6
   s2 := Sum([]int64{1, 2, 3})
   fmt.Printf(&amp;quot%T %d\n&amp;quot, s2, s2) // int64 6
}اول خروجی ها رو می بینیم که آره. همونطوری که انتظار داشتیم جمع درست انجام شده بود و تایپ خروجی ها براساس ورودی ها داینامیک بود! این خیلی خوبه. حالا ببینیم کدش رو چطور زدیم؟تابع رو به این شکل تعریف کردیم. Sum[T int | int64](s []T) T توی این خط مشخصه که Sum اسم تابع هست اما بین [] ما تایپ های قابل قبولمون رو با استفاده از constraints ها تعریف میکنیم. مثلا توی این تابع گفتیم ما یک تایپ T داریم که نوعش میتونه int یا int64 باشه. جلوتر گفتیم ورودی s ما از نوع اسلایس T هست و خروجی ما هم از نوع T هست. حالا وقتی توی ورودی تابع یک اسلایس int64 دادیم متوجه میشه که توی کد ما T=int64 و لاجیک ما برای یک اسلایس int64 اجرا میشه. حالا اگه بخوایم به Sum یک تایپ جدید مثلا float64 اضافه کنیم چی؟func Sum[T int | int64 | float64](s []T) T {
   var sum T
   for _, item := range s {
      sum += item
   }
   return sum
}

func main() {
   s1 := Sum([]int{1, 2, 3})
   fmt.Printf(&amp;quot%T %d\n&amp;quot, s1, s1) // int 6
   s2 := Sum([]int64{1, 2, 3})
   fmt.Printf(&amp;quot%T %d\n&amp;quot, s2, s2) // int64 6
   s3 := Sum([]float64{1, 2.5, 3})
   fmt.Printf(&amp;quot%T %f\n&amp;quot, s3, s3) // float64 6.500000
}همونطوری که توی کد واضح هست به T تایپ float64 رو هم اضافه کردیم و خیلی راحت تونستیم از تابع برای float64 ها هم استفاده کنیم.چیزی که هست اگه توجه کنید ما داریم تعریف تابع sum رو شلوغ تر میکنیم. یعنی جایی که تایپ های T رو مشخص میکنیم طولانی و طولانی تر میشه و متاسفانه reusable نیست کد ما. یعنی اگه یک تابع دیگه برای prod (حاصل ضرب آیتم ها) بخوایم بنویسیم باز باید تک تک int int64 float64 و .. ها رو دوباره بنویسیم.برای حل این مشکل میتونیم از interface ها اینطور استفاده کنیم:type numbers interface {
   int | int64 | float64
}

func Sum[T numbers](s []T) T {
   var sum T
   for _, item := range s {
      sum += item
   }
   return sum
}برای تمیز تر شدن کد توصیه میشه یک پکیج به اسم constrains داشته باشیم و این اینترفیس ها اونجا تعریف شده باشن و جا های مختلف پروژه ازشون استفاده کنیم.جنریک های Go از لحاظ پرفورمنسی چطور هستن؟بیاید با هم یک مثال رو در نظر بگیریم. میخوایم یک تابع بنویسیم که بگرده دنبال یک آیتم توی یک اسلایس و بهمون بگه ایندکسش توی اون اسلایس چنده و اگر نتونست پیداش کنه -1 برگردونه.func Find[T comparable](s []T, search T) int {
   for i, item := range s {
      if item == search {
         return i
      }
   }
   return -1
}توی این کد از comparable استفاده کردیم که یکی از کانسترین های پیشفرض زبون هست که به ما اجازه مقایسه رو میده. یعنی یک اینتفرفیس از پیش تعریف شده گو برای این منظور که شامل بولین, اعداد, متن و ... میشود:comparable is an interface that is implemented by all comparable types(booleans, numbers, strings, pointers, channels, interfaces,arrays of comparable types, structs whose fields are all comparable types).خب حالا میدونیم این کد کار میکنه. ولی خوب کار میکنه؟ پرفورمنسی چطوره؟ میخوایم با این دو تابع که مخصوص پیدا کردن یک آیتم توی اسلایس int و string هستن مقایسش کنیم.func FindStr(s []string, search string) int {
   for i, item := range s {
      if item == search {
         return i
      }
   }
   return -1
}
func FindInt(s []int, search int) int {
   for i, item := range s {
      if item == search {
         return i
      }
   }
   return -1
}برای بنچ مارک گیری یک اسلایس 1000 عضوی میسازیم و دنبال عضو 500 ام میگردیم.مقایسه خروجی برای int ها:BenchmarkFindInt       4918909               244.5 ns/op
BenchmarkFind_ForInt           4918566               245.2 ns/opهمونطوری که مشاهده میکنید تفاوت در حد یک نانوثانیه هست که واقعا قابل چشم پوشی هست. برای رشته ها هم همین حرکت رو انجام بدیم:BenchmarkFindStr        888519              1267 ns/op
BenchmarkFind_ForStr            943863              1274 ns/opباز هم می بینیم تفاوت در حد چند نانوثانیه هست که بسیار کم هست و با در نظر گرفتن اینکه چقدر کد ما رو تمیز تر کرده قابل چشم پوشی.جمع بندیبا اومدن جنریک ها به گو در خیلی از توابع به اصطلاح utility کار ما ساده تر خواهد شد. من خیلی خوشحالم چون طی توسعه خیلی نیازش رو حس میکردم. اما جنریک در گو بسیار ابتدایی میباشد و هنوز باید به رعایت اصول و معماری درست پروژه توجه ویژه ایی داشت چون جنریک با magic هایی که در زبان های Dynamic داریم بسیار متفاوت اصل و صرفا در مواردی جلوی دوباره نویسی لاجیک را میگیرد.</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Sun, 02 Jan 2022 22:20:14 +0330</pubDate>
            </item>
                    <item>
                <title>پوینتر ها در گو |‌ آشنایی و درک بهتر آنها</title>
                <link>https://virgool.io/golangpub/%D9%BE%D9%88%DB%8C%D9%86%D8%AA%D8%B1-%D9%87%D8%A7-%D8%AF%D8%B1-%DA%AF%D9%88-%D8%A2%D8%B4%D9%86%D8%A7%DB%8C%DB%8C-%D9%88-%D8%AF%D8%B1%DA%A9-%D8%A8%D9%87%D8%AA%D8%B1-%D8%A2%D9%86%D9%87%D8%A7-ja8q9uwzezin</link>
                <description>اگر قلق و پشت صحنه پوینترها  رو ندونیم به احتمال زیاد توی پیاده سازی هامون دردسر های زیادی خواهیم داشت و خیلی جاها بدون اینکه متوجه باشیم چطور کدمون کار میکنه (یا بدتر با آزمون و خطای چند حالت و رسیدن به حالتی که کار میکنه!) کد میزنیم و بعدا میتونه دردسر داشته باشه برامون.پوینتر ها در گولنگ چی هستن؟قبل از همه یک تعریف انگلیسی در موردش بخونیم:Pointers in Go programming language is a variable which is used to store the memory address of another variable.این تعریف داره میگه پوینترها در گو متغیرهایی هستند که آدرس مموری یک متغیر دیگه رو توی خودشون نگه میدارند. این تعریف یک نکته مهم داره. پوینتر ها یک متغیر هستند. یعنی پوینتر ها چیز خیلی پیچیده ایی نیستند، یک متغیر هستند که جای مثلا استرینگ و اینتیجر توی خودشون آدرس نگه میدارند. خود پوینترها توی مموری مثل بقیه متغیرها یک آدرس دارن و میتونیم با de-reference کردن یک پوینتر به متغیری که به اون اشاره میکنه دسترسی داشته باشیم. بیاید یک مثال بزنیم، یک تایپ تغریف کردم که جلوتر باهاش کلی کار داریم:type T struct {
   Name string
}برای مثال میتونیم به روش زیر از این تایپ یک پوینتر بسازیم:p := &amp;T{
   Name: &amp;quotMohammad&amp;quot,
}علامت &amp; داره میگه که به من مقدار متغیری که ساختم رو برنگردون، در عوض آدرسش توی مموری رو برگردون. پس ما یک پوینتر داریم به اسم P که value اون آدرس یک متغیر توی مموری هست.fmt.Printf(&amp;quotp is pointing to: %p \n&amp;quot, p)       // 0xc00010a040با استفاده از %p هم میتونیم یک آدرس رو نمایش بدیم. همونطوری که می‌بینید توی کد بالا آدرسی که p داره بهش اشاره میکنه رو مشاهده میکنیم. اما همونطوری که گفتم خود پوینترها هم یک نوع متغیر هستند و توی مموری یک آدرس دارن. چطور به اون ها دسترسی داشته باشیم؟fmt.Printf(&amp;quotp address: %p \n&amp;quot, &amp;p)       // 0xc000128018این کد خود آدرس p رو برمیگردونه. همونطوری که میبینید یک آدرس کاملا متفاوت از آدرس متغیر قبلی داریم. حالا اگر بخوایم از یک پوینتر یک کپی بسازیم چه اتفاقی میوفته؟newP := p
fmt.Printf(&amp;quotnewP is pointing to: %p \n&amp;quot, newP) // 0xc00010a040
fmt.Printf(&amp;quotnewP address: %p \n&amp;quot, &amp;newP) // 0xc000128028همونطوری که توی کدبالا می‌بینید آدرسی که متغیر جدیدمون بهش اشاره میکنه با آدرس متغیر قبلی یکسانه. اما چون یک متغیر جدید ساختیم آدرس خود پوینتر با آدرس های قبلی فرق داره.وقتی یک تابع پوینتر میگیره چنین اتفاقی میوفته. ما آدرس رو داریم میفرستیم و باید تغییراتی که میخوایم رو روی اون آدرس تغییر بدیم. یعنی روی value ورودی تابعمون نه روی آدرس متغیری که به عنوان ورودی تعریف کردیم.چطور پوینتر بسازیم؟با سه روش زیر شما یک متغیر پوینتر میسازید:var p1 *T
p2 := new(T)
p3 := &amp;T{}روش دوم و سوم تقریبا یک کار هستند. داره میگه اول یک متغیر توی حافظه بساز، حالا آدرسش رو بریز توی پوینتری که ما گفتیم. اما روش اول صرفا میگه یک پوینتر بساز. بیاید ببینیم این پوینترها به چه آدرسی توی حافظه اشاره میکنند:fmt.Printf(&amp;quotp1 address: %p\n&amp;quot, p1) // 0X0
fmt.Printf(&amp;quotp2 address: %p\n&amp;quot, p2) // 0xc000010200
fmt.Printf(&amp;quotp3 address: %p\n&amp;quot, p3) // 0xc000010210نکته مهمی که باید توجه کنیم آدرسیه که p1 داره بهش اشاره میکنه. به p1 میگیم nil pointer، یعنی یک پوینتر که به متغیر خاصی توی حافظه اشاره نمیکنه و خالیه. چرا باید حواسمون بهش باشه؟ما راحت میتونیم متغیر های p2 و p3 رو با استفاده از *p دی رفرنس کنیم و به مقداری که بهش اشاره میکنن دسترسی داشته باشیم. اما گفتیم p1 به هیچی اشاره نمیکنه، اگه dereference اش کنیم چه اتفاقی میوفته؟panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x49a812]پنیک میکنیم و کرش میکنیم! همیشه باید مراقب این nil pointer ها باشیم و قبل از دی رفرنس کردن یک پوینتر نیل بودنش رو چک کنیم.کار با اسلایس و پوینتر‌هااسلایس چیه؟ یک پوینتر به یک آرایه هست که علاوه بر امکانات آرایه یک شعوری داره که باعث میشه اگر داشتیم از ظرفیت آرایه بیشتر میشدیم یک آرایه جدید میسازه و مقادیر قبلی رو اونجا کپی میکنه و پوینترش دیگه به آرایه جدید اشاره میکنه.برای ساختن یک اسلایس از پوینترها میتونیم از این روش ها استفاده کنیم:var a1 []*T
a2 := make([]*T, 10)
a3 := []*T{
   {Name: &amp;quot&amp;quot},
}خب خب رسیدیم جای جالب. همونطوری که گفتیم اسلایس در اصل یک پوینتر هست. پس اما پوینتر a1 به کجا اشاره میکنه؟ برعکس a2 و a3 که یک آرایه براشون ساخته شده به هیچ جا اشاره نمیکنه:fmt.Printf(&amp;quota1 address: %p\n&amp;quot, a1) // 0x0
fmt.Printf(&amp;quota2 address: %p\n&amp;quot, a2) // 0xc000066050
fmt.Printf(&amp;quota3 address: %p\n&amp;quot, a3) // 0xc00000e028اما بازم باید مراقب باشیم! چرا؟ چون توی a2 ما گفتیم یک آرایه از پوینترها برامون بساز. برامون پوینتر رو میسازه ولی پوینترها nil هستد! fmt.Println(a2) // [&lt;nil&gt; &lt;nil&gt; &lt;nil&gt; &lt;nil&gt; &lt;nil&gt; &lt;nil&gt; &lt;nil&gt; &lt;nil&gt; &lt;nil&gt; &lt;nil&gt;]
fmt.Printf(&amp;quota2[0] address: %p\n&amp;quot, a2[0]) // a2[0] address: 0x0خب این چطور میتونه خطرناک باشه؟ برای مثال توی a3 ما گفتیم یک آرایه از پوینتر ها بساز ولی برای تک تک پوینتر ها داریم میگیم به کجا اشاره کن و nil نیستند.func C4() {
   a1 := []*T{
      {Name: &amp;quot&amp;quot},
   }
   ChangeItem(a1[0])
   fmt.Println(&amp;quotName is&amp;quot, a1[0].Name) // Name is Mohammad
}
func ChangeItem(t *T) {
   *t = T{Name: &amp;quotMohammad&amp;quot}
}اگر این مثال رو اجرا کنیم چون a1[0] ما پوینتری هست که به جایی توی حافظه اشاره میکه و ما داریم اون آدرس رو به تابع میدیم و آدرس توی t هست و با دی رفرنس کردن t به اون متغیر توی حافظه دسترسی داریم. پس میتونیم مقدارش رو تغییر بدیم.func C5() {
   a1 := make([]*T, 10)
   ChangeItem(a1[0]) // panic !!
   fmt.Println(&amp;quotName is&amp;quot, a1[0].Name)
}اما اگر این کد رو اجرا کنیم panic میکنیم. علت اینه ما داریم آدرس 0x0 رو به عنوان ورودی به تابع میدیم و آدرسی که t بهش اشاره میکنه 0x0 هست، پس ما نمیتونیم این پوینتر رو دی-رفرنس کنیم در نتیجه پنیک میخوریم.a1 := make([]*T, 10)
a1[0] = &amp;T{}
ChangeItem(a1[0])
fmt.Println(&amp;quotName is&amp;quot, a1[0].Name) // Name is Mohammadبرای حل این مشکل ما میتونیم قبل صدا زدن تابع یک آدرس توی حافظه به پوینتر بدیم و انتظار داشته باشیم اون آدرس تغییر کنه.البته با این تریک هم میشه یک پوینتر نیل رو مقدار دهی کنیم که من خیلی ازش خوشم نمیاد چون علاوه بر پیچیده کردن و سخت شدن کد امکان بوجود اومدن خطاهای دیگه ایی رو به کد بیس ما اضافه میکنه.ما میتونیم جای اینکه پوینتر نیل رو بخوایم مقدار دهی کنیم میتونیم آدرس اون پوینتر رو به عنوان پوینترِ یک پوینتر به تابع بدیم و از طریق این پوینترِ پوینتر به خود پوینتر دسترسی داشته باشیم و بگیم که به کجا اشاره کنه.func main() {
   var p *T
   ChangeName(&amp;p)
   fmt.Println(p.Name)
}
func ChangeName(t **T) {
   *t = &amp;T{
      Name: &amp;quotHello&amp;quot,
   }
}</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Thu, 15 Jul 2021 13:40:34 +0430</pubDate>
            </item>
                    <item>
                <title>پیاده سازی یک سرویس قابل تست در Golang - قسمت ۱</title>
                <link>https://virgool.io/golangpub/%D9%BE%DB%8C%D8%A7%D8%AF%D9%87-%D8%B3%D8%A7%D8%B2%DB%8C-%DB%8C%DA%A9-%D8%B3%D8%B1%D9%88%DB%8C%D8%B3-%D9%82%D8%A7%D8%A8%D9%84-%D8%AA%D8%B3%D8%AA-%D8%AF%D8%B1-golang-%D9%82%D8%B3%D9%85%D8%AA-%DB%B1-zqlxusr66l33</link>
                <description>گو زبون قوی ایه. به صورت پیشفرض وقتی که نصبش میکنید روی سیستمتون با کتاب خانه‌های استاندارد خودش میتونید یک سیستم قوی بنویسید که پرفورمنس عالی ایی داشته باشه. نیازی به فریم ورک و .. ندارید که کدتون سریع تر و بهینه تر اجرا شه. این موضوع خیلی خوبه اما اگر از دنیای فریم ورک ها وارد Go شده باشید حتما براتون سوال هست خب چطور من کد بزنم؟ معماری نرم افزارم چطور باشه؟ چطور تست کنم و ...کوتاه کننده لینک، مثال همیشگیفرض کنید میخوایم یک سیستم کوتاه کننده لینک بزنیم. این سیستم ۲ تا وظیفه ساده رو میخواد انجام بده، بهش لینک بدیم و بهمون یک آدرس کوتاه شده بده، وارد آدرس کوتاه شده هم شدیم ما رو redirect کنه به آدرسی که بهش داده بودیم.چنین سیستمی رو خیلی راحت میشه زد. میگیم کلا ۲ تا route داره، توی اون route ها دیتابیس رو صدا میزنیم و از دیتابیس اطلاعات میگیریم. اگر قرار باشه سیستم ما دقیقا کارش این باشه و هیچ وقت هیچ تغییری توش نداشته باشیم خوب همین کد رو میزنیم میره پی کارش. اما الان درسته مثال ساده ایی زدیم ولی در نظر بگیریم سیستم ما یک مونولیت بزرگی میخواد بشه بعدا که کلی آدم روش کار میکنن و قراره تکنولوژی های جدیدی بهش اضافه بشه و فیچر بدیم و ... اینجاست که سوال اصلی مطرح میشه. معماری سیستم من چطور باشه؟ میتونیم خودمون طبق نظرمون یک معماری بزنیم و بریم جلو ببینیم جواب میده یا نه، یا از معماری های معمولی که بقیه هم باهاشون آشنا هستن مثل هگزاگونال و کلین و onion و ... استفاده کنیم.توی این مقاله میخوایم سیستم کوتاه کننده آدرس اینترنتیمون رو با معماری ایی شبیه هگزاگونال پیاده سازی کنیم. می بینیم چقدر فرایند تست و توسعه راحت تر میشه و در آینده کارمون برای بزرگتر شدن سیستم بی دردسر تر میشه.معماری هگزاگونال چی میگه؟اول ببینیم نرم‌افزاری که معمولا میزنیم چیه؟ وقتی به نرم‌افزار میگیم فلان URL رو کوتاه کن ما از طریق HTTP داریم به نرم‌افزار دستور میدیم. حالا این اتفاق میتونست توسط web socket اتفاق بیوفته! اصلا نه، ما میتونستیم یک command line داشته باشیم و یک دستور بدیم که فلان URL رو بساز. توی این حالت روشی که به نرم‌افزار دستور میدیم فرق میکنه. اما همیشه به نرم افزار یک URL میدیم و ازش یک URL کوتاه شده میخوایم. منطق و بیزینس لاجیک نرم افزار ثابته ولی اینکه چطور باهاش صحبت میکنیم فرق میکنه.حالا از یک زاویه دیگه نگاه کنیم. دیدیم که ما داریم به سیستممون یک سری دستور میدیم اما خود سیستم هم با یک سری بازیگر های دیگه کار داره! مثلا وقتی میگیم فلان آدرس رو کوتاه کن سیستم باید آدرس جدید و آدرس قدیمی رو یک جا ذخیره کنه. حالا میتونه دیتابیس SQLایی باشه، مونگو باشه، توی فایل و ردیس. توی دو بند بالا دیدیم سیستم ما ۲ تا وابستگی داره که باهاشون یا صدا زده میشه یا سیستم اون ها رو صدا میزنه. اما اینکه اون ها چطور انجام میشن تاثیری در هسته سیستم ما نداره. وقتی میخوایم URL بسازیم باید یک رشته تصادفی تولید کنیم، بررسی کنیم آدرسی که به ما دادن آدرس اینترنتی درستی هست یا نه و در نهایت بدیم که ذخیره بشه. اینکه چطور ذخیره میشه دیگه به هسته سیستم ما ربطی نداره.برای اینکه به چنین چیزی برسیم میتونیم از معماری هگزاگونال استفاده کنیممعماری هگزاگونالدر این معماری ما دو تا ۶ ضلعی داریم. ۶ضلغی داخلی تعریف کننده هسته‌ی برنامه ماست.توی هسته ما کد های لاجیک ما قرار میگیرد. بیزینس لاجیک ما از طریق یک سری port (درگاه) با بیرون ارتباط برقرار میکنند. ما دو نوع port داریم:پورت های Driving: از طریق این درگاه ها اپلیکیشن ما صدا زده شده و عملیات خاصی که میخواهیم انجام میشود. به این‌ها پورت های ورودی هم میگیم چون اولین پورتی هستن که دنیای خارج صدا میزنه.پورت‌های Driven: این درگاه ها توسط اپلیکیشن صدا زده میشوند (مانند دیتابیس، سرویس SMS، کش و ..)همونطوری که گفتیم ۶ضلعی داخلی هسته و لاجیک ما هست که از طریق یک سری درگاه با دنیای خارج صحبت میکنه. حالا ۶ ضلعی خارجی ما میشه پیاده‌سازی های اون پورت ها که بهش آداپتر ها هم میگیم.پروژه Go رو بسازیممن یک پروژه Go ایی با go modules ساختم که میخوایم سرویس کوتاه کننده لینک رو توش پیاده سازی کنیم. اول از همه یک پوشه به اسم internal میسازم که کد های مربوط به سرویسمون رو اونجا بیاریم.اول ببینیم چه دیتاهایی قراره داشته باشیم. توی پوشه internal یک پوشه به اسم models میسازیم که پکیج نگه‌داری مدل هامون رو داشته باشیم. توی پوشه models هم اولین مدلمون یعنی URL رو میسازیم:پکیج models رو برای ساخت مدل هامون استفاده میکنیممیدونیم که قراره مدل URL ما یک کلید کوتاه مختص به خودش داشته باشه و همچنین یک فیلد redirect هم میخوایم که مشخص کنه کجا میخوایم کاربر رو redirect کنیم:type URL struct {
   Key       string
   Redirect  string
   CreatedAt int64
}حالا port هامون رو مشخص میکنیم. توی پوشه internal یک پوشه برای پکیج ports میسازیم. همونطوری که گفتیم دو نوع پورت داریم که توی دو فایل مختلف به نام های incoming و outgoing تعریفشون میکنیم (اسم ها برای من اینطور واضح ترن)حالا با استفاده از یک اینترفیس تعریف میکنیم که پورت های outgoing ما میخوان چطور باشن (بعدا آداپتور ها باید این اینترفیس ها رو پیاده سازی کنند، مثلا دیتابیس SQLایی)type URLRepository interface {
   Save(ctx context.Context, url *models.URL) error
   Find(ctx context.Context, code string) (models.URL, error)
}و همینکار رو هم برای پورت های incoming هم میکنیم (پورت هایی که صدازده میشن. مثلا rest ما صدا میزنه که فلان url رو بساز)type URLService interface {
   Save(ctx context.Context, url *models.URL) error
   Find(ctx context.Context, code string) (models.URL, error)
}چون مثال ما ساده هست service ما خیلی شبیه repository میشه. توی مرحله اول کار service اینه یک ولیدیشن ساده روی دیتاها انجام بده و یک کلید تصادفی به URL بده  و در نهایت برای repository بفرسته که ذخیره بشن. اما یکم جلوتر cache رو هم اضافه میکنیم که می بینیم کارمون چقدر راحت تر میشه.حالا پورت ها رو ساختیم. چیکار کنیم؟ میدونیم همه چیزی که هسته ما نیاز داره رو داریم. شروع میکنیم به نوشتن هسته. اولین چیزی که باید پیاده سازی کنیم سرویس URLهست. برای این کار توی internal یک فایل به اسم url.go میسازم:type urlService struct {
   urlRepository ports.URLRepository
}

func (u urlService) Save(ctx context.Context, url *models.URL) error {
   url.CreatedAt = time.Now().Unix()
   url.Key = shortid.MustGenerate()
   return u.urlRepository.Save(ctx, url)
}
func (u urlService) Find(ctx context.Context, code string) (models.URL, error) {
   return u.urlRepository.Find(ctx, code)
}کاری که ما کردیم اینه اطلاعاتی که گرفتیم رو یکم ویرایش کردیم و برای مدلمون زمان ساخت رو براساس زمان timestamp فعلی ست کردیم و یک کلید رندوم با استفاده از پکیج shortid ساختیم و دادیم به دیتابیس و اطلاعاتی که دیتابیس به ما میده رو مستقیم داریم بر میگردونیم. اما مثلا اگر به ما URL اشتباهی دادن چه اتفاقی میوفته؟ من میخوام یک validation به لایه service مون اضافه کنم. برای این کار اول مدل URLرو اینطور عوض میکنم که از پکیج go-playground/validator بتونم استفاده کنم:type URL struct {
   Key       string
   Redirect  string `validate:&amp;quotrequired,url&amp;quot`
   CreatedAt int64
}و توی سرویسمون هم اینطور ورودی رو validate میکنیم.import (
   &amp;quotcontext&amp;quot
   &amp;quoterrors&amp;quot
   &amp;quotgithub.com/mhrlife/kootah/internal/models&amp;quot
   &amp;quotgithub.com/mhrlife/kootah/internal/ports&amp;quot
   errs &amp;quotgithub.com/pkg/errors&amp;quot
   &amp;quotgopkg.in/go-playground/validator.v9&amp;quot
)

var (
   ErrInputValidationFailed = errors.New(&amp;quotinput validation failed&amp;quot)
)

type urlService struct {
   urlRepository ports.URLRepository
   validator     *validator.Validate
}

func NewUrlService(urlRespository ports.URLRepository) ports.URLService {
   return &amp;urlService{
      urlRepository: urlRespository,
      validator:     validator.New(),
   }
}

func (u urlService) Save(ctx context.Context, url *models.URL) error {
   if err := u.validator.Struct(url); err != nil {
      return errs.Wrap(ErrInputValidationFailed, err.Error())
   }
   url.CreatedAt = time.Now().Unix()
   url.Key = shortid.MustGenerate()
   return u.urlRepository.Save(ctx, url)
}

func (u urlService) Find(ctx context.Context, code string) (models.URL, error) {
   return u.urlRepository.Find(ctx, code)
}من یه سری کار کردم. اول از همه یک ارور جدید تعریف کردم به اسم ErrInputValidationFailed که با استفاده از اون اگر ورودی هام مشکل داشتن بتونم ارور برگردونم. برای اینکه بتونم validation هم انجام بدم باید یک validator میساختم. همیشه برای اینکه یک تایپ خاص رو بسازیم معمولا تابع NewTypeName میسازیم که توش هم مقدار دهی اولیه انجام بدیم و در نهایت آبجکتی که ساختیم رو خروجی بدیم. اگر توجه کنید نوع خروجی ما اینترفیس سرویسی هست که قبلا ساخته بودیم. در نهایت هم قبل از اینکه بخوایم چیزی ذخیره کنیم کار validation رو انجام میدیم.چرا این همه خودمون رو اذیت کنیم؟ احتمالا این سوالیه که الان توی ذهنتون شکل گرفته. مثالی که ما میزنیم مثال ساده ایی هست که فقط یک repository داره و یک کار ساده انجام میده. اگر پروژه بزرگتر بشه این معماری میتونه جلوی خیلی از پیچیدگی ها رو بگیره و تست مارو ساده تر کنه. راستی چطور بفهمیم کدی که زدیم درست؟نوشتن تست و اجرای آنبرای اینکه متوجه بشیم لاجیکی که نوشتیم درست کار میکنه یا نه باید تستش کنیم. اما الان ما آداپتوری نداریم. دیتابیسی نداریم. postman ایی نداریم که درخواست HTTP بزنه. چطور تست کنیم؟معماری هگزاگونال به ما این امکان رو میده بیزینس لاجیک رو بدون نیاز به هیچگونه آداپتوری (دیتابیس، REST و...) تست کنیم. چون اصلا هدف کار ما این بود. ما کدی زدیم که لایه لاجیک رو از همه چیز جدا کنیم.برای اینکه تست کنیم از mock کردن استفاده میکنیم. یعنی چون interface هایی که لاجیک ما بهشون نیاز داره رو داریم یک تایپ mock میسازیم که کارای اون اینترفیس ها رو تقلید کنیم.  تست هایی که مینویسیم لایه  لایه خواهد بود. یعنی توی لایه بیزینس لاجیک در نظر میگیریم که آداپتور های ما همیشه درست کار میکنن و بعدا وقتی آداپتور ها رو مینویسیم براشون تست ایی مینویسیم که فقط اون ها رو تست کنن.برای مثال اگر میخوایم ببینیم لاجیک ما درست کار میکنه برای تابع Save که خیلی سادست یک تست مینویسیم که URL اشتباه باشه و یک تست که URL درست باشه (تنها لاجیک ما در این مثال ساده همین بوده). بیاید همین الان همین کار رو بکنیم. برای نوشتن ماک و تست کردن از کتابخانه mockery و testify استفاده میکنیم. با استفاده از کامند زیر برای تک تک اینترفیس هامون یک فایل mock میسازیم:mockery --allاین ابزار ماک های ما رو در یک پوشه به نام mocks قرار میده و بعدا از این پکیج میتونیم استفاده کنیم.  برای نوشتن تست های urlمون توی پوشه internal کنار فایل url یک فایل به نام url_test.go میسازم.اولین تستی که میخوایم بنویسیم اینه که یک URL درست بدیم و انتظار داریم Save ریپازیتوری Url صدا زده بشه و یک مقدار random برای کلید URL ست بشه.  برای این کار اینطور تستی مینویسیم:func TestUrlService_SaveSuccessful(t *testing.T) {
   urlRepMock := &amp;mocks.URLRepository{}
   logicUrl := NewUrlService(urlRepMock)
   ctx := context.Background()

   urlRepMock.On(&amp;quotSave&amp;quot, ctx, mock.AnythingOfType(&amp;quot*models.URL&amp;quot)).Return(nil)

   url := models.URL{Redirect: &amp;quothttps://google.com&amp;quot}
   err := logicUrl.Save(ctx, &amp;url)

   assert.Equal(t, nil, errs.Cause(err))
   assert.NotEqual(t, &amp;quot&amp;quot, url.Key)

   urlRepMock.AssertExpectations(t)
}برای اجرای تست:go test ./...اول اومدیم یک logic با استفاده از mockمون ساختیم. mockبه ما این قابلیت رو میده بهش بگیم انتظار چه ورودی هایی داشته باشه و هر کدوم رو گرفت چه رفتاری نشون بده. برای مثال گفتیم اگر Save صدا زده شد ورودی اولت باید context ایی باشه که اول ساختیم و ورودی دوم از تایپ models.URL و وقتی این رو گرفتی nil برگردون (یعنی ارور نخوردیم)حالا وقتی logicUrl.Save صدا زده میشه توی لاجیک متد Save از urlRepository هم صدا زده میشه که طبق mockمون عمل میکنه. در آخر برای اینکه متوجه بشیم تست ما درست انجام شده چک میکنیم که ارورمون حتما nil باشه و اینکه توی تابع برای Key یک مقدار تصادفی ست شده باشه. در نهایت هم با AssertExpectations هم به Mock میگیم چک کن تمام انتظاراتی که داشته برآورده شده اند یا نه.حالا یک تست جدید مینویسیم که انتظار داریم ارور دریافت کنیم چون میخوایم جای urlیک عبارت اشتباه بدیم:func TestUrlService_SaveErrValidation(t *testing.T) {
   urlRepMock := &amp;mocks.URLRepository{}
   logicUrl := NewUrlService(urlRepMock)
   ctx := context.Background()

   url := models.URL{Redirect: &amp;quotrandom text&amp;quot}
   err := logicUrl.Save(ctx, &amp;url)

   assert.Equal(t, ErrInputValidationFailed, errs.Cause(err))

   urlRepMock.AssertExpectations(t)
}توی این تست صرفا چک کردیم که حتما error بگیریم و ارورمون ErrInputValidationFailed  باشه.جمع بندیما با استفاده از این معماری تونستیم بیزینس لاجیکمون رو بدون بالااوردن دیتابیس و نوشتن یک سرویس RESTو... تست کنیم. اینطور باعث میشه سرعت کارمون هم بیشتر بره بالا هم بخاطر تست هایی که مینویسیم کدمون در آینده قابل اعتماد تر باشه.در مقاله بعدی آداپتور ها رو مینویسیم و یک لایه کش هم به سرویسمون اضافه میکنیم.</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Wed, 02 Jun 2021 22:29:19 +0430</pubDate>
            </item>
                    <item>
                <title>ارث‌بری در Go؟ آشنایی با Composition در Golang.</title>
                <link>https://virgool.io/golangpub/%D8%A7%D8%B1%D8%AB-%D8%A8%D8%B1%DB%8C-%D8%AF%D8%B1-go-%D8%A2%D8%B4%D9%86%D8%A7%DB%8C%DB%8C-%D8%A8%D8%A7-composition-%D8%AF%D8%B1-golang-jijn4mfxkeij</link>
                <description>یک مثال ساده رو در نظر بگیریم. ما در زبان‌های شی‌گرا احتمالا یک دانش‌آموزی داریم که از کلاس انسان ارث بری میکنه.class Person {
   String name
}
class Student extends Person{
   String grade
}در کد بالا کلاس دانش‌آموز ما از کلاس Person ارث بری ( Inheritance ) کرده. یعنی دانش‌آموز ما تمام خصوصیات ( صفات و متدها ) کلاس Person رو به ارث برده. ما میتونیم اگر جایی ورودی تابعی Person بود یک آبجکت از Student ورودی بدیم (اصل Liskov Substitution - جلوتر بیشتر در موردش صحبت میکنیم)حالا این اتفاق توی Go چطور انجام میشه؟توی Go ما از Inheritance استفاده نمیکنیم، بلکه از Composition با استفاده از Embedding تایپ ها استفاده میکنیم. مثال دانش‌آموز و انسان رو در نظر بگیرید. در Inheritance ما از کلاس Person استفاده میکردیم و یک کلاس جدید با تمام خصوصیات Person استفاده میساختیم و در ادامه صفات بیشتری که مد نظرمون رو هم به کلاس Student اضافه میکردیم.اما در Composition ما تایپ جدیدی که میسازیم تایپ مادر رو شامل میشه. یعنی چی؟ مثال قبلی رو اگر بخوایم با Composition بنویسیم اینطور میشه:class Person {
   String name
}
class Student{
   Person person
}همونطوری که می‌بینید ما تمام خصوصیاتی که از Person میخوایم رو در صفتی به نام person ذخیره کردیم و برای مثال از طریق person میتونیم به نام دانش‌آموز دسترسی داشته باشیم. حالا مثالش توی Go چطور میشه؟type Person struct {
   Name string
}
type Student struct {
   Person
   Grade int
}اینطور هم میتونیم یک دانش‌آموز بسازیم:Ali := &amp;Student{
   Person: Person{
      Name: &amp;quotAli&amp;quot,
   },
   Grade: 12,
}
fmt.Println(Ali.Person.Name)
fmt.Println(Ali.Grade)به لطف Go وقتی Composition ایی انجام میدیم که صفاتی با نام های متفاوت داشته باشیم میتونیم اسم متغیری که کامپوزیشن رو نگه میداره رو هم صدا نزنیم و مستقیم متغیری که میخوایم رو صدا بزنیم:fmt.Println(Ali.Person.Name) // Ali
fmt.Println(Ali.Name) // Aliاما برای مثال اگر تایپ Student ما علاوه بر Person شامل Class هم بود که مشخص کنه دانش‌آموز در چه کلاسی درس میخونه کد ما اینطور میشد:type Class struct {
   Name string
}
type Student struct {
   Person
   Class
   Grade int
}حالا اگر Ali.Name بزنیم کامپایلر به ما ارور میده که Name مبهم هست (منظور اسم Class هست یا اسم Person؟؟). در چنین شرایطی اسم کامپوزیشن رو هم باید صدا بزنیم: Ali.Person.Name یا Ali.Class.Name.خب بریم سراغ اصل Liskov. اگر از یک زبان شی گرا سمت Go اومده باشید مثل من این اصل اینطور توی ذهنتون مونده که اگر کلاس A فرزند کلاس B باشه هرجا نیاز به کلاس B بود میتونیم کلاس A رو به عنوان ورودی بدیم. مثلا اگر تابعی داشتیم ورودی Person میگرفت میتونیم جاش Student رو بدیم. اما توی Go ما که Inheritance نداریم. حالا چیکار کنیم؟ فرض کنید چنین تابعی داریم:func Greeting(person Person) {
   fmt.Println(&amp;quotWelcome &amp;quot, person.Name)
}ورودی این تابع از نوع Person هست. اگر بخوایم Student بهش بدیم چه اتفاقی میوفته؟ احتمالا IDE تون این ارور رو میده اما اگرم نداد و بخواید کامپایل کنید اینطور اروری میگیرید:Ali := Student{
   Person: Person{
      Name: &amp;quotAli&amp;quot,
   },
   Grade: 12,
}
Greeting(Ali)
...
cannot use Ali (type Student) as type Person in argument to Greetingخب. برای این کار باید چیکار کنیم؟ اول بیایم ببینیم اصل Liskov چی میگه؟ فرض کنید کلاینت (کلاس، متد، تابع و... کلا هر چیزی) A از کلاس B میخواد استفاده کنه. حالا کلاس C از کلاس B نشئت گرفته. ما باید بتونیم در صورت نیاز کلاس C رو جای کلاس B به A بدیم. یعنی B نه چیزی رو کمتر برای C پیاده سازی کرده باشه نه چیزی رو بیشتر.حالا برای اینکه این موضوع رو در Go داشته باشیم از اینترفیس ها استفاده میکنیم. اول برای تایپ Person یک متد میسازیم به اسم GetName() که نام رو خروجی بده:type Person struct {
   Name string
}
func (p Person) GetName() string {
   return p.Name
}ما میتونیم به این متد از طریق student.GetName() دسترسی داشته باشیم. حالا میایم یک اینترفیس به اسم Namer تعریف میکنیم که متد GetName() داشته باشه:type Namer interface {
   GetName() string
}حالا میتونیم جای اینکه یک تایپ رو به عنوان ورودی بگیریم از اینترفیس استفاده کنیم. یعنی تابع Greeting ما اینطور میشه:func Greeting(person Namer) {
   fmt.Println(&amp;quotWelcome &amp;quot, person.GetName())
}و راحت میتونیم Ali رو به عنوان ورودی بهش بدیم:func main() {
   Ali := Student{
      Person: Person{
         Name: &amp;quotAli&amp;quot,
      },
      Grade: 12,
   }
   Greeting(Ali)
}
func Greeting(person Namer) {
   fmt.Println(&amp;quotWelcome&amp;quot, person.GetName()) // Welcome Ali
}حالا میتونیم راحت تایپ Teacher رو هم با استفاده از Person بسازیم و به عنوان ورودی Greeting بدیم:type Teacher struct {
   Person
   Age int
}
func main() {
   Saeed := Teacher{
      Person: Person{Name: &amp;quotSaeed&amp;quot},
      Age:    24,
   }
   Greeting(Saeed)
}
func Greeting(person Namer) {
   fmt.Println(&amp;quotWelcome &amp;quot, person.GetName()) // Welcome Saeed
}با این کار ما مجبور میشیم اصل Dependency inversion رو هم رعایت کنیم.</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Tue, 18 May 2021 10:31:17 +0430</pubDate>
            </item>
                    <item>
                <title>پیاده سازی Promises در Golang</title>
                <link>https://virgool.io/RubikGroup/%D9%BE%DB%8C%D8%A7%D8%AF%D9%87-%D8%B3%D8%A7%D8%B2%DB%8C-promises-%D8%AF%D8%B1-golang-sevzfaom1o7i</link>
                <description>زبان Go با معماری CSP به ما قدرت خیلی زیادی میده که سیستم concurrent مورد نظرمون رو طراحی کنیم. اما حداقل برای من موقع شروع کار یکم گیج کننده بودن، چنل ها و گوروتین ها و اینکه چطور با ترکیبشون میتونم از تمام قدرت CPU مون برای یک برنامه استفاده کنیم.یک مثال ساده، درخواست batchفرض کنید یک سرویس دارید که چند هزار ریکوئست برثانیه میخواد پاسخ بده. این سرویس میخواد یک دیتایی رو از کش ردیس بخونه و به ما بده. حالا اگر برای هردرخواست ریکوئست بزنیم به ردیس i/o ما اذیت میشه، درحالی که میدونیم ردیس قابلیت درخواست های batch رو داره که میدونیم خیلی خیلی سریع تر پاسخشون میده. سوالی که مطرح میشه چطور از این قابلیت ردیس توی Go استفاده کنیم؟حالت معمولی اینطوره که برای هر درخواست HTTP یک تابع صدا میزنیم که دیتا رو از ردیس بگیره. پس به ازای هر درخواست HTTP یک درخواست به redis داریم. اما کاری که میخوایم انجام بدیم اینه هر ۵۰ میلی‌ثانیه یک بار تمام درخواست های redis رو با هم بفرستیم. یعنی یک درخواست HTTP وقتی اون تابع رو صدا زده میشه باید صبر کنه که پنجره ۵۰میلی‌ثانیه ایی تموم شه که سرویس ما تمام درخواست ها رو با هم بفرسته.چرا این موضوع میتونه پرفورمنس سیستم رو بهتر کنه؟همون طوری که توی این پست صحبت کردیم، اگر از pipeline ردیس استفاده کنیم هم i/o ما فشار کمتری بهش میاد هم اینکه خود ردیس syscall های کمتری میزنه و به شکل چشم‌گیری پرفورمنس ما هم بهتر میشه.اگر سیستم رو به حال خودش رها کنیم و ۲ هزار ریکوئست برثانیه داشته باشیم ۲ هزار ریکوئست برثانیه به ردیس میزنیم. اما اگر به صورت batch هر ۵۰ میلی‌ثانیه درخواست بزنیم همیشه ۲۰ درخواست در ثانیه به ردیس میزنیم.البته به یک موضوعی باید توجه کرد. با این کار میانگین ریسپانس تایم سیستم رو در حال معمولی ۵۰ میلی‌ثانیه بیشتر میکنیم که عدد زیادیه. اما توی درخواست های این سیستم خودش رو نشون میده.متوجه شدیم که میخوایم چیکار کنیم. اما چطور توی Go پیادش کنیم؟ با کمک channel های go ما میتونیم promises رو پیاده سازی کنیم. اینطوری به قضیه نگاه کنید. ما وقتی تابع رو صدا میزنیم، تابع به ما یک متغیر میده که میدونیم در لحظه هنوز توش مقداری ریخته نشده. اون متغیر میره توی thread های مختلف و ما اصلا نگران race condition نیستیم، ما فقط منتظریم تا توی اون متغیر مقداری ریخته بشه. درخواست HTTP ما قفل میشه تا وقتی مقداری توی اون متغیر ریخته بشه.مثال Promises در Golangنمونه کد زیر رو در نظر بگیرید:func PromiseMe() chan bool {
   c := make(chan bool)
   go func() {
      time.Sleep(2 * time.Second)
      c &lt;- true
   }()
   return c
}توی این تابع Go ما میایم اول یک چنل میسازیم، در ادامه یک تابع رو با استفاده از go توی یک گوروتین‌ (thread خیلی سبک) اجرا میکنیم. گوروتین ما خیلی سادست، ۲ ثانیه صبر میکنه و بعدش مقداری رو توی چنل میریزه. اما میدونیم این تابع توی ترد دیگه ایی اجرا میشه. ما همون موقع داریم چنل رو برمیگردونیم.func main() {
   promise := PromiseMe()
   response := &lt;-promise
   if response {
      // we got true!
   }
}حالا توی این نمونه کد ما چنل رو از تابع گرفتیم. میدونیم در لحظه مقداری توی چنل ریخته نشده، توی خط بعد ما با استفاده از &lt;- داریم میگیم آخرین مقدار توی چنل رو به ما بده (چنل ها مثل صف میمونن و میتونن تعداد مقادیر مدنظرمون عضو نگه دارن). نکته ایی که هست چون چنل ما مقداری توش نیست اون خط کد قفل میشه (همچنین تردش) تا وقتی مقداری توی اون چنل ریخته بهشه.پیاده سازی batch با استفاده از channelsمثالی که اول مقاله گفتم یکم پیچیده هست (قبلا توی این مقاله نمونه کدش رو زده بودیم، میتونید اینجا یه نیم نگاهی بندازید) بیاید یک مثال ساده بزنیم. فرض کنید یک تابع داریم که بهش یک متن میدی و اون رو نمایش میده. حالا میخوایم جای اینکه هربار تابع رو صدا زدیم متن رو نشون بده، جمع کنه هر ۵ ثانیه همه رو با هم نشون بده.type Printer struct {
   channel chan string
}
func (p *Printer) Print(s string) {
   p.channel &lt;- s
}
func (p *Printer) work() {
   for {
      time.Sleep(5 * time.Second)
      // reading
      batch := &amp;quot&amp;quot
      lenChannel := len(p.channel)
      if lenChannel == 0 {
         continue
      }
      for i := 0; i &lt; lenChannel; i++ {
         batch += &lt;-p.channel + &amp;quot\n&amp;quot
      }
      fmt.Println(&amp;quot\n&gt; batch print on &amp;quot, time.Now())
      fmt.Println(batch)
   }
}
func NewPrinter() *Printer {
   p := &amp;Printer{
      channel: make(chan string, 100),
   }
   go p.work()
   return p
}همونطوری که میبینید یک تایپ جدید به اسم Printer نوشتیم که دو تا متد داره. اولی Print هست که خیلی ساده هرمقداری که میگیره رو توی چنلی که قبلا برای Printer تعریف کردیم میریزه. همچنین متد work که جلوتر میبینیم قراره توی یک گوروتین جدا اجرا بشه. این تابع ۵ ثانیه صبرمیکنه، چک میکنه چقدر توی چنلمون ریخته شده، تک تکشون رو میخونه و درنهایت با هم نمایش میده.نکته ایی که هست تابع NewPrinter هست که یک چنل با ظرفیت ۱۰۰ میسازه، متد work رو به صورت گوروتین اجرا میکنه و در نهایت Printer ایی که ساخته رو برمیگردونه.func main() {
   printer := NewPrinter()
   for {
      printer.Print(&amp;quotHello -&amp;quot + time.Now().String())
      time.Sleep(1 * time.Second)
   }
}در نهایت توی تابع main یک پرینتر میسازیم، و هر ثانیه یک متن جدید رو باهاش پرینت میکنیم. خروجی چطور میشه؟&gt; batch print on  2021-04-11 22:39:48.922123619 +0430 +0430 m=+10.000904303
Hello -2021-04-11 22:39:43.922688513 +0430 +0430 m=+5.001469122
Hello -2021-04-11 22:39:44.923071867 +0430 +0430 m=+6.001852526
Hello -2021-04-11 22:39:45.92339546 +0430 +0430 m=+7.002176162
Hello -2021-04-11 22:39:46.92362527 +0430 +0430 m=+8.002405921
Hello -2021-04-11 22:39:47.924194516 +0430 +0430 m=+9.002975169&gt; batch print on  2021-04-11 22:39:53.92252801 +0430 +0430 m=+15.001308662
Hello -2021-04-11 22:39:48.924275894 +0430 +0430 m=+10.003056544
Hello -2021-04-11 22:39:49.92445176 +0430 +0430 m=+11.003232412
Hello -2021-04-11 22:39:50.924880096 +0430 +0430 m=+12.003660754
Hello -2021-04-11 22:39:51.925031043 +0430 +0430 m=+13.003811747
Hello -2021-04-11 22:39:52.925309521 +0430 +0430 m=+14.004090169اگر میخواید یک کد یکم پیچیده‌تر ولی با همین منطق رو ببینید حتما این مقاله رو مطالعه کنید. از ۸۰۰ ریکوئست برثانیه میرسیم به ۱۴ هزار تا :)چند تا مثال دیگهunc PromiseMe() chan bool {
   c := make(chan bool)
   go func() {
      time.Sleep(2 * time.Second)
      c &lt;- true
   }()
   return c
}

func main() {
   // Simple promise
   a1 := PromiseMe()
   &lt;-a1
   // Wait for all
   a2 := PromiseMe()
   b2 := PromiseMe()
   c, d := &lt;-a2, &lt;-b2
   // Wait for the first one
   a3 := PromiseMe()
   b3 := PromiseMe()
   var c3 bool
   select {
   case c3 = &lt;-a3:
   case c3 = &lt;-b3:
   case &lt;-time.After(time.Second * 2): // 2 seconds timeout
      c3 = false
      // handling timeout
   }
   // all with timeout
   a4 := PromiseMe()
   b4 := PromiseMe()
   ctx, _ := context.WithTimeout(context.Background(), time.Second*1)
   select {
   case &lt;-a4:
   case &lt;-ctx.Done():
   }
   select {
   case &lt;-b4:
   case &lt;-ctx.Done():
   }
}</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Sun, 11 Apr 2021 23:02:25 +0430</pubDate>
            </item>
                    <item>
                <title>پوینتر ها میتونن کد Go شما رو کند کنن!</title>
                <link>https://virgool.io/golangpub/%D9%BE%D9%88%DB%8C%D9%86%D8%AA%D8%B1-%D9%87%D8%A7-%D9%85%DB%8C%D8%AA%D9%88%D9%86%D9%86-%DA%A9%D8%AF-go-%D8%B4%D9%85%D8%A7-%D8%B1%D9%88-%DA%A9%D9%86%D8%AF-%DA%A9%D9%86%D9%86-snqvaelmueqq</link>
                <description>اوایل که وارد دنیای Go شده بودم برام دنیای گیج کننده ایی بود. توی PHP خیلی با پوینتر سر و کله نمیزدم. چیزی که معمولا میشنیدم از دوستام اینه که خروجی توابعت رو با پوینتر برگردونی بنظر بهتر میاد. با خودمم فکر میکردم که منطقیه دیگه. نمیاد همه‌ی مقادیر یک متغیر رو کپی کنه. فقط پوینتر رو برمیگردونه. اما اشتباه میکردم.آیا خروجی دادن یک مقدار پوینتری سریع تره؟خداروشکر توی Go ابزار Benchmark گیری داریم! بدون اینکه بریم ببینیم دلیل چیه و اینا یک بنچمارک بگیریم ببینیم اصلا حرفی که میزنیم درست هست یا نه. من یک تایپ به اسم SimpleReturn تعریف کردم که یک struct ساده هست که قراره خروجی تابعمون باشه.type SimpleReturn struct {
   Value int
} حالا وقتی این متغیر رو ساختیم کلش رو برگردونیم یا فقط پوینترش رو؟func NewSimpleReturn(n int) *SimpleReturn {
   return &amp;SimpleReturn{Value: n}
}
func NewSimpleReturnV(n int) SimpleReturn {
   return SimpleReturn{Value: n}
}همونطوری که می‌بینید توی تابع اول ما مقدار پوینترش رو برگردوندیم و در تابع دوم یک کپی از مقادیر متغیر رو.NewSimpleReturn: به صورت پوینتر برمیگردونهNewSimpleReturnV: به صورت مقدار برمیگردونهبرای تست این موضوع که کدوم یکی از این دو روش سریع تر هستن یک بنچ مارک هم نوشتم:var vSimpleReturnV SimpleReturn
var vSimpleReturn *SimpleReturn
func BenchmarkNewSimpleReturn(b *testing.B) {
   for i := 0; i &lt; b.N; i++ {
      vSimpleReturn = NewSimpleReturn(10)
   }
}
func BenchmarkNewSimpleReturnV(b *testing.B) {
   for i := 0; i &lt; b.N; i++ {
      vSimpleReturnV = NewSimpleReturnV(10)
   }
}همونطوری که میبینید بنچمارک اول برای تابع ‌ایی هست که پوینتر برمیگردونه و بنچمارک دومی برای تابعی که مقدار برمیگردونه. اجرا میکنم و با هم ببینیم جواب چطوره:goos: linux
goarch: amd64
pkg: hello
BenchmarkNewSimpleReturn-8      84650518                14.6 ns/op
BenchmarkNewSimpleReturnV-8     1000000000               0.405 ns/op
PASSاجرای هر خروجی مقداری نیم نانو‌ثانیه طول کشید درحالی که وقتی پوینتر برمیگردوندیم ۱۵ میلی‌ثانیه! خب این برعکس چیزیه که قبلا فکر میکردیم. یکم بیشتر بررسیش کنیم.آیا فقط روش خروجی دادن مهمه؟ مقداری که خروجی میدیم چطور؟میخوایم تست رو تغییر بدیم. جای اینکه یک تایپ یکسان رو به ۲ شکل مختلف خروجی بدیم. دو تا تایپ رو به شکل مقداری خروجی میدیم. با این تفاوت که یکی از این تایپ‌ها توی خودش پوینتر داره. ببینیم خروجی چطور میشه:type SimpleReturn struct {
   Value int // using value
}
type SimpleReturnWithPointer struct {
   Value *int  // using pointer
}
func NewSimpleReturn(n int) SimpleReturnWithPointer {
   return SimpleReturnWithPointer{Value: &amp;n}
}
func NewSimpleReturnV(n int) SimpleReturn {
   return SimpleReturn{Value: n}
}سمت کد بنچمارک هم چنین چیزی داریم:var vSimpleReturnV SimpleReturn // all value
var vSimpleReturn SimpleReturnWithPointer // has pointer inside
func BenchmarkNewSimpleReturn(b *testing.B) {
   for i := 0; i &lt; b.N; i++ {
      vSimpleReturn = NewSimpleReturn(10)
   }
}
func BenchmarkNewSimpleReturnV(b *testing.B) {
   for i := 0; i &lt; b.N; i++ {
      vSimpleReturnV = NewSimpleReturnV(10)
   }
}اجرا کنیم ببینیم خروجی چطور میشه؟goos: linux
goarch: amd64
pkg: hello
BenchmarkNewSimpleReturn-8      83301891                15.1 ns/op
BenchmarkNewSimpleReturnV-8     1000000000               0.449 ns/op
PASSباز هم نتیجه مثل قبل شد. انگار پوینتر موقع کپی شدن داره اذیت میکنه. اما علت چیه؟چرا کپی کردن پوینتر کند تر از کپی کردن مقدار هست؟همونطوری که توی ویکی کد ریویو گولنگ میتونیم بخونیم تاکید داره برای ارسال مقادیر کوچیکی که به مقدارشون فقط نیاز داریم از پوینتر استفاده نکنیم.توی متغیرهای کوچیک (کمتر از ۳۲کیلوبایت) کپی کردن یک پوینتر تقریبا به اندازه کپی کردن مقدار اون متغیر هزینه داره (توی حافظه کش). پس از این جهت سودی نمیبریم.کامپایلر چک هایی رو تولید میکنه که موقع ران‌تایم زمان dereferencing پوینتر اجرا میشن.نکته‌ی دیگه ایی هم که هست پوینتر ها اکثرا توی Heap ذخیره میشن. برای مثال کدی که نوشتیم رو escape analysis کنیم. برای این کار از ابزار های  Go استفاده میکنیم ( go build -gcflags=&quot;-m&quot; main.go )type SimpleReturn struct {
   Value int
}
func NewSimpleReturn(n int) *SimpleReturn {
   return &amp;SimpleReturn{Value: n}
}
func NewSimpleReturnV(n int) SimpleReturn {
   return SimpleReturn{Value: n}
}

go build -gcflags=&amp;quot-m&amp;quot main.go     
...
./main.go:44:9: &amp;SimpleReturn literal escapes to heap
...همونطوری که آنالیز به ما میگه return &amp;SimpleReturn{Value: n} مقدارش در heap ذخیره میشه. اما اگر به صورت مقداری برگردونیم در stack ذخیره میشه. همونطوری که میدونیم ذخیره در stack بسیار بهینه تر هست. Garbage collector میاد heap رو چک میکنه و همونطوری که میدونیم هربار GC درحال بررسی هست به مدت چند میلی‌ثانیه کل سرویس ما فریز میشه. و میتونه مشکل هایی مثل Memory Leak و .. بوجود بیاد. ( در مورد این موضوع خیلی بیشتر میشه توضیح داد. کلا چرا باید سعی کنیم سمت stack کدمون اجرا شه، حقیقتا موضوع جذابیه و در قالب یک پست که چطور مموری لیک رو تشخیص دادیم و رفع کردیم در آینده حتما صحبت میکنیم). پوینتر نه تنها سود نداشت بلکه کلی ضرر داشت! پس کی پوینتر استفاده کنیم؟پوینتر رو وقتی استفاده میکنیم که واقعا بهش نیاز داریم. فرض کنید یک فایل ۱۰۰ مگابایتی رو توی یک متغیر لود کردید. منطقیه که برای استفاده کمتر از حافظه بهتره اون رو به صورت پوینتر پاس بدیم تا ازش کپی نسازیم. (طبق نیاز البته). یا برای مثال نیازه توی یک تابع روی متغیر تغییراتی ایجاد کنیم که خارج از تابع هم به اون تغییرات نیازه (کلا اساس پوینتر). اما وقتی پوینتر نیازی از ما رفع نمیکنه و صرفا فقط برای سریع تر شدن یا صرفه جویی حداقلی (چند کیلوبایتی) در حافظه بخوایم از پوینتر استفاده کنیم لزوما کار درستی نیست.</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Sat, 10 Apr 2021 13:15:44 +0430</pubDate>
            </item>
                    <item>
                <title>گروه‌بندی سریع کاربران با استفاده از Consistent Hashing</title>
                <link>https://virgool.io/golangpub/%DA%AF%D8%B1%D9%88%D9%87-%D8%A8%D9%86%D8%AF%DB%8C-%D8%B3%D8%B1%DB%8C%D8%B9-%DA%A9%D8%A7%D8%B1%D8%A8%D8%B1%D8%A7%D9%86-%D8%A8%D8%A7-%D8%A7%D8%B3%D8%AA%D9%81%D8%A7%D8%AF%D9%87-%D8%A7%D8%B2-consistent-hashing-vywr0vqggzcs</link>
                <description>فرض کنید میخواید یک امکان خیلی خاص به نرم‌افزارتون اضافه کنید که فقط برای بخشی از کاربران فعال باشه. یعنی اون کاربر هروقت سر زد به نرم‌افزار اون امکان رو ببینه. بتونید تعیین کنید چنددرصد کاربران این امکان براشون فعال باشه و اصلا نیاز نباشه چیزی رو توی دیتابیس ذخیره کنید و پدر سیستمتون دراد!یک مثال دیگه. فرض کنید ترافیک کاربر ها به سایت خیلی زیاده. میخوایم بگیم اطلاعات ۵۰ درصد کاربرها توی یک سرور و اطلاعات ۵۰ درصد دیگه ی کاربر‌ها توی یک سرور دیگه ذخیره شه. چطور این کارو انجام بدیم؟راهی سریع با دقت بالا.نکته‌ایی که هست جواب معمولی ایی که به ذهن اکثریت میرسه اینه خب یک دیتابیس داشته باشیم که بیاد بگه هر آی دی توی کدوم دیتابیس ذخیره شده (یعنی دیتابیس اول key value هست) و در نهایت با توجه به اطلاعات اون دیتابیس وصل شیم به دیتابیس اصلی‌ایی که دیتاهای کاربر توش ذخیره شده.یا برای مثال اول بیایم یه کوئری بزنیم رندوم ۲۰ درصد کاربر هامون رو انتخاب کنه. یک جدول هم داشته باشیم که مشخص کنیم هر کاربر آیا فلان فیچر براش فعال هست یا نه؟این دو تا راه حل توی شرایط خاص خودشون به کار میان اما ۲ تا مشکل دارن. دیتای زیادی داریم ذخیره میکنیم، ریکوئست های زیادی به دیتابیس هامون میزنیم و برای یک سیستم لبه (edge) که قراره صرفا درخواست های کاربر ها رو بین چند تا سیستم توزیع کنه یکم بار اضافی داره.استفاده از Consistent Hashingبخوام خلاصه بگم Consistent Hashing یا هش ثابت! چیه؟ فرض کنید یک تابع هش داریم که بهش میتونیم یک id به صورت string یا هر چیزی بدیم و در نهایت به ما خروجی یک عدد بده. مثلا آی‌دی کاربر من یک uuid به شکل 71be0578-6c6c-11eb-9439-0242ac130002 هست. ما این آی‌دی رو به این تابع میدیم و به ما خروجی یک عدد میده (مثلا ۲۹۳۲۰).حالا ما میدونیم کلا ۲ تا سرور داریم و اطلاعات این کاربر توی یکی از این دو سروره. با خودمون قرارداد میکنیم که اگه خروجی تابع هش ما زوج بود اطلاعات این کاربر توی سرور یک و اگر فرد بود توی سرور دوم هست. پس اطلاعات 71be0578-6c6c-11eb-9439-0242ac130002 توی سرور اول هست. پس وصلش میکنیم به سرور اول.حالا نکته قشنگ این کار کجاست؟ اگر یک تابع هش سریع داشته باشیم که بدونیم توزیع یکنواخت داره، میتونیم به صورت یک نواختی بار رو بین چندین سرور پخش کنیم، یا مسئله اول (A/B تست) رو بدون نیاز به ذخیره اطلاعات اضافی حل کنیم.حل مسئله اول: این مسئله که یکی از استفاده های A/B تست هست داره میگه ما میخوایم یک فیچر رو برای یک درصد خاصی از کاربران فعال کنیم. کافیه چیکار کنیم؟ آی‌دی هر کاربر رو بگیریم و بدیم به تابع هشمون.خروجی تابع هش رو باقیمانده تقسیم بر ۱۰۰ (mod 100) بگیریم.حالا اگر قراره به N درصد کاربران اون فیچر رو نشون بدیم، اگر باقیمانده تقسیم کمتر از N بود به کاربر نشون میدیم درغیراینصورت نشون نمیدیم.حل مسئله دوم: این مسئله که یکی از کاربرد های sharding هست (که دیتابیس های مختلف این رو خیلی خوب پیاده کردن) هم مثل مسئله اول هست. برای مثال اگر ۵ تا دیتابیس داریم، برای اینکه تصمیم بگیریم اطلاعات کاربر قراره کجا ذخیره بشه کافیه باقیمانده تقسیم خروجی تابع هش رو بر ۵ بگیریم. این عدد شناسه سرور رو به ما میده.تابع هش fnvهش فانکشنی که نیاز داریم برای این کار باید چند تا خصوصیت داشته باشه:سریع باشهتوزیع یکنواخت داشته باشهروی سرورهای مختلف (سیستم عامل های ۳۲ یا ۶۴ بیت ایی) خروجی یکسان داشته باشه.یکی از این توابع، تابع fnv هست که ورژن ۳۲ بیتیش هم به شدت سریعه هم کار مارو راه میندازه.من یک نمونه تست براش با go نوشتم که هم سرعتش رو چک کنیم هم یکنواختیش رو.تست اول: توی این تست میخوایم سرعت این تابع رو بررسی کنیم:func HashFunction(str string, serverCount int) int {
   h := fnv.New32()
   h.Write([]byte(str))
   return int(int(h.Sum32()) % serverCount)
}با استفاده از Go یک بنچ مارک هم نوشتم که بررسی کنه سرعت این کد رو:func BenchmarkHashFunction(b *testing.B) {
   for i := 0; i &lt; b.N; i++ {
      str := &amp;quotuserid&amp;quot + strconv.Itoa(i)
      HashFunction(str, 10)
   }
}خروجی:BenchmarkHashFunction-8         10057630               118 ns/op
PASS
ok      hello   1.307sهمونطوری که می‌بینید اجرای تابعمون فقط ۱۱۸ نانوثانیه طول میکشه. حالا اگر برای این کار از دیتابیس و ... استفاده میکردیم در بهترین حالت چند صد میکروثانیه بود. (به علاوه استفاده زیاد از IO)تست دوم: توی این تست یکنواختی توزیع تابع رو بررسی میکنیم:func main() {
   m := make(map[int]int)
   for i := 0; i &lt; 100000; i++ {
      id := &amp;quotuser&amp;quot + strconv.Itoa(i)
      m[HashFunction(id, 5)]++
   }
   fmt.Println(m)
}کد رو اجرا میکنم:map[0:20033 1:19996 2:19944 3:19984 4:20043]همونطوری که توی خروجی میبینید به شکل قابل قبولی خروجی یکنواخت بوده.</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Thu, 11 Feb 2021 17:25:17 +0330</pubDate>
            </item>
                    <item>
                <title>پایپ‌لاین (Pipeline) در ردیس، فشار کمتر و سرعت بیشتر</title>
                <link>https://virgool.io/@mhrlife/%D9%BE%D8%A7%DB%8C%D9%BE-%D9%84%D8%A7%DB%8C%D9%86-pipeline-%D8%AF%D8%B1-%D8%B1%D8%AF%DB%8C%D8%B3-%D9%81%D8%B4%D8%A7%D8%B1-%DA%A9%D9%85%D8%AA%D8%B1-%D9%88-%D8%B3%D8%B1%D8%B9%D8%AA-%D8%A8%DB%8C%D8%B4%D8%AA%D8%B1-n4y0wuqqhvrw</link>
                <description>همین اول بگم، همه چیز به دیزاین و طراحی سیستم برمیگرده. اما بهتره ابزارهایی که داریم رو بیشتر بشناسیم تا توی طراحی‌ها با تسلط بیشتری بهشون نگاه کنیم. توی این مقاله میخوام با Pipeline ها در ردیس آشنا شیم، یکم با Go کد بزنیم و بنچ‌مارک بگیریم.با پایپ‌لاین، الکی صبر نمیکنیم.پایپ‌لاین چیست؟توی سایت ردیس چنین تعریفی اومده که &quot;بعضی وقت ها یک سری درخواست میخوای به سرور بفرستی که برای مثال کامند دوم برای اجراش نیاز به پاسخ کامند اول نداره ، در نهایت جواب همه کامند ها رو با هم میگیری&quot; برای مثال من میخوام به ردیس بگم که یک کاربر اضافه کن (با Map)، وقتی اضافه شد نام کاربریش رو عوض کن، حالا برو کاربر دوم رو اضافه کن و بعد نام کاربریش رو عوض کن.اگه به صورت معمولی به ازای اضافه کردن کاربر، تغییر نام‌کاربریش ، اضافه کردن کاربر دوم و تغییر نام‌کاربریش ۴ تا درخواست به ردیس میفرستم. اگه پینگ سرورم به ردیس ۵۰ میکروثانیه باشه درخواست دوم منتظر اتمام درخواست اول (۵۰ میکروثانیه) میمونه و بعد درخواست خودش رو میفرسته. همینطور درخواست سوم منتظر دوم میمونه و درخواست چهارم منتظر درخواست سوم. پس این وسط ۲۰۰ میکروثانیه رو هدر دادیم. این عدد زیاد بزرگ نیست. اما اگه هزار تا درخواست بخوایم بدیم چند میکروثانیه رو هدر دادیم؟ ۵۰*۱۰۰۰ که میشه ۵۰ میلی‌ثانیه. حالا ما اگه در لحظه هزار درخواست به ردیس داشته باشیم چه اتفاقی میوفته؟ ۵۰ میلی‌ثانیه پاسخ هامون کند‌تر میشه.تا الان از جنبه RTT یا همون Round-trip delay به قضیه نگاه کردیم. اما فقط این نیست! به ازای هر درخواست که به ردیس زده میشه یک syscall از نوع read یا write داریم. اگه همه درخواست‌های Read یا Writeمون رو باهم بفرستیم جای هزار syscall فقط یک syscall از اون نوع داریم که پرفورمنس رو خیلی بهتر میکنه و فشار روی سرور کمتر میشه.مثال بزنیم! چی رو دقیقا پایپ‌لاین کنیم؟یک سیستم وبلاگی رو درنظر بگیرید. در صفحه اول قراره ۱۰۰ پست برتر هرساعت رو نشون بدیم. میخوایم یک طراحی ساده برای این سیستم داشته باشیم طوری که بدونیم هم Responsive هست ( اگه تغییری روی عکس /عنوان/خلاصه یکی از پست ها بوجود اومد توی یک بازه زمانی قابل قبولی تغییرات اعمال بشه ). همینطور برای ما خیلی مهمه سریع باشه و درخواست های زیادی رو بتونه هندل کنه. (مثلا هر پاد ۱۰ هزار درخواست درثانیه رو هندل کنه خیلی خوب میشه)ما میخوایم لایه‌ی محتوا رو تبدیل به ۳ زیرلایه کنیم. لایه اول که دیتاها رو توی دیتابیس ذخیره میکنه. لایه دوم کش دیتاهای توی دیتابیس که هر ساعت تغییرات رو از دیتابیس میخونه. لایه سوم کش توی مموری هر پاد که با TTL ده دقیقه دیتایی که از ردیس گرفته رو توی مموی ذخیره میکنه.توی این مقاله ارتباط بین لایه کش ردیس و دیتابیس رو توضیح دادم که چطور ۸۰۰ ریکوئست برثانیه رو برسونیم ۱۴هزار تا. توی این تمرکزمون روی ارتباط بین لایه کش In-Memory و Redis هست.ردیس هم کش In-Memory هست. پس چرا دارم با یک کش In-Memory دیگه مقایسش میکنم؟من توی این مقاله و مقاله قبلی در مورد این موضوع صحبت کردم که بهتره جای اینکه تک تک درخواست بزنیم که اطلاعات فلان پست رو بده، توی یک بازه ایی همه پست ها رو با هم بگیریم. اما یه جا آخر توی سیستم باید من بگم اطلاعات پست ۲۳۱ رو بهم بده؟ بهترین جا برای این کار کش In-Memory هر پادمون هست. چون دیگه RTT ایی درکار نیست. دیتا توی خود مموری سیستم هست و تک تک میتونیم به مموری سیستم درخواست بزنیم.اینطور در نظر بگیرید. روی لبه سیستم ما ۴ تا سرور داریم که درخواست های کاربر ها رو میگیرن. این ۴ تا سرور مموری‌شون با مموری کش Redis یکی نیست. چون Redisرو توی یک کلاستر سرور جدا داریم. حالا وقتی ۱۰ تا درخواست به یکی از این ۴ تا سرور رفت جای اینکه به ازای هر درخواست یکی یکی به ردیس Requestبزنن که اطلاعات فلان پست رو بده، نگه میداره و اون ۱۰ تا درخواست رو با هم میفرسته. وقتی از ردیس Responseرو گرفت اون رو توی مموری خودش ۱۰ دقیقه نگه میداره و طی اون ۱۰ دقیقه هر درخواست جدیدی اومد یکی یکی توی مموری خودش چک میکنه. اینطور سیستم ما کاملا Scalable هست.یکم مقایسه کنیم(Benchmark)من روی سیستم یک سرور ردیس بالا اوردم. قراره این سناریو رو با زبون Go پیاده‌سازی کنیم. یک دیتابیس Redis داریم که با استفاده از دیتاتایپ Hash اطلاعات کاربر ها رو ذخیره میکنیم. ( مثلا یک کاربر میتونه نام، ایمیل و سن داشته باشه). نیاز داریم نام و ایمیل ۱۰۰۰ تا کاربر رو داشته باشیم. توی حالت اول یکی یکی ریکوئست میزنیم(میگیم نام و ایمیل فلان کاربر رو بده) و توی حالت دوم ۱۰۰۰ تا رو با هم میفرستیم سمت ردیس.کد اول، ساختن افراد توی دیتابیس:func Create(rdb *redis.Client) {
   for i := 0; i &lt; 10000; i++ {
      rdb.HMSet(context.Background(), &amp;quotuser:&amp;quot+strconv.Itoa(i), &amp;quotuser&amp;quot, &amp;quotMohammad&amp;quot, &amp;quotage&amp;quot, 21, &amp;quotemail&amp;quot, &amp;quotexample@gmail.com&amp;quot)
   }
}یک نمونه کامند تولیدیش برای ردیس:HMSET user:12 user Mohammad age 21 email example@gmail.comکد دوم، دریافت تکی اطلاعات کاربرهاfunc FindNormal(rdb *redis.Client) {
   for i := 1000; i &lt; 2000; i++ {
      rdb.HMGet(context.Background(), &amp;quotuser:&amp;quot+strconv.Itoa(i), &amp;quotuser&amp;quot, &amp;quotemail&amp;quot)
   }
}نمونه کامندش:HMGET user:12 user emailکد سوم، دریافت با استفاده از پایپ‌لاینfunc FindBatch(rdb *redis.Client) {
   p := rdb.Pipeline()
   for i := 1000; i &lt; 2000; i++ {
      p.HMGet(context.Background(), &amp;quotuser:&amp;quot+strconv.Itoa(i), &amp;quotuser&amp;quot, &amp;quotemail&amp;quot)
   }
   p.Exec&#40;context.Background(&#41;)
}خب، وقت اجرا و بنچ‌مارک گرفته: (گرفتن اطلاعات هزار کاربر)Pipeline took            2.481666ms
Single requests took     31.444584msخروجی همونطوری بود که انتظارش رو داشتیم. با پایپ‌لاین ۲ میلی‌ثانیه طول کشید درحالی که اگر درخواست‌های تکی بزنیم ۳۰‌ ‌میلی‌ثانیه!ما که این همه راه اومدیم. توی ردیس یک میلیون رکورد میریزم و ۱۰ هزار تا همزمان بگیریم ببینیم چطور میشه. ( که پیش‌نیاز سیستمی که طراحی میخواستیم بکنیم رو برآورده کرده باشیم)Pipeline took            22.51976ms
Single requests took     288.796202msبنظر منطقی میاد که حتما پایپ‌لاین ها رو برای طراحی در نظر بگیریم D:یک نمونه کد Goاگه Go کار میکنید احتمالا داکیومنت زیادی برای پایپ‌لاین ها توی Go پیدا نمیکنید. این نمونه کد رو هم میذارم نشون بدم چطور اطلاعات رو میشه دریافت کرد و توی تابع خروجی داد:func FindBatchUsers(rdb *redis.Client, ctx context.Context, fields []string, userIds []int) (map[int]map[string]string, error) {
   p := rdb.Pipeline()
   resps := make([]*redis.SliceCmd, 0, len(userIds))
   for _, id := range userIds {
      resps = append(resps, p.HMGet(ctx, &amp;quotuser:&amp;quot+strconv.Itoa(id), fields...))
   }
   _, err := p.Exec&#40;ctx&#41;
   if err != nil {
      return nil, err
   }
   ret := make(map[int]map[string]string)
   for i, id := range userIds {
      resp := resps[i]
      if resp.Err() != nil {
         ret[id] = nil
      } else {
         ret[id] = map[string]string{}
         for j, field := range fields {
            ret[id][field] = resp.Val()[j].(string)
         }
      }
   }
   return ret, nil
}</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Mon, 18 Jan 2021 16:45:24 +0330</pubDate>
            </item>
                    <item>
                <title>چرا تک کوئری های SELECT در SQL لزوما کار درستی نیست؟</title>
                <link>https://virgool.io/@mhrlife/%DA%86%D8%B1%D8%A7-%D8%AA%DA%A9-%DA%A9%D9%88%D8%A6%D8%B1%DB%8C-%D9%87%D8%A7%DB%8C-SELECT-%D8%AF%D8%B1-SQL-%D9%84%D8%B2%D9%88%D9%85%D8%A7-%DA%A9%D8%A7%D8%B1-%D8%AF%D8%B1%D8%B3%D8%AA%DB%8C-%D9%86%DB%8C%D8%B3%D8%AA%D8%9F-otnaonk0xixr</link>
                <description>فرض کنید یک سیستم دارید طراحی میکنید که قرار توی آدرس site.com/post/12 پست‌ایی با آی‌دی ۱۲ رو نشون بده.(طی این آموزش فرض کش کردن رو کلا در نظر نمیگیریم، اما در آخر توضیح میدم چطور این کار خیلی برای کش هم بهینه‌تر هست) احتمالا کاری که میکنید این هست توی اون route کوئری زیر رو اجرا میکنید:SELECT post_title, post_content FROM posts WHERE id=12 LIMIT 0,1;برای راحتی این آموش من مثال پست رو در نظر نمیگیرم. فرض کنید یک سایت کوتاه کننده لینک دارید میزنید، اینطور که هر لینک یک شناسه داره ( site.com/xgSy ) و توی دیتابیستون جدولی دارید که دو سطر random_key و url داره (random_key رو ایندکس میکنیم) که از طریق اون random_key به url اصلی دسترسی پیدا میکنیم و کاربر رو به اون آدرس redirect میکنیم. خب حالا فرض کنید یک route نوشتید برای این کار و xgSy رو از کاربر گرفتید، حالت خیلی ساده اینه کوئری پایین رو اجرا کنیم:SELECT url FROM urls WHERE random_key=xgSy LIMIT 0,1;اگه تست اش کنید می‌بینید خیلی خوب داره جواب میده، کوئری در حد میکروثانیه طول میکشه. سوالی که مطرح میشه، آیا واقعا همه چیز خوبه؟ باید بنچ‌مارک بگیریم!من توی دیتابیس ام ۱۰ میلیون ریکورد تصادفی تولید کردم و با go یک کد خیلی ساده نوشتم که یک route به شکل /single داشته باشیم که هر ریکوئستی که بهش زده شد یک آی دی تصادفی تولید کنه و اون رو از دیتابیس بگیره. کدش به شکل زیره:func getSingle(db *gorm.DB) func(c echo.Context) error {
   return func(c echo.Context) error {
      id := rand.Intn(10000000)
      var url URL
      tr := db.Where(&amp;quotrandom_key = ?&amp;quot, id).First(&amp;url)
      if tr.Error != nil {
         fmt.Println(tr.Error)
         return c.String(http.StatusInternalServerError, url.URL)
      }
      return c.String(http.StatusOK, url.URL)
   }بنچ مارک اولی که میگیرم برای ۲۰ هزار ریکوئست ولی با کانکارنسی ( ریکوئست های همزمان ) ۲۰ هست. یعنی همزمان فقط ۲۰ تا ریکوئست سمت سایت ما میاد. ( برای این کار هم از Apache benchmark استفاده میکنم )$ ab -n 20000 -c 20 http://localhost:1323/single   
Requests per second:    4076.20 [#/sec] (mean)
Time per request:       4.907 [ms] (mean)
Time per request:       0.245 [ms] (mean, across all concurrent requests)
Percentage of the requests served within a certain time (ms)
  50%      5
  66%      6
  75%      6
  80%      6
  90%      7
  95%      8
  98%     10
  99%     12
 100%     23 (longest request)با این بار کم تونستیم ۴ هزار ریکوئست برثانیه رو هندل کنیم! عدد خوبیه. 99th percentile مون هم ۱۲ میلی ثانیست با میانگین ۵ میلی‌ثانیه. عدد های خوبی هستن. اگه نیازمون در همین حده ( یعنی نهایتا ۲۰ ریکوئست همزمان ) که این روش جوابه و دیگه نیازی به تغییر نیست. اما اگه همزمان ۲ هزار ریکوئست رو قرار بود هندل کنیم چه اتفاقی میوفته؟ ارور میخوریم و دیتابیس پاسخگو نیست D: علت چیه؟ توی MySQL ما یک limit داریم روی تعداد کانکشن های همزمان. کامند زیر رو اگه اجرا کنید احتمالا بهتون ۱۵۰ این حدودا میده. یعنی پیشفرض دیتابیس ۱۵۰ کانکشن همزمان رو پشتیبانی میکنه.mysql&gt; show variables like &#039;max_connections&#039;;اما میتونیم این عدد رو بیشتر کنیم تا بتونیم تست کنیم واقعا سیستم ما روی بار زیاد چطور عمل میکنه؟mysql&gt; SET GLOBAL max_connections = 2500;الان مطمئن شدیم که دیتابیسمون این حجم از کانکشن همزمان رو پشتیبانی میکنه. دوباره بنچ‌مارک بگیریم:Requests per second:    872.82 [#/sec] (mean)
Time per request:       2291.436 [ms] (mean)
Time per request:       1.146 [ms] (mean, across all concurrent requests)
Percentage of the requests served within a certain time (ms)
  50%     79
  66%    118
  75%    210
  80%    502
  90%   1114
  95%   1177
  98%   3070
  99%   3159
 100%   7319 (longest request)این پرفورمنس چیزی نیست که ما بخوایم. ریسپانس تایم برای 99th percentile روی ۳ ثانیست! اما مشکل این حرکت چیه؟اگه قرار باشه ارتباط ما با دیتابیس ۵۰۰ میکروثانیه طول بکشه، برای ۲ هزار کانکشن همزمان ایی که داریم حدود  یک ثانیه رو از دست میدیم.وقتی میخوایم چند تا ریکوئست رو در لحظه بگیریم، چرا چند تا چند تا بفرستیم سمت دیتابیس؟ همه رو با هم بفرستیم!دریافت اطلاعات از دیتابیس به شکل دسته‌ایی (Batch)مسئله رو دوباره مطرح کنم. فرض کنید یک سیستم دارید با لود بالا که در لحظه چند هزار درخواست با هم میاد. حالا ما بیایم یه کاری کنیم، تمام اون درخواست ها رو با هم بفرستیم سمت دیتابیس. دیگه تک تک نفرستیم. برای این کار یک بنچ مارک ریز گرفتم. اگه شما ۱۰ هزار تا SELECT تکی بزنید به دیتابیس حدود ۱.۷ثانیه طول میکشه. اما اگه همه رو با هم بگیری ۷۰میلی‌ثانیه طول میکشه.اگه دقیق تر بخوایم بررسی کنیم کلمه در لحظه گنگ هست. یعنی شاید فاصله بین دو ریکوئست چند هزارم ثانیه باشه، ایا این ها در لحظه هستن؟ ما باید یک پنجره زمانی مشخص کنیم. هر ریکوئستی که توی اون پنجره زمانی اومد با بقیه ریکوئست های اون پنجره در لحظه در نظر میگیریم. این طبیعتا یک delay به سیستم ما اضافه میکنه. یعنی اگه اون پنجره رو ۵۰ میلی‌ثانیه در نظر بگیریم، هر میانگین پاسخ ما به سیستم حدود ۵۰ میلی‌ثانیه هست.این نکته رو باید در نظر داشت. این سیستم ایی که پیاده میکنیم سیستمی نیست که در مرز سیستم با کاربر در ارتباطه، سیستمی که طراحی میکنیم معمولا provider برای cache هست که از دیتابیس اطلاعاتی که توی کش نیست رو میریزه توی کش. یعنی اگر کاربر ما یک صفحه رو میخواد ببینه و توی کش نیست به این سیستم میگیم فلان آی دی توی کش نیست برای ما provide کن و کاربرای بعدی دیگه دیتا رو از cache میخونن.برای پیاده سازی سیستم من از زبان Go استفاده میکنم ( یکم هم قدرت Go رو توی طراحی سیستم ها نشونتون بدم :) )اول یک دور ببینیم چه چیزی میخوایم طراحی کنیم:توی Route برنامه هر آی‌دی جدیدی که دریافت کردیم به سیستممون میدیم که فلان آی‌دی رو به من بده. اون در جواب به من یک چیزی (توی Go ما با Channel ها سروکار داریم) میده که میتونیم روش Listen کنیم و تا وقتی که جواب توش ریخته میشه ترد (Thread) فعلی برناممون که میخواد پاسخ HTTP رو به کاربر بده متوفق میشه. مثل پترن Async/Await. از اونجایی که من نمیخوام وارد جزئیات Go بشیم مفهوم این بخش رو اینطور در نظر بگیرید. ما یک متغیر داریم که خالیه. میتونیم بگیم تا وقتی پر نشده ترد ما بلاک شه.حالا سیستم ما طوری پیاده سازی شده که در لحظه آی‌دی های جدید رو میگیره، هر ۲۰ میلی‌ثانیه یک بار (همون پنجره زمانی که توضیح دادم) تمام آی‌دی هایی جدیدی که گرفته رو با یک کوئری از دیتابیس میگیره.حالا که تک‌تک جواب ها رو داریم به ترتیب توی اون متغیر هایی که در مرحله اول ساخته بودیم میریزیم، اون ترد چون دیتارو گرفته ادامه پیدا میکنه و HTTP Response رو به کاربر میده.یک نمونه کد و بنچ مارکبرای این موضوع من یک نمونه کد Go آماده کردم که روش بنچ‌مارک بگیرم.func NewSQLProvider(db *gorm.DB) *SQLProvider {
   sp := &amp;SQLProvider{
      fetch:        make(chan int, MaxSPS*10),
      promises:     make(map[int][]chan&lt;- *URL),
      promisesLock: sync.Mutex{},
      db:           db,
   }
   go sp.Work()
   return sp
}تایپ SQLProvider ما یک چنل به نام fetch داره که آی‌دی هایی که قرار بود Provide شن رو توی خودش نگه‌میداره. همونطوری که گفتیم سیستم ما دو بخش هست. بخشی که درخواست های URL های جدید رو براساس آی دی دریافت میکنه و بخش دوم که آی‌دی ها رو با هم از دیتابیس میگیره.برای طراحی بخش اول ما یک مپ به اسم promises داریم. هر وقت متد Provide اجرا میشه یک چنل میسازیم که اون سمت Route ما روش Listen کنه و درعین حال ما توی مپ promises با آی‌دی لینک نگهش میداریم که بعدا که Work اون لینک رو از دیتابیس گرفت، از طریق مپ بهش دسترسی داشته باشیم و دیتا رو بریزیم توش و Route ما پاسخ بده:func (sp *SQLProvider) Provide(id int) &lt;-chan *URL {
   sp.promisesLock.Lock()
   defer sp.promisesLock.Unlock()
   promise := make(chan *URL, 1)
   if _, founded := sp.promises[id]; !founded {
      sp.promises[id] = make([]chan&lt;- *URL, 0)
   }
   sp.promises[id] = append(sp.promises[id], promise)
   sp.fetch &lt;- id
   return promise
}و در نهایت Work ما که هر لحظه چک میکنه که از آخرین باری که Provide کرده چقدر گذشته که اگه اون پنجره زمانی به اتمام رسید یا به حداکثر ظرفیت رسیدیم شروع کنه Provide کردن.func (sp *SQLProvider) Work() {
   ch := sp.fetch
   lastFetch := time.Now()
   for {
      // if true then we reached time or size limit
      if len(ch) &gt; MaxSPS || time.Now().Sub(lastFetch) &gt; (MaxSelectWait*time.Millisecond) {
         fLen := int(math.Min(float64(len(ch)), MaxSPS))
         lastFetch = time.Now()
         if fLen == 0 {
            continue
         }
         // make array of url ids
         ids := make([]int, 0, fLen)
         for i := 0; i &lt; fLen; i++ {
            select {
            case id := &lt;-ch:
               ids = append(ids, id)
            }
         }
         // run provider
         var urls []*URL
         sp.db.Where(&amp;quotrandom_key IN ?&amp;quot, ids).Find(&amp;urls)
         providedUrls := make(map[int]bool)
         // response provided urls
         for _, url := range urls {
            sp.promisesLock.Lock()
            if cc, founded := sp.promises[url.RandomKey]; founded {
               providedUrls[url.RandomKey] = true
               sp.response(cc, url)
            }
            sp.promisesLock.Unlock()
         }
         // response not founds
         for _, id := range ids {
            if _, provided := providedUrls[id]; !provided {
               if cc, founded := sp.promises[id]; founded {
                  sp.response(cc, nil)
               }
            }
         }
      }
   }
}
func (sp *SQLProvider) response(cc []chan&lt;- *URL, url *URL) {
   for _, c := range cc {
      go func(c chan&lt;- *URL, url *URL) {
         select {
         case c &lt;- url:
         default:
         }
      }(c, url)
   }
}و در نهایت Route ایی که باهاش بنچ‌مارک میخوایم بگیریم:// Handler
func getBatch(db *gorm.DB) func(c echo.Context) error {
   sp := NewSQLProvider(db)
   return func(c echo.Context) error {
      id := rand.Intn(10000000)
      urlPromise := sp.Provide(id)
      select {
      case url := &lt;-urlPromise:
         return c.String(http.StatusOK, url.URL)
      case &lt;-time.After(1 * time.Second):
         fmt.Println(&amp;quotError &amp;quot, id)
         return c.String(http.StatusNotFound, &amp;quotNot founded :(&amp;quot)
      }
   }
}خب، بنچ‌مارک بگیریم؟Requests per second:    14361.56 [#/sec] (mean)
Time per request:       139.261 [ms] (mean)
Time per request:       0.070 [ms] (mean, across all concurrent requests)
Percentage of the requests served within a certain time (ms)
  50%    131
  66%    140
  75%    147
  80%    152
  90%    165
  95%    190
  98%    209
  99%    215
 100%    258 (longest request)رسیدیم به ۱۴هزار ریکوئست در ثانیه که هم open connection های دیتابیسمون خیلی کم شد ( از ۲۵۰۰ رسیدیم به یک!) و هم درخواست های بیشتری رو میتونیم هندل کنیم، و هم از لحاظ زمانی 99th percentile سیستم خیلی بهتر عمل میکنه و از همه مهم تر، سیستم رو به امان خدا ول نکردیم، میتونیم به بهترین شکل Tune کنیم و کاملا به سیستم مسلط هستیم.در ادامه چه کنیم؟دو تا نکته هست. احتمالا وقتی به چنین طراحی ایی نیاز دارید روی سیستم بزرگی کار میکنید که شامل چندین سرویس مختلف هست. چطور قرار این سیستم رو بشکونیم به دو سیستم؟ انشالله توی مقاله ایی در آینده یک طراحی خفن با RabbitMQ میریم که بتونیم سیستم رو بشکونیم به ۲ سرویس.باز هم تاکید کنم، این سیستم بیشتر برای Provide کش هستش و از اونجایی که همیشه میخوایم Response Time زیر ۵ ثانیه باشه باید حتما پاسخ ها کش بشه.</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Sat, 02 Jan 2021 14:44:48 +0330</pubDate>
            </item>
                    <item>
                <title>استفاده از ردیس برای کش پست‌های هر تگ.</title>
                <link>https://virgool.io/@mhrlife/%D8%A7%D8%B3%D8%AA%D9%81%D8%A7%D8%AF%D9%87-%D8%A7%D8%B2-%D8%B1%D8%AF%DB%8C%D8%B3-%D8%A8%D8%B1%D8%A7%DB%8C-%DA%A9%D8%B4-%D9%BE%D8%B3%D8%AA%E2%80%8C%D9%87%D8%A7%DB%8C-%D9%87%D8%B1-%D8%AA%DA%AF.-lf3qf5ottp5q</link>
                <description>یکی از چالش هایی که جدیدا بهش برخوردیم این بود که براساس تگ هایی که یک سری پست دارن اون ها رو نشون ندیم، بولد (Bold) نشون بدیم و ... برای این کار یک دیتابیس داریم که لیست تگ‌ها رو توی خودش نگه میداره و یک دیتابیس دیگه که با Many-to-many رابطه بین پست و تگ ها رو ذخیره میکنه. تا اینجای قضیه سادست. اما از اونجایی که درخواست های زیادی سمت وبسایتمون میاد باید حتما یک کش داشته باشیم که دیتابیسمون به رحمت خدا نره.ردیس.چرا از ردیس استفاده میکنیم؟برای کش کردن کلی راه حل مثل Redis, memcached و... هست. اما تفاوتی که ردیس داره اینه که دیتاتایپ هایی به ما میده که بتونیم طبق نیازی که داریم ازشون استفاده کنیم. توی این مطلب ویرگول با Geo-spatial ردیس کار کردیم و نشون دادم چه امکانات خفنی ردیس میتونه به ما بده.حالا که معلوم شد چرا از ردیس استفاده میکنیم بیاید با هم مشکلمون رو بررسی کنیم و براساس ابزار هایی که ردیس بهمون میده چند تا راه حل پیدا کنیم و با بنچ مارک گرفتن بهینه ترینشون رو انتخاب کنیم.مسئله ما اینه که یک لیست از پست آی‌دی داریم (۱۰۰ تا پست) و میخوایم چک کنیم برای تک تک اون آی‌دی ها که ۴ تا تگ مشخص رو دارن یا نه.راه حل های ابتدایی و بدرد نخوربدرد نخور لفظ خوبی نیست! اما خب برای این همه بار اضافه ایی که این راه حل ها میارن حرف خوبیه. هر دیتاتایپ ایی جای خودش به کار میاد. قرار نیست همه چیز با یک ارایه خشک و خالی حل شه.راه حل ابتدایی اولیه اینه که کل آی‌دی های پست های هر تگ رو توی یک آرایه بریزیم و برای هر بار چک کردن هم از اول تا آخر ارایه بریم که پیچیدگی زمانی N داره و به درد این کار نمیخوره.راه حل ابتدایی دوم اینه ارایه ایی که آی دی ها ریخته میشه توش رو Sort کنیم که پیچیدگی زمانی دریافت لگاریتمی میشه. اما خب چه کاریه اینقدر پیچیدش کنیم!ساده‌ترین راه حل،‌ ترکیب شناسه‌ پست با تگ در Key ردیس.احتمالا ساده‌ترین راه حلی که به ذهن همه میاد اینه که یک ترکیب مثل postid:tagid توی ردیس ذخیره کنن. حالا هر بار نیاز بود چک کنن پست X آیا تگ Y رو داره یا نه، باید چک کنن که کلید x:y وجود داره توی ردیس ما یا نه. قبل از هر تصمیمی بهتره یک بنچ‌مارک ازش بگیریم. من یک اسکریپت گو نوشتم که برای ۱۰۰ تا تگ و ۴۰۰ هزار تا پست دیتا تولید میکنه. ( در مجموع میشه ۴۰ میلیون داده توی ردیس ). همچنین یک اسکریپت دیگه هم داریم که برای ۱۰۰ تا پست چک کنه که ۴ تا تگ رو دارن یا نه.اجرای اسکریپت افزودن دیتا یک دقیقه و ۳۸ ثانیه طول کشید.حجم مموری ردیس بعد از افزودن دیتای: ۳.۱۳گیگاجرای اسکریپت دریافت ۴ تگ برای ۱۰۰ تا پست: ۱.۲۴ میلی‌ثانیه. ( با استفاده از MGET )چون حالت دیگه ایی نداریم که مقایسه کنیم عددا یکم برامون گنگ هستن. ۳ گیگ خوبه یا نه؟ بهتر راه حل های دیگه رو هم چک کنیم.استفاده از هش ردیسیکی از دیتاتایپ های خوب ردیس هش ها هستن. هش مثل مپ هستش. یک نمونش اینه یک کلید به اسم user:11 داشته باشیم که هش باشه و مپ اش به شکل name:Mohammad age:22 و.. باشه.‍‍HMSET user:1000 username antirez password P1pp0 age 34حالا ما چطور ازش استفاده میکنیم؟ یک کلید میسازیم برای هر تگ و به اون هشمون تک تک پست هایی که اون تگ رو دارن با مقدار 1 اضافه میکنیم. مثلا اگه تگ free داشته باشیم با پست های 5,12 و 21 دستور ردیسمون اینطور میشه:HMSET tag:free 12 1 5 1 21 1توی دستور بالا میگیم یک هش به اسم tag:free بساز که به ترتیب 12,5,21 مقدارشون یک باشه.ما برای این کار دو تا راه حل داشتیم، کلید ما تگ باشه و هش ما پست ها، یا کلید پست باشه و هش ما لیست تگ های اون پست. چرا راه اول رو انتخاب کردیم؟برای اینکه علت رو دقیق تر متوجه بشیم باید صورت مسئله رو یک بار دیگه بخونیم. یک لیست ۱۰۰ تایی داریم و میخوایم برای هر تگ چک کنیم که آیا تک تک این پست ها فلان تگ رو دارن یا نه. همچنین باید در نظر داشته باشیم برای دریافت اطلاعات از هش ما نمیخوایم یکی یکی چک کنیم که تگ فلان پست فلان رو داره یا نه. ما میخوایم همزمان بگیم که یک پست اون ۴ تا تگ رو داره یا یک تگ اون ۱۰۰ تا پست رو داره یا نه؟ حالا اگه کلید ما پست آی دی باشه و هش ما تگ های ما، باید ۱۰۰ تا چک برای ۴ تا تگ داشته باشیم در حالی که الان با این روش ۴ تا چک برای ۱۰۰ تا پست رو داریم.حالا که متوجه کلیت این روش شدیم بهتره بریم سراغ بنچ مارک گرفتن.اسکریپت افزودن دیتاها ۴۰ ثانیه طول کشید.حجم مموری گرفته شده ۲.۱ گیگ بود.اسکریپت دریافت اطلاعات از کش ۸۵۰ میکروثانیه یعنی کمتر از یک میلی‌ثانیه طول کشید.این روش خیلی سریع تر بود. اما مشکلی که هست اینه میخوایم سعی کنیم اگر امکانش هست حافظه کمتر از ۲ گیگ باشه.یک دیتاتایپ ایی که هنوز در موردش صحبت نکردیم ست ها (Sets) هستند. اگه درس ساختمان داده رو خونده باشید میدونید ست ها از هش توی خودشون استفاده میکنن. اما برعکس هش‌ایی که توی ردیس داشتیم قرار نیست دیگه دیتای اضافی ایی که به هر آیتم بدیم. توی ست میتونیم چک کنیم که آیا یک آیتم عضو ست هست یا نه.استفاده از ست‌های ردیسکاری که میخوایم انجام بدیم تقریبا شبیه مپ هست. به ازای هر تگ یک ست داریم که حاوی آی‌دی پست هایی هست که اون تگ رو دارن. فرقش با هش اینه دیگه نمیگیم هر عضو مقدار true (یا یک) رو داره. چون هر عضو قرار نیست دیتایی رو توی خودش ذخیره کنه. صرفا با کامند های ردیس میتونیم چک کنیم که آیا فلان عضو توی فلان ست هست یا نه. براساس این موضوع میشه پیشبینی کرد که حافظه رم ما کمتر میشه و احتمالا به زیر ۲ گیگ برسیم. بهتره بنچ‌مارک بگیریم:اسکریپت افزودن دیتا ها فقط ۳۰ ثانیه طول کشید.حجم مموری گرفته شده ۱.۷ گیگ بود ( هورااا‌ )اسکریپت دریافت اطلاعات از ردیس ( ؟؟؟‌)خب، به یک مشکل خوردیم! بذارید مسئله رو دوباره بگم. ما یک کلید داریم که نشون دهنده یک تگ هست که حاوی یک ست از آی‌دی پست هاست. ما میخوایم ۱۰۰ تا پست رو توی اون ست چک کنیم. اما کامندی که ردیس برای چک کردن عضو درون ست به ما میده single هست. یعنی فقط یک عضو رو میتونیم تست کنیم. چطور ۱۰۰ تا رو با هم چک کنیم؟ از pipeline استفاده میکنیم (با استفاده از پایپ‌لاین ها یک مجموعه دستور برای ردیس با هم می فرستیم که پشت سر هم اجرا بشن.) اما برعکس هش یا get دستوری نیست که همه رو با هم بگیره و مطمئن باشیم بهینه سازی های ردیس روش انجام میشه.ردیس کامند SMISMEMBER رو از ورژن ۶.۲.۰ به بعد اضافه میکنه.( که روی ورژن stable فعلی نیست) پس قضیش برای استفاده توی پروداکشن کنسله! اما من روی سیستم خودم اوردم بالا اون ورژن رو و تستش کردم زیاد فرقی با پایپ‌لاین ها نداشت.۳. اسکریپت دریافت اطلاعات از ردیس: ۱.۴ میلی‌ثانیهجمع بندیست ها میتونست جواب بهتری باشه ولی مجبوریم ۴۰۰ مگ رو با stability معاوضه کنیم. البته سرعت دریافت از هش تا دو برابر سریع تر از ست بود که کنار استیبل بودن هش ها باعث شد این گزینه رو انتخاب کنیم.</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Thu, 31 Dec 2020 15:04:14 +0330</pubDate>
            </item>
                    <item>
                <title>چطور یک سایت داینامیک رو کش (Cache) کنیم؟</title>
                <link>https://virgool.io/@mhrlife/%DA%86%D8%B7%D9%88%D8%B1-%DB%8C%DA%A9-%D8%B3%D8%A7%DB%8C%D8%AA-%D8%AF%D8%A7%DB%8C%D9%86%D8%A7%D9%85%DB%8C%DA%A9-%D8%B1%D9%88-%DA%A9%D8%B4-cache-%DA%A9%D9%86%DB%8C%D9%85-hcytyntsjruw</link>
                <description>وقتی میگیم کش اولین چیزی که به ذهن خیلیامون میاد اینه کل صفحه HTML سایت که خروجی سایتمون هست رو کش کنیم. اگه وبلاگ داشته باشید و خیلی ساده بخواید لود سایت رو کمتر باشه تا میتونید با این حرکت به منظورتون برسید. اما یک سوال در نظر بگیرید.سایت ایی مثل Twitter که هر بار Refresh میکنید اش یک جواب جدید به ما میده چطور کش رو انجام میده؟ اصلا چطور میشه کش داینامیک داشت؟ Best Practice های کش برای این کار چطور هستن؟اول ببینیم چه نیازی داریمما میخوایم یک وبسایت داشته باشیم که توش کاربر ها مطلب منتشر میکنن. هر لحظه ممکنه مطلب جدید منتشر بشه. اگه من یک مطلب منتشر کردم توی تایم لاین همه کسایی که من رو فالو کردن نمایش داده بشه ( پس قابلیت فالو کردن باید داشته باشه ).نکته بعدی اینه که میخوایم سایتمون سریع باشه. نمیخوایم کش چند ساعتی داشته باشیم! طوری که اگه تعداد فالوئینگ های من منطقی بود هر بار رفرش کنم سایت رو احتمالا مطلب جدیدی برام لود بشه.ساده ترین روش کش رو در نظر بگیریم. من بیام تمام پیام خروجی API رو برای هر کاربر کش کنم! یعنی من وقتی خواستم تایم لاین رو بگیرم یک کوئری بزنه توی MySQL و برام تایم لاینم رو بسازه. بعد که رفرش کردم دوباره اون رو نشون بده. قاعدتا لود سایت کمتر میشه ولی این مخالف حرفیه که توی توضیحات هدف سایت گفته بودیم.پس کش کردن کل تایم لاین به ازای هر کاربر کار منطقی ایی نیست. باید یک فکر دیگه ایی کرد.کش ها رو بشکونیم!بیاید ببینیم تایم لاین من از چی تشکیل شده؟ یک لیست از پست ها. درسته؟ خب میتونیم این رو بشکونیم! جای اینکه کل تایم لاین رو کش کنیم پست ها رو کش میکنیم. چه اتفاقی میوفته؟ کوئری ایی که به دیتابیس میزنیم فقط id پست های جدید رو میگیره و از طریق اون آی دی میتونیم کل پست رو از کش بخونیم. لود دیتابیس خیلی کمتر میشه.اما شاید خود پست رو بقیه بتونن لایک کنن. یا بتونن کامنت بذارن. اگه همشون با هم کش کنیم هر بار کسی کامنت میفرسته باید کلی صبر کنه تا کش دوباره ساخته شه تا کامنتش نمایش داده بشه. پس بهتره پست رو هم بشکونیم. یک کش میذاریم برای توضیحات مثل Text و تصویر و سازنده پست و ... که معمولا تغییر نمیکنن. اگر هم تغییر کنن مثلا Text رو آپدیت میکنیم.یک کش جدید نیاز داریم که لیست کامنت ها رو توی خودش بر اساس یک پست نگه داره. این کش هم فقط id کامنت ها رو نگه میداره و اگر کامنت جدیدی اضافه شه این id جدید به کش اضافه میشه. خود کامنت ها هم جدا جدا کش میشن و براساس اون آی دی ها میتونیم به کامنت دسترسی داشته باشیم.مرتب تر ترتیب کش ها رو بگم. ( جلوی هر مورد نمونه کش ایی که میخوایم توی ردیس ذخیره کنیم نوشته شده )یک لیست که آی دی تک تک پست های تایم لاینمون رو نگه میداره. timeline:$useridمحتویات static پست ها ( مثل description و image و author و .. ) پست ها براساس آی دیشون. postinfo:$postidلیست آی دی کامنت های یک پست. postcomments:$postidلیست کسایی که یک پست رو لایک کردن براساس پست آی دی. postlikes:$postidمحتویات یک پست ( میتونید این رو هم بشکونید ولی طبق نیاز آموزش انجام نمیدم ) comment:$commentidلیست فالوئر های یک شخص followers:$useridلیست فالوئینگ های یک شخص following:$useridو در نهایت اطلاعات یک کاربر ( که اینم میتونید بشکونید ) user:$useridاین چیزی که گفتم میشه کلی بالا پایین شه. نکته ایی که هست کلیت ایده رو منتقل میکنه که ما قرار چیکار کنیم.دیتاتایپ های هرکدوم چی هستن؟ما میخوایم از Redis استفاده کنیم. چون خیلی خفنه. توی همین مثال خودش رو نشون میده که چرا خفنه. ما نمیایم لیست آی دی پست های تایم لاین یک نفر رو به صورت json ذخیره کنیم! میایم از data type های ردیس استفاده میکنیم. سریع تره. تست هاش انجام شده و خودش رو ثابت کرده.برای لیست ها همون طوری که از اسم اش میاد از دیتاتایپ لیست استفاده میکنیم. یعنی هر شخص به عنوان لیست یک تایم لاین توی ردیس داره. حالا پست جدید میاد. چیکار میکنیم؟ درست حدس زدید. به آخر لیست آی دی پست جدید اضافه میکنیم. به همین راحتی!حالا دیتاهایی مثل postinfo رو چطور ذخیره کنیم؟ دو تا راه حل داریم. استفاده از hash ردیس یا اینکه تبدیل به بایت کنیم اطلاعات postinfo رو و توی ردیس ذخیره کنیم.در مورد این میشه خیلی صحبت کرد. اما خلاصش اینه که وقتی از هش استفاده میکنید برای دسترسی به تمام اطلاعات پست باید تمام فیلد های هش رو پرواید کنید که زمانی که میگیره خیلی بیشتر از اینه JSON یا بایت بگیرید و تبدیلش کنید به آبجکت. اگر نیاز بود هر بار فقط یکی از فیلد های یک پست که توی postinfo ذخیره شده رو دربیاریم استفاده از هش منطقی تر بود. چون سریع تر میشد.برای postlikes باید یک فکری هم بکنیم که یه نفر دوبار پست رو لایک نکنه. یعنی اگه پست رو لایک کرده بهش نشون بدیم. برای این کار از لیست استفاده نمیکنیم. چرا؟ چون هر بار باید چک کنیم آی دیمون توی لیست هست یا نه و این یعنی o(n) که زیاده. ما میتونیم از Sorted Set استفاده میکنیم که cost اضافه کردن و دریافت o(log n) هست. یا اگه فضای اضافه داشته باشیم کنارش یک Hash هم میذاریم که بررسی آیا لایک کرده یا نه o)1( باشه.یک سناریو رو در نظر بگیریم.میخوایم یک پست جدید بسازیم و انتظار داریم توی تایم لاین تک تک کسایی که ما رو فالو کردن اضافه شه. چه اتفاقی میوفته؟پست جدید ما در دیتابیس ثبت میشهاطلاعات استاتیک پست جدید ما توی ردیس ثبت میشه. ( postinfo )لیست کسایی که مارو فالو کردن از followers میگیریم.آی دی پست ایی که ساختیم به آخر تایم لاین اون افراد اضافه میکنیم.حالا خودتون میتونید حدس بزنید وقتی یک کامنت جدید اضافه میشه به پست چه اتفاقی میوفته؟این چیزی که ما درموردش صحبت کردیم تقریبا کاریه که توییتر انجام میده. البته خیلی باید Tune بشه و چالش های دیگه ایی بخصوص توی Scale کردن بوجود میاد. مثلا وقتی لیدی گاگا با 5 میلیون فالوئر پست بزنه چطور آی دی پست رو به آخر تایم لاین 5 میلیون نفر اضافه کنیم طوری که سرورمون نخوابه =))</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Mon, 09 Nov 2020 23:46:38 +0330</pubDate>
            </item>
                    <item>
                <title>پیداکردن نزدیکترین کافی‌شاپ درلحظه با Redis</title>
                <link>https://virgool.io/@mhrlife/%D9%BE%DB%8C%D8%AF%D8%A7%DA%A9%D8%B1%D8%AF%D9%86-%D9%86%D8%B2%D8%AF%DB%8C%DA%A9%D8%AA%D8%B1%DB%8C%D9%86-%DA%A9%D8%A7%D9%81%DB%8C%D8%B4%D8%A7%D9%BE-%D8%A8%D9%87-%D8%B5%D9%88%D8%B1%D8%AA-%D8%AF%D8%B1%D9%84%D8%AD%D8%B8%D9%87-%D8%A8%D8%A7-redis-liq5dg3op9ik</link>
                <description>همیشه یکی از چالش هایی که توی ذهن من بود این بود که بتونم به صورت در لحظه براساس لوکیشن کاربر ها کاری انجام بدم. اما همیشه توی ذهنم پیاده سازی الگوریتم های Geo سخت بود. دیتابیس های سیکوئلی و مونگو چنین ابزار هایی ارائه میدن ولی خب اینکه دیتاها بخواد توی دیتابیس ریخته بشه و هر لحظه از اونا خونده شه زیاد حرکت جالبی نیست چون توی لود بالا کم میارن.آشنایی با Geo Spatial در Redisوقتی با مختصات تک بعدی کار داریم کار خیلی راحته. براساس فاصله ایندکس میکنیم و همه چیز حله. اما چطور من بیام ۱۰۰ تا نقطه توی مختصات دو بعدی رو مرتب کنم؟این کار توسط Geo Hash انجام میشه. به وسیله‌ی Geo Hash مختصات دو بعدی به یک هش که شامل string و اعداد هست تبدیل میشن و ردیس میاد از این هش استفاده میکنه. در واقع پشت Geo Spatial ایی که ارائه میده از یک Sorted Set هست که برای Score اش از این هش استفاده میکنن.خب این خبر خوبیه! چون اگه من خودم میخواستم الگوریتمی پیاده کنم مطمئن ام هیچ جوره به سرعت ردیس نمی‌رسید XD. خب حالا بیام ببینیم ردیس چه امکاناتی در اختیارمون قرار میده. ( توی این بخش از مثال های خود سایت ردیس استفاده میکنم که با کامند ها آشنا بشیم ، بعدش خودمون یک سیستم ساده پیاده میکنیم و بنچ مارک میگیریم‌)GEOADD cars -115.17087 36.12306 my-carبه Geo هم مثل Sorted Set نگاه کنیم. یک ست داریم که یک سری آیتم با score توش هستن. الان ست ما cars هست‌، آیتم my-car که scoreاش آدرس lat و long اش توی مختصات جغرافیایی. ( فقط توجه کنید که توی مختصات اولی longitude هست دومی latitude.)کامند دومی که نیاز داریم GEORADIUS هست.GEORADIUS cars -115.17258 36.11996 100 mهمون طوری که معلوم یک مختصات ( که میشه مختصات لحظه ایی ما ) رو بهش میدیم. دو تا ورودی دیگه هم داره که اولیش‌( ۱۰۰ ) داره شعاع ایی که میخوایم نقاط رو بگیریم بهمون نشون میده و دومی میگه شعاع ایی که دادی علامتش چیه ( مثلا متر ، کیلومتر ، فیت و .. ). میتونید تعریف کنید توی جواب بهمون مختصات های هر نقطه هم بده ، فاصله نقطه تا مختصات داده شده هم بده ، چند تا نقطه برامون برگردونه با چه sort ایی و ..کامند های خفن دیگه ایی هم داره مثل فاصله بین دو عضو ست، اعضای نزدیک به یک عضو که میتونی فاصله هم ازش بگیری و ... که اینجا میتونی یه لیستی ازشون رو ببینی.بریم سراغ پروژه خودمونما میخواستیم یک سری رستوران تعریف کنیم و لوکیشن خودمون رو بهش بدیم به صورت در لحظه لیست رستوران های نزدیکمون رو بهمون بشه. اینکه میگیم در لحظه قاعدتا یک پنجره زمانی داره، مثلا منطقیه وقتی کاربرمون راه میره هر چند ثانیه ریکوئست بفرسته یا اگه با ماشین حرکت میکنه با پنجره کوتاه تر. ولی اینا سمت کلاینت هندل میشه که واردش نمیشم)چیزی که ما میدونیم طبق آموزش های قبلی یک ست جدید به اسم shops تعریف میکنیم و با GEORADIUS هر بار نزدیک ترین رستوران ها رو دریافت میکنیم.برای پیاده سازی من از go استفاده میکنم ولی کلیت مفهوم توی زبان های مختلف باید یکی باشه. تهشم میخوایم بنچ مارک بگیریم ببینیم سیستم ما میتونه به چند نفر همزمان پاسخ بده.func createClients(r *redis.Client) {
   latFrom, longFrom, latTo, longTo := tehranLatlong()
   itr := 50000
   ctx := context.Background()
   for i := 0; i &lt; itr; i++ {
      lat := fromRange(latFrom, latTo, rand.Float64())
      long := fromRange(longFrom, longTo, rand.Float64())
      r.GeoAdd(ctx, &amp;quotshops&amp;quot, &amp;redis.GeoLocation{
         Name:      strconv.Itoa(i),
         Longitude: long,
         Latitude:  lat,
      })
   }
}
func tehranLatlong() (float64, float64, float64, float64) {
   latFrom := 35.679292
   longForm := 51.306170
   latTo := 35.764646
   longTo := 51.487273
   return latFrom, longForm, latTo, longTo
}
func fromRange(from float64, to float64, x float64) float64 {
   return x*(to-from) + from
}توی کد بالا من یک تابع نوشتم که ردیسمون رو با دیتای تست پر کنه. اگه توجه کنید یک تابع به اسم tehranLatLong دارم که مختصات حدودی تهران رو بهم میده ( خواستم دیتای تست روی رنج تهران باشه که منطقی تر به چشم بیاد برای تستمون ). توی تابع fromRange هم بهش یک محدوده میدم و طبق عدد رندومی که بهش میدیم توی اون محدوده بهمون یک عدد میده.توی تابع createClients اصل کار شروع میشه که ۵۰ هزار دیتا به صورت رندوم میسازیم و با استفاده از تابع GeoAdd کلاینت ردیسمون به سرور ردیس اضافه میکنیم.همه چیز امادست. تابع بالا رو صدا میزنم و دیتا ها رو اضافه میکنم ( اضافه کردن ۵۰ هزار دیتا حدود ۱.۵ ثانیه طول کشید ، چیزی که هست اینه افزودن این دیتاها میتونه پروسه پیچیده ایی باشه . یعنی توی سیستم من حدود ۴۰ هزار ریکوئست در ثانیه. این صرفا برای این میگم که بعدا باید دنبال scalability سیستم هم بریم. ) اما فکر نکنم تهران بیشتر از ۵۰ هزار کافه داشته باشه. داره؟؟اما خب رستوران های ما فعلا ثابت هستن! نیازی نداریم هر لحظه آپدیتشون کنیم. پس load کردن دیتاها  توی init اپلیکیشن انجام میشه و زیاد تاثیری روی پرفورمنس نداره.حالا یک HTTP route میزنم که نزدیک ترین رستوران ها رو بهم نشون بده. برای اینکه بنچ‌مارک بگیرم به صورت رندوم به سیستم یک مختصات توی رنج میدم که ۵۰ رستوران نزدیک توی رنج ۲۵۰ متر ایی رو برگردونه.// radius in meters
func findNear(r *redis.Client, lat float64, long float64, radius float64) []redis.GeoLocation {
   ctx := context.Background()
   res := r.GeoRadius(ctx, &amp;quotshops&amp;quot, long, lat, &amp;redis.GeoRadiusQuery{
      Radius:    radius,
      Unit:      &amp;quotm&amp;quot,
      WithCoord: true,
      Count:     50,
      Sort:      &amp;quotASC&amp;quot,
   })
   return res.Val()
}func Benchmark(r *redis.Client) func(c echo.Context) error {
   return func(c echo.Context) error {
      latFrom, longFrom, latTo, longTo := tehranLatlong()
      lat := fromRange(latFrom, latTo, rand.Float64())
      long := fromRange(longFrom, longTo, rand.Float64())
      items := findNear(r, lat, long, 250)
      result := &amp;quot&amp;quot
      for _, item := range items {
         result += &amp;quot\n&amp;quot + fmt.Sprintf(&amp;quot%s\t%f, %f&amp;quot, item.Name, item.Latitude, item.Longitude)
      }
      return c.String(http.StatusOK, result)
   }
}همون‌طوری که میبینید توی روت‌مون یک مختصات رندوم میسازیم و به fromRange میدیم که توی اون از GeoRadius استفاده میکنیم. خیلی ساده هست و نیازی به توضیح بیشتر نداره فقط Sort رو اگه ببینید دو حالت ASC و DESC داره که ASC نزدیک ترین رستوران ها به من توی اون بازه و DESC دورترین ها به من توی اون بازه رو نشون میده. خیلی امکانات بیشتری هم داره مثل محاسبه فاصله و ... که من توی این کار نیازی بهش نداشتم. یه تست بکنیم:خروجیخب همون طوری که میبینید به خوبی کار کرد‌:) حالا ببینیم همین روت چند ریکوئست برثانیه هندل میکنه؟Requests per second:    4340.66 [#/sec] (mean)
Time per request:       115.190 [ms] (mean)روی یک ماشین نتیجه خوبیه!این تازه شروع کارهنکته ایی که هست شاید بنظرتون تا اینجا کافی باشه. ۴ هزار ریکوئست برثانیه میشه هندل کردن حدود ۲۰ هزار کاربر آنلاین همزمان ( اگه مثلا هر ۵ ثانیه ریکوئست بدن به سرور ). اما ردیس شما میتونه هر لحظه بیاد پایین یا شاید کاربراتون از اینی که هست خیلی بیشتر شد. باید به فکر Master Slave کردن ردیس و کلاسترینگش باشید که هم مطمئن باشید پایین اومدن یک سرور ردیس توی اپلیکیشنتون مشکلی بوجود نمیاره و اینکه هر قدر تعداد یوزر های آنلاینتون بیشتر شد میتونید با اضافه کردن سرور پاسخگوی نیاز کاربراتون باشید ( scale out )</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Sun, 08 Nov 2020 15:48:16 +0330</pubDate>
            </item>
                    <item>
                <title>مراقب باش کد ات یک کار یکسان رو چند بار انجام نده!</title>
                <link>https://virgool.io/@mhrlife/%D9%85%D8%B1%D8%A7%D9%82%D8%A8-%D8%A8%D8%A7%D8%B4-%DA%A9%D8%AF-%D8%A7%D8%AA-%DB%8C%DA%A9-%DA%A9%D8%A7%D8%B1-%DB%8C%DA%A9%D8%B3%D8%A7%D9%86-%D8%B1%D9%88-%DA%86%D9%86%D8%AF-%D8%A8%D8%A7%D8%B1-%D8%A7%D9%86%D8%AC%D8%A7%D9%85-%D9%86%D8%AF%D9%87-arnnpaavtfo8</link>
                <description>CPU spikes! yikes!بیاید یک سناریو رو در نظر بگیریم. ما یک سیستم ساده داریم که یک دیتابیس سیکوئلی داره، یک سری کوئری سنگین هم داریم و تصمیم میگیریم که جوابش رو برای یک مدت زمانی کش کنیم. تا اینجا همه چیز خوبه. تست میکنیم جواب میده ، میبریم روی Production بازم خوبه. شب میخوابیم و صبح بیدار میشیم با تصویر بالا روبرو میشیم! یه زمان هایی سی پی یو میچسبه به سقف. علت چیه؟دوباره تحلیل کنیم که چه کد ایی زدیم.کدی که ما زدیم احتمالا اینطوره. اول یک شرط داریم چک میکنه دیتا توی کش هست یا نه. اگه نبود میریم کوئری میزنیم به دیتابیس و دیتا رو به کش اضافه میکنیم. اما یک حالت رو در نظر بگیرید:سرویس ما لود اش رفته بالا‌، حدود ۱۰۰ کاربر همزمان درخواست HTTP به اون اند پوینت ما میزنن. چه اتفاقی میوفته ؟ کاربر اول میره میبینه توی کش دیتا نیست ، میگه خب حله من میرم دیتا رو از دیتابیس بیارم و بریزم تو کش. حالا این فرایند ۱۰۰ میلی ثانیه طول میکشه. توی این پنجره ۱۰۰ میلی ثانیه ایی ۵۰ تا کاربر دیگه درخواست HTTP میزنن. چه اتفاقی میوفته؟ تمام اون ۵۰ درخواست هم می بینن چیزی توی کش نیست و شروع میکنن به provide کردن اون دیتا از دیتابیس. توی اون پنجره ۱۰۰ میلی‌ثانیه ایی سی‌پی‌یو میچسبونیم به سقف.این مثال جاهای مختلف کد میتونه وجود داشته باشه، پیدا کردن این موارد توی یک کدبیس بزرگ میتونه سخت باشه، اما باید مشخص کنیم منابع کند ما چیا هستن. میتونه خوندن یک فایل استاتیک باشه‌، درخواست های HTTP یکسان ، یک کوئری یکسان به دیتابیس و ...حالا فرض کنید متوجه شدیم که کجای کد مشکل ایجاد میکنه و باعث اون اسپایک ها توی سی‌پی‌یو میشه. چطور حلش کنیم؟ ( مسئله Thundering Herd )همون طوری که گفتم، فقط یک بار انجامش بدهمن با زبان go کار میکنم، راه حلی که دارم از کتابخانه ایی برای این زبان استفاده میکنه اما یقیناً مشابه این کتابخانه برای زبان های دیگه هم باید باشه. روش های دیگه ایی هم هستن که مستقل از زبان باشن.توی این مقاله از یکی از مهندسین اینستاگرام با روش Promises این مشکل رو حل میکنه که خیلی جالبه. اما فعلا ما توی یک اسکیل معمولی به قضیه نگاه میکنیم.گوگل سال ۲۰۱۳ یک کتابخانه به اسم groupcache معرفی کرده که علاوه بر این مشکل ( Thundering Herd ) یک سری مشکل های دیگه رو هم حل میکنه. کلا کتابخانه ی جذابیه. اما نکته ایی که هست توی خودش داره از singleflight استفاده میکنه. این کتاب‌خانه یک duplicate function call suppression mechanism ارائه میده. یعنی توابع مشابه ایی که همزمان دارن اجرا میشن یک بار اجرا شن و اون جواب به بقیه call ها داده بشه.cacheData, err := cache.Get(&amp;quotgetData&amp;quot)
var result string
if err != nil {
   result = GetData()
   cache.Set(&amp;quotgetData&amp;quot, []byte(result))
} else {
   result = string(cacheData)
}
return c.String(http.StatusOK, result)کد بالا رو در نظر بگیرید. ما یک کار خیلی ساده انجام دادیم! گفتیم که مقدار getData رو از cache به من بده. اگه مقدار وجود داشت که نمایشش میده و اگه مقدار نبود تابع GetData رو صدا میزنه و بعد دیتایی که گرفته رو توی کش میریزه و در نهایت مقدار رو برمیگردونه. تابع GetData هم یک تابع ساده هست ( که مثلا وصل میشه به دیتابیس یا ... و حدود ۱۰۰ میلی ثانیه طول میکشه‌ )من یک سری متریک اضافه کردم که بررسی کنم تابع GetData چند بار صدا زده میشه؟ اگر یک درخواست جدید بیاد توی بازه ۱۰۰ میلی‌ثانیه ایی که جواب رو پیدا میکنه درخواست جدیدی بیاد دوباره صدا زده میشه. (دقیقا Thundering herd ایی که صحبتش رو کرده بودیم)حالا چطور مشکل رو رفع کنیم؟ اول یک resourceGroup با singleflight میسازم. var resourceGroup singleflight.Group بعد اون رو پاس میدم به route ایی که قراره دیتا توش پرواید بشه. ( نکته ایی که هست اینه تمام goroutine ها باید به یک resourceGroup یکسان دسترسی داشته باشن.حالا میتونیم متد Do رو صدا بزنیم. اینطور که یک کلید بهش میدیم و یک تابع. خودش تابع رو اجرا میکنه اما مراقب این هست اگر یک گوروتین دیگه ایی خواست تابعی صدا بزنه اگه کلیدش مشابه این کلید بود صدا زده نشه ، بلکه منتظر میزنه اون بار اولی که تابع صدا زده شده به اتمام برسه و جواب رو به بقیه روتین ها میده.func HomeRoute( cache *bigcache.BigCache) func(c echo.Context) error {
   var resourceGroup singleflight.Group
   return func(c echo.Context) error {
      cacheData, err := cache.Get(&amp;quotgetData&amp;quot)
      var result string
      if err != nil {
         v, _, _ := resourceGroup.Do(&amp;quotgetData&amp;quot, func() (interface{}, error) {
            return GetData(), nil
         })
         result = v.(string)
         cache.Set(&amp;quotgetData&amp;quot, []byte(result))
      } else {
         result = string(cacheData)
      }
      return c.String(http.StatusOK, result)
   }
}خب کد ما تقریبا همون کد قبلیه با یک تفاوت. ریکوئست اول میاد و میبینه دیتا توی کش نیست ، میاد متد Do رو صدا میزنه ، resourceGroup می‌بینه که قبلا کسی Do ی getData رو صدا نزده پس میاد اون تابع ایی که بهش دادیم رو صدا میزنه. حالا ریکوئست دوم ( یا چندین ریکوئست بعدی ) میاد و Do رو صدا میزنه. اما resourceGroup میبینه که قبلا اجرا شده این تابع ، منتظر میمونه که ریکوئست اول مقدار رو provide کنه.وقتی ریکوئست اول مقدار رو فراهم کرد resourceGroup مقدار رو هم به ریکوئست اول میده هم ریکوئست دوم و مقدار توی کش هم ذخیره میشه. از این به بعد بقیه ریکوئست ها مقدار رو از کش میگیرن.در نتیجه اگر ۱۰۰ ریکوئست همزمان بیاد جای اینکه ۱۰۰ بار به دیتابیس ریکوئست بزنیم یک بار میزنیم که اون اسپایک های سی پی یو رو از بین میبره.مثال پیچیده تر! چندین سرویس داریم با زبان های مختلفچیزی که تا الان صحبت کردم یک مثال خیلی ساده بود. یه وقت هایی پیش میاد ما چندین سرویس داریم که همزمان از یک کش استفاده میکنن. میتونن زبان های مختلف باشن و خلاصه نتونیم از singleflight استفاده کنیم. اگه همزمان به دو تا سرویس درخواست بره همزمان هر دو میرن که دیتا رو از سرویسی که دیتا رو ارائه میدن دریافت کنن. برای این حالت چیکار میکنیم؟ انشالله مقاله بعدی D:</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Sat, 07 Nov 2020 12:06:04 +0330</pubDate>
            </item>
                    <item>
                <title>کش چند لایه. تنها راه نجات!</title>
                <link>https://virgool.io/@mhrlife/%DA%A9%D8%B4-%DA%86%D9%86%D8%AF-%D9%84%D8%A7%DB%8C%D9%87-%D8%AA%D9%86%D9%87%D8%A7-%D8%B1%D8%A7%D9%87-%D9%86%D8%AC%D8%A7%D8%AA-wwihetbnhdgh</link>
                <description>چند وقت پیش یک پروژه داشتم که یکی از end point هاش نیاز به یک سری محاسبات داشت. اینطور که به ازای هر درخواستی که سمت سرور میومد باید حدود ۱۰۰۰ درخواست get به کش میزد و بعد از انجام محاسبات (‌ که خود محاسبات زیاد سنگین نبودن ) و محاسبه مقادیر جدید همون مقدار درخواست set به کش بزنه. در آخر یک Worker دیگه هر چند وقت یک بار اطلاعات کش رو دریافت میکنه و توی دیتابیس ذخیره میکنه.من زیاد وارد جزئیات فنی پروژه نمیشم ، یکم خلاصه ترش میکنم. فرض کنید یک api با http میخوایم که توی هر درخواست ۱۰۰۰ درخواست set به کش بزنه و بعدش اون مقادیر رو بخونه. زیاد وارد جزئیات persist بودن دیتا ها هم نمیشیم چون اون خودش یک چالش دیگه هست.نکته دومی که هست میخوایم پاسخ اونقدر سریع باشه که اپ ما بتونه به بیش از ۳۰۰۰ هزار ریکوئست برثانیه پاسخ بده.ردیس سریع هست ، ولی نه به اندازه کافیبیاید یکم بیشتر با Redis آشنا بشیم. ردیس یک دیتابیس ذخیره دیتا به صورت key value هست که مقدار ها رو توی مموری ذخیره میکنه. این موضوع که دیتا ها توی مموری ذخیره میشن یعنی نسبت به دیتابیس هایی که دیتا رو مستقیم روی هارد درایو و .. ذخیره میکنن سریع تره. خلاصه بگم که اگه نیازه یک دیتایی رو از دیتابیس بگیرید ، و میدونید مثلا هر چند دقیقه یک بار اون مقدار ثابت هست جای اینکه هر ریکوئست به دیتابیس وصل شه و مقدار رو بگیره یک بار دیتا رو توی ردیس میریزیم و اون چند دقیقه دیتا رو از ردیس میگیریم. این موضوع باعث میشه اپلیکیشن ما خیلی سریع تر بشه در نتیجه با یک سرور یکسان میتونیم به ریکوئست های بیشتری در یک ثانیه پاسخ بدیم.نکته دومی که هست و ردیس رو با بقیه سیستم های کشینگ متمایز میکنه دیتاتایپ های ذخیره سازیش هست. که شامل string , list , set , hash , sorted set میشه. برای مثال من نیاز داشتم توی این پروژه یک سری از دیتا ها sort شده ذخیره بشن که sorted set به این موضوع کمک کرد. اما ردیس چقدر سریع هست؟همون طوری که گفتم ردیس نسبت به دیتابیس های مثلا SQL خیلی سریعه. ما ترجیح میدیم که اگر امکان داره برای دریافت یک دیتا به ردیس ریکوئست بزنیم تا MySQL. اما باید در نظر بگیرید ردیس هم داره یک سری امکانات در کنار سرعتش هم میده. یعنی یک Trade Off هست. فقط برای سرعت خالص ساخته نشده. با استفاده از redis-benchmark روی سیستمتون میتونید ببینید که بنچ مارک ردیستون چطوره.$ redis-benchmark -n 100000 -t set,get -q SET: 129870.13 requests per secondGET: 125000.00 requests per secondهمون طوری که میبینید ردیس روی سیستم من میتونه حدود ۱۰۰ هزار ریکوئست بر ثانیه داشته باشه! خیلی زیاده! اما توجه کنید هر ریکوئست قرار بود ۲ هزار درخواست به ردیس بزنم. یعنی در ثانیه اند پوینت من میتونه در ثانیه 100000/2000=50 ریکوئست رو هندل کنه. ۵۰ ریکوئست کجا ۳۰۰۰ کجا! بیاید این رو توی عمل ببینیم. ( برای نوشتن بنچ مارک ها از زبون go استفاده میکنم‌ ، بخصوص وقتی سرعت مهمه :)‌)func Test01() func(c echo.Context) error {
   rdb := redis.NewClient(&amp;redis.Options{
      Addr:     &amp;quotlocalhost:6379&amp;quot,
      Password: &amp;quot&amp;quot, // no password set
      DB:       0,  // use default DB
   })
   return func(c echo.Context) error {
      iteration := 1000
      // set
      for i := 0; i &lt; iteration; i++ {
         key := fmt.Sprintf(&amp;quotkey_%d&amp;quot, i)
         rdb.Set(c.Request().Context(), key, i, 100*time.Second)
      }
      // get
      for i := 0; i &lt; iteration; i++ {
         key := fmt.Sprintf(&amp;quotkey_%d&amp;quot, i)
         rdb.Get(c.Request().Context(), key)
      }
      return c.String(http.StatusOK, &amp;quotDone&amp;quot)
   }
}همون طوری که توی کد میبینید اول به دیتابیس ردیس وصل میشیم. توی هر درخواست هم ۱۰۰۰ مقدار توی کش میریزیم و بعدش همون ۱۰۰۰ تا رو میخونیم. حالا با آپاچی بنچ مارک بهش ۱۰۰ ریکوئست برثانیه در مجموع ۱۰۰۰ ریکوئست بزنیم ببینیم چطور جواب میده.$ ab -n 1000 -c 100 http://localhost:1323/  
Requests per second:    41.73 [#/sec] (mean)
Time per request:       2396.329 [ms] (mean)
Time per request:       23.963 [ms] (mean, across all concurrent requests)
Percentage of the requests served within a certain time (ms)
  50%   2419ms
  66%   2447ms
  75%   2482ms
  80%   2493ms
  90%   2505ms
  95%   2513ms
  98%   2519ms
  99%   2526ms
 100%   2552 (longest request)عددا همونطوری هستن که حدس میزدیم. سیستممون تونست حدود ۴۰ ریکوئست برثانیه هندل کنه.یه وقت هایی نیاز به این امکانات ردیس نداریم ، ما نیاز داریم value یک key در سریع ترین زمان ممکن به ما داده بشه.  برای این کار باید یک لایه جدید از کش اضافه کنیم. خودمون یک سیستم کش با Map بسازیم!وقتی توی مصاحبه های کاری و ... با مپ کار میکنیم میگیم دسترسی به value یک key در Map پیچیدگی زمانی o(1) داره. کلا بنظر خیلی سریع میاد. برای پیاده سازی لایه جدید کشمون یک پکیج جدید میسازم که یک تایپ جدید به اسم MyCache تعریف میکنه.type MyCache struct {
   db        map[string]int
   localLock sync.Mutex
}
func NewMyCache() *MyCache {
   return &amp;MyCache{db: map[string]int{}}
}
func (p *MyCache) Set(key string, val int) {
   p.localLock.Lock()
   defer p.localLock.Unlock()
   p.db[key] = val
}
func (p *MyCache) Get(key string) int {
   p.localLock.Lock()
   defer p.localLock.Unlock()
   return p.db[key]
}همون طوری که از کد هم معلومه یک Map داریم که بهش مقدار اضافه میشه یا مقدارش دریافت میشه. چیز پیچیده ایی نیست. ببینیم حالا این کد ما چطور جواب میده: Requests per second:    1764.61 [#/sec] (mean)
Time per request:       56.670 [ms] (mean)
Time per request:       0.567 [ms] (mean, across all concurrent requests)
Percentage of the requests served within a certain time (ms)
  50%     56ms
  66%     60ms
  75%     62ms
  80%     64ms
  90%     70ms
  95%     78ms
  98%     87ms
  99%     92ms
 100%    100 (longest request)پیشرفت زیادی کردیم! از ۴۰ ریکوئست برثانیه رسیدیم به ۱۷۰۰ ریکوئست برثانیه. من در این مرحله بنظرم به آخر خط رسیده بودم. مگه میشه از مپ سریع تر شد؟ تا اینکه این مقاله رو خوندم.خلاصه بخوام بگم ( حتما اون مقاله رو مطالعه کنید ) شما وقتی Map دارید Garbage Collector توی فاز های مارک و اسکن تک تک آیتم های مپ تون رو بررسی میکنه. این موضوع وقتی که دیتای زیادی دارید میتونه توی پرفورمنس کش‌تون تاثیر بذاره. توی این مقاله که هدفشون رسیدن به یک سیستم کش به شدت سریع بوده خیلی نکات دیگه ایی رو مطرح میکنه و در نهایت به پکیج ایی که ارائه دادن یعنی BigCache میرسه. بیاید حالا جای مپ از BigCache استفاده کنیم. ببینیم چه اتفاقی میوفته.cache, _ := bigcache.NewBigCache(bigcache.DefaultConfig(10 * time.Minute))
return func(c echo.Context) error {
   iteration := 1000
   // set
   for i := 0; i &lt; iteration; i++ {
      key := fmt.Sprintf(&amp;quotkey_%d&amp;quot, i)
      cache.Set(key, []byte(strconv.Itoa(i)))
   }
   // get
   for i := 0; i &lt; iteration; i++ {
      key := fmt.Sprintf(&amp;quotkey_%d&amp;quot, i)
      cache.Get(key)
   }
   return c.String(http.StatusOK, &amp;quotDone&amp;quot)
}و بنچ مارک :Requests per second:    3225.35 [#/sec] (mean)
Time per request:       31.004 [ms] (mean)
Time per request:       0.310 [ms] (mean, across all concurrent requests)
Percentage of the requests served within a certain time (ms)
  50%     27
  66%     31
  75%     34
  80%     37
  90%     43
  95%     49
  98%     54
  99%     60
 100%     95 (longest request)اوه اوه! رسیدیم به ۳۲۰۰ ریکوئست برثانیه. یعنی حدود ۲ برابر سریع تر از Map. تعداد ریکوئست های کانکارنت رو به ۱۰۰۰ ریکوئست و تعداد ریکوئست های کل رو هم به ۱۰۰۰۰ افزایش دادم. توی بنچ مارک باز هم ۲ برابر سریع تر از مپ بود.خلاصه و نتیجه گیری این مقاله به هیچ وجه برای این نیست نشون بده ردیس به درد نمیخوره! توی این پروژه من خیلی جا ها نیاز داشتم ( و گزینه ایی بهتر از ردیس نبود ) که از ردیس استفاده کنم.ردیس رو میشه Scale کرد. دیتا های ردیس Persist هستن. ردیس دیتاتایپ های مختلف با کلی امکانات خفن داره. هدف این مقاله این بود نشون بده که چندین تکنولوژی کنار هم توی جای درستشون میتونن چقدر به افزایش سرعت کمک کنن در نتیجه روی یک سرور یکسان بتونیم لود بیشتری رو هندل کنیم.</description>
                <category>محمد حسینی راد</category>
                <author>محمد حسینی راد</author>
                <pubDate>Fri, 06 Nov 2020 12:27:52 +0330</pubDate>
            </item>
            </channel>
</rss>