آموزش Redis با استفاده از یک پروژه ی کاربردی

آموزش Redis با استفاده از یک پروژه ی کاربردی

در این قسمت از دوره می ‌خواهیم، یک سیستم پرسش و پاسخ را، با استفاده از Redis پیاده سازی کنیم. با توجه به قسمت قبل که توضیحات مقدماتی Redis ارائه شد، در این مقاله به طراحی ارتباطات بین موجودیت­ ها و همچنین بازیابی و نمایش آن ‌ها خواهیم پرداخت.

هدف از این قسمت، آموزش راه اندازی یک پروژه با استفاده از لاراول نیست و تنها استفاده ی ردیس در این پروژه را تحت فریم ورک لاراول آموزش می‌دهیم.

اگر بخواهیم در دیتابیس ردیس مانند دیتابیس‌های رابطه ­ای شناسه در نظر بگیریم، می‌ باییست به ازای هر سطر در یک جدول، یک کلید در دیتابیس ردیس ذخیره کنیم که برای نام ‌گذاری آن می‌توانیم از جداکننده ­های ":" یا "." بین نام جدول و کلید استفاده کنیم. برای مثال اگر بخواهیم کلیدی برای پست شماره 1 در نظر بگیریم به شکل posts:1 خواهد بود.

در صورتی که بخواهیم اطلاعات ساختاریافته ایجاد کنیم، می­ بایست از نوع داده ی Hash استفاده کنیم چون تنها نوع داده‌ای در Redis است که این امکان را به ما می ­دهد که اطلاعاتی مشابه یک سطر از جدول دیتابیس رابطه ­ای را در یک کلید ذخیره کنیم.

جهت نگهداری اطلاعات به یک ترتیب خاص از Sorted Set استفاده می­ شود که این دیتاتایپ، کلیدهای Hash را بر اساس Score خاصی ذخیره می­ کند که در نتیجه در هنگام واکشی داده، ترتیب کلیدها مشخص می ‌باشد.

با توجه به این دو نوع، ذخیره ارتباطات بین موجودیت ­ها در ردیس اصولا بدین شکل است:

1. ارتباط یک-به-یک : اطلاعات هر کدام از موجودیت­ ها به صورت جداگانه ذخیره می­ شود و کلید هر کدام در اطلاعات دیگری ذخیره می­شود.

2. ارتباط یک-به-چند : اطلاعات هر کدام از موجودیت ­ها به صورت جداگانه ذخیره می­ شود و از یک دیتاتایپ Sorted set جهت ذخیره کلیدهای این ارتباط که بر اساس ترتیب خاصی هستند، استفاده می ­شود.

3. ارتباط چند-به-چند : اطلاعات هر کدام از موجودیت­ ها به صورت جداگانه ذخیره می ­شود و از دو دیتاتایپ Sorted set استفاده می‌ شود. یکی از آن‌ها برای ذخیره ی ارتباط هر کدام از موجودیت نوع اول با موجودیت های نوع دوم و دیگری برای ذخیره ی ارتباط هر کدام از موجودیت نوع دوم با موجودیت های نوع اول.

 

لاراول


در این پروژه ی آموزشی که یک سیستم پرسش و پاسخ می‌ باشد، از فریمورک لاراول که محبوب‌ترین فریمورک زبان PHP است استفاده خواهیم کرد. این فریمورک به راحتی قابلیت کار با Redis را در اختیار ما قرار می‌ دهد.

جهت ارتباط با Redis از پکیج predis که توسط فریمورک لاراول پیشنهاد شده است، استفاده خواهیم کرد که آموزش کامل کار با آن در سایت لاراول موجود است و در این قسمت تا حدی که با پروژه ی آموزشی مرتبط باشد معرفی می نماییم.

 

سیستم پرسش و پاسخ


در این سیستم افراد پس از ثبت‌ نام و ورود به سیستم، می ­توانند سؤالات خود را ثبت کرده و به سؤالات دیگران پاسخ دهند. همچنین دسترسی به ویرایش و حذف سؤال خود را نیز دارند. علاوه بر این می ­توانند برای سؤالات و پاسخ افراد دیگر Like و Dislike ثبت کنند.

در این سیستم سؤال را پست (Post) و جواب را کامنت (Comment) در نظر می­ گیریم. نحوه نمایش و ذخیره اطلاعات پست­ ها و همچنین نمایش و ذخیره‌سازی کامنت­ ها مطابق شکل های زیر است.

 پست


ابتدا به سراغ پست می ­رویم که ساختار اطلاعات آن Hash است. از بین فیلد های موجود در شکل زیر فقط فیلد body ضروری است و توسط کاربر ایجاد می­ شود.

 {
     "id" => "2"
     "body" => "foo bar"
     "user" => "sasan"
     "created_at" => "2020-03-09 16:21:20"
     "user_id" => 1
     "Like" => 0
 }

حال به سراغ اِعمال CRUD بر روی پست­ ها و همچنین افزودن like و dislike به آن‌ها می ­رویم. جهت کنترل و پاسخ به درخواست­های مربوط به پست از کلاس PostController استفاده می­ کنیم.

ذخیره : جهت ذخیره‌ سازی اطلاعات پست، اطلاعات کامل شده توسط کاربر را به کنترلر PostController ارسال می­ کنیم که مطابق زیر است:

public function store(Request $request)
 {
     $postiId = Redis::command('incr', ['post_id']);
     $postKey = "posts:".$postId;
     Redis::hmset($postKey,
         "id", $postId,
         "body", $request->get( 'body'),
         "user", Auth::user()->name,
         "created_at", Carbon::now(),
         "user_id", Auth::id(),
         "Like", 0
     );
     Redis::zadd( 'post_time', 
Carbon: :now( )->timestamp, $postKey );
     return redirect( )->route( 'home' )
->with('message','Post Added Succesfully' );
  }

ابتدا برای این که شناسه ی پست ها را به صورتی ذخیره کنیم که به صورت اتوماتیک به ازای هر پست افزایش یابد،  از یک set استفاده کرده و آی دی آخرین پست را در آن ذخیره می ‌کنیم و قبل از هر ذخیره ی سازی یک عدد به آن اضافه کرده و از آن برای ساختن کلید Hash پست ها استفاده می ‌کنیم. برای اینکار از دستور

command('incr', ['post_id'])

استفاده می ‌کنیم که در دفعه ی نخست عدد یک را تولید می ‌کند و در دفعات بعد با هر بار اجرای آن یک عدد اضافه می ‌شود. در ادامه ی کد با استفاده از کلید Hash ساخته شده، کلیه اطلاعات دریافتی را به همراه باقی اطلاعات ذخیره می­ شود. همچنین یک Sorted set جهت مرتب­ سازی پست ها بر اساس زمان ایجاد آن‌ها (post_time) در نظر می­ گیریم که به همراه آن کلید پست نیز ذخیره می­ شود.

به ‌روزرسانی : این عملیات مشابه عملیات ذخیره است فقط متن پست (فیلد body) تغییر می­ کند و دیگر نیازی به تولید کلید بعد از کلید قبلی نیست:

public function update($postId, Request $request )
 {
     $postKey = "posts:".$postId;
     Redis::hmset($postKey,
         "body", $request->get( 'body'),
     );
     return redirect( )->route( 'home' )
->with('message','Post Updated Succesfully' );
  }

نمایش لیست پست ­ها : جهت نمایش اطلاعات پست­ های ذخیره شده، مطابق کد زیر ابتدا کلیدهای 10پست­ آخر موجود در Sorted set استخراج می ­شود، سپس کلیه اطلاعات هر یک از Hash ها استخراج شده و در داخل آرایه ذخیره شده می­ شود و سپس برای نمایش به View ارسال می ‌گردد.

  public function index( )
  {
     $posts = collect();
     $postIds = Redis::zrevrange('post_time', 0, 10);
     foreach ($postIds as $postId) {
        $posts->push(Redis: :hgetall($postId) );
     }

     return view( 'home',compact('posts'));
  }

در کد بالا تابع zrevrange برای استخراج آخرین اطلاعات ذخیره شده استفاده می ‌شود که تعداد دیتا را نیز می­توان برای آن تعیین کرد که در اینجا کلید پست­ها از 0 تا 10 در نظر گرفته شده است.

همچنین تابع hgetall جهت بازیابی اطلاعات یک کلید hash استفاده می­ شود.

 

Like و Dislike: جهت ثبت تعداد این دو مورد، شناسه پست دریافتی از طرف کاربر را در دیتاتایپ Hash پیدا کرده و مقدار آن‌ را یک مورد افزایش یا کاهش می ‌دهیم.
تابع hincrby موجود در کد زیر جهت افزایش یک فیلد از دیتاتایپ Hash استفاده می ­شود. این تابع کلید دیتاتایپ و فیلد مورد نظر جهت افزایش مقدار و همچنین میزان تغییر را به عنوان ورودی دریافت کرده و برای Like یک واحد افزایش می­ دهد و برای Dislike یک واحد کاهش می­ دهد.

public function like(Request $request )
 {
    $postKey = "posts:".$request->get('id');
    Redis::hincrby($postKey, "Like", 1);
    return redirect( )->route('home' )
          ->with( 'message','Saved Your Like');
 }

 public function dislike(Request $request)
 {
    $postKey = "posts:".$request->get('id');
    Redis::hincrby($postKey, "Like", -1);
    return redirect( )->route( 'home' )
      ->with( 'message','Saved Your Dislike');
 }

هرچند جهت انجام این کار به جای ذخیره تعداد کل like یا dislike به عنوان یک فیلد در دیتاتایپ Hash، بهتر بود که یک set در نظر گرفته شود.

 

نمایش یک پست خاص : جهت نمایش پست و کامنت­های مرتبط با آن، شناسه پست که از سمت کاربر دریافت می‌شود، کلید پست و کلید Sorted set ارتباط پست و کامنت‌ ها ایجاد می­ گردند و اطلاعات آن‌ها استخراج گردیده و به View ارسال می ­شوند.

    public function show($id)
    {
        $comments = collect();
        $postKey = "posts:".$id;
        $post = Redis::hgetall($postKey);
        $commentIds=Redis::zrevrange(
$postKey.":comments", 0, -1);
        foreach ($commentIds as $commentId) {
            if (empty(Redis::hgetall($commentId)))
                continue;
           $comments->push(Redis::hgetall($commentId));
        }

        return view('post',compact('post','comments'));
    }

 ابتدا  کلید پست مورد نظر با توجه به آی دی ورودی ایجاد می­ گردد. سپس کل اطلاعات پست مورد نظر با استفاده از تابع hgetall استخراج می­ شود. در خط بعد با استفاده از zrevrange کل کامنت‌های یک پست (برای مثال پست با شماره 6 دارای کلید posts:6:comments است) از یک دیتا تایپ Sorted set استخراج می ­شود. و در انتها هر کدام از کامنت‌ها با استفاده از تابع hgetall از دیتاتایپی که برای کامنت‌ها در نظر گرفتیم گرفته می‌ شود.

 

حذف :  جهت حذف پست و ارتباطات آن که در اینجا کامنت­ های مرتبط با آن پست می ­باشد، می­ بایست شناسه پست را به تابع destroy ارسال ­کنیم و مطابق کد زیر، کلید پست مورد نظر برای حذف ایجاد را به دست بیاوریم. سپس کلید پست مورد نظر از sorted set ی به نام "post_time" که جهت ذخیره ی ترتیب بندی شده ی پست ها در نظر گرفته بودیم، حذف می ­گردد. در خط بعد، کلیه اطلاعات کلید پست مورد نظر با استفاده از تابع hdel از دیتاتایپ Hash حذف می­ شوند.

public function destroy(Request $request)
 {
    $id = $request->get('id');
    $postKey = 'posts:'.$1d;
    Redis::zrem('post_time', $postKey);
    Redis::hdel($postKey,
                "id",
                "body", 
                "user",
                "created_at",
                "user_id",
                "lLike"
             	);
    return redirect( )->route('home' )
   	->withErrors('Post Deleted Successfully');
 }

نکته ی مهم این که در تابع hdel نیاز است که همه ی فیلدهای مورد نظر جهت حذف حتما وارد گردد و چون ما می‌ خواهیم کل فیلدها را حذف کنیم در نتیجه کل فیلدها را به عنوان ورودی به تابع hdel فرستاده­ایم.

درباره ی پاک کردن کامنت‌ های یک پست در بخش کامنت ‌ها به آن می ‌پردازیم.

 

کامنت


اطلاعات هر کامنت به صورت جداگانه ذخیره می ­گردند و جهت ایجاد ارتباط میان کامنت و پست از یک دیتاتایپ sorted set استفاده می­ کنیم. فرض کنید دو کامنت با کلیدهای comments:1 و comments:2 با دیتا تایپ hash ایجاد شده ­اند و این دو کامنت برای پستی با کلید posts:20 درج گردیده­ اند. برای ایجاد ارتباط میان پست و کامنت­ ها از یک کلید با نام posts:20:comments که دارای دیتاتایپ sorted set هست استفاده می­ کنیم و کلید این دو کامنت را بر اساس زمان ایجاد آن‌ها در  posts:20:comments درج می­ کنیم.

نحوه رفتار و کار ما با کامنت در لاراول مطابق پست بوده و ساختار اطلاعات کامنت مطابق شکل زیر است که از میان فیلدها فقط فیلد body ضروری است و توسط کاربر وارد می­ شود.

{
 	"id" => "4"
	"body" => "How are you ?"
	"user" => "Sokan"
 	"created_at" => "2020-03-09 16:49:21"
	"user_id" => 1
	"like" => 0
 }

حال به بررسی نحوه ذخیره، به‌روزرسانی و سایر عملیاتها می ­پردازیم. جهت کنترل و پاسخ به درخواست­ های مربوط به کامنت از CommentController استفاده می­ کنیم.

 

ذخیره : جهت ذخیره ‌سازی اطلاعات کامنت برای هر پست، اطلاعات کامل شده توسط کاربر را به کنترلر CommentController ارسال می­ کنیم که مطابق کد زیر است:

public function store(Request $request )
 {
    $commentId = Redis::command('incr', ['comment_id']);
    $postId = $request->get( 'post_id');
    $postKey = "posts:".$postId;
    $commentKey = "comments:".$commentId;
    Redis: :hmset($commentKey,
       "id", $commentid,
       "body", $request->get('comment'),
       "user", Auth: :user( )->name,
       "created_at", Carbon: :now(),
       "user_id", Auth::id(),
       "Like", 0
    );
    Redis::zadd($postKey.":comments", 
           Carbon: :now( )->timestamp, $commentKey
     );
     return redirect( )->route('post.show',['id'=>$postId] )
      	 ->with('message','Comment Added Succesfully' );
 }

ابتدا کلید پست و کلید کامنت مورد نظر را ایجاد کرده و سپس سایر اطلاعات مربوط به کامنت را ذخیره می­ کند. همچنین یک Sorted set جهت مرتب ­سازی و ارتباط با پست ­ها بر اساس زمان ایجاد آن‌ ها (posts:id:comments) در نظر می­ گیریم که در آن کلید کامنت ذخیره می‌ شود.

همان‌طور که مشاهده می­ کنید ارتباط بین پست و کامنت از طریق Sorted set ایجاد گردید.

 

به‌روزرسانی : مطابق عملیات ذخیره است فقط برخی اطلاعات تغییر یافته مانند متن کامنت و زمان ایجاد و ... تغییر می ­کند.
 

Like و Dislike : جهت ثبت تعداد این موارد مطابق کد زیر، کلید کامنت مورد نظر ایجاد گردیده سپس با استفاده از تایع hincrby که در بخش پیشین توضیح داده شد، مقدار فیلد like از یک دیتاتایپ Hash یک واحد افزایش و برای dislike کاهش می­ یابد.

public function Like(Request $request )
 {
   $commentKey = "comments:".$request->get('id');
   Redis::hincrby($commentKey, "Like", 1);
   return redirect( )->back( )
     ->with('message','Saved Your Like');
 }

 public function dislike(Request $request )
 {
   $commentKey = "comments:".$request->get('id');
   Redis::hincrby($commentKey, "Like", -1);
   return redirect( )->back( )
     ->with('message','Saved Your Dislike');
 }

حذف :  جهت حذف کامنت و ارتباطات آن می­ بایست شناسه پست و شناسه ی کامنت را به تابع destroy ارسال کنیم و مطابق کد زیر، با استفاده از تابع zrem کلید کامنت را از ارتباط میان پست و کامنت را حذف کرده سپس کل اطلاعات کامنت را با استفاده از تابع hdel از دیتا تایپ Hash حذف کنیم.

 public function destroy(Request $request )
 {
   $commentId = $request->get('id');
   $postId = $request->get( 'post_id');
   $postKey = "posts:".$postId;
   $commentKey = 'comments:'.$commentId;
   Redis::zrem($postKey.':comments', $commentKey);
   Redis::hdel($commentKey,
        "id", "body", "user", 
      "created_at", "user_id", "Like");
   return redirect()->route( 'post.show',['id'=>$postId] )
     ->with('message','Comment Deleted Successfully' );
 }

نتیجه گیری


در این مقاله به بررسی نحوه طراحی دیتابیس سامانه ی پرسش و پاسخ با استفاده از دیتابیس ردیس پرداختیم و نحوه ارتباط آن با لاراول و بازیابی آن را بررسی کردیم.

با این که در این پروژه یک رابطه ی ساده بین موجودیت ها وجود داشت، ولی باز هم مدیریت و کنترل کلیدها و ارتباط بین داده­ ها بسیار سخت بود. این در حالی است که مدیریت این ارتباطات در پروژه­ های بزرگ، تقریبا غیرممکن است. درنتیجه همان‌طور که قبلا اشاره شده بود این نوع دیتابیس مناسب سیستم ­های نرم‌افزاری بزرگ با ارتباطات زیاد نیست در نتیجه پیشنهاد می­ گردد که از این نوع دیتابیس به عنوان دیتابیس جانبی استفاده شود.

نظرات
اگر login نکردی برامون ایمیلت رو بنویس: