پیداکردن نزدیکترین کافی‌شاپ درلحظه با Redis

همیشه یکی از چالش هایی که توی ذهن من بود این بود که بتونم به صورت در لحظه براساس لوکیشن کاربر ها کاری انجام بدم. اما همیشه توی ذهنم پیاده سازی الگوریتم های 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 < itr; i++ { lat := fromRange(latFrom, latTo, rand.Float64()) long := fromRange(longFrom, longTo, rand.Float64()) r.GeoAdd(ctx, &quotshops&quot, &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, &quotshops&quot, long, lat, &redis.GeoRadiusQuery{ Radius: radius, Unit: &quotm&quot, WithCoord: true, Count: 50, Sort: &quotASC&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 := &quot&quot for _, item := range items { result += &quot\n&quot + fmt.Sprintf(&quot%s\t%f, %f&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 )