Redis یک ساختار ذخیره داده در حافظه اصلی است که از یک پایگاه داده با الگوی کلید-مقدار بهره می‌گیرید. استفاده از Redis و فریم‌ورکی مانند لاراول به صورت همزمان برای ذخیره داده‌هایی که بیشتر برای خواندن هستند و کمتر دچار تغییر می‌شوند، موجب می‌شود که سرعت استخراج داده‌ها در مقایسه با پایگاه‌های داده‌ای مانند Mysql به میزان قابل‌ملاحظه‌ای افزایش یابد.

استفاده از لاراول و Redis

استفاده از لاراول و Redis

مواردی که در این آموزش فراخواهیم گرفت:

  • نحوه نصب Redis و گزینه‌هایی که در این رابطه برای شما وجود دارند.
  • انواع داده‌های Redis همراه با مثال‌های مرتبط
  • ایجاد یک eCommerce ساده با استفاده از لاراول به همراه Redis برای ذخیره داده
  • استفاده از Laravel Websocket به همراه Redis queue برای نمایش بروزرسانی لحظه‌ای به کاربران بدون نیاز به تازه‌سازی صفحه

پیش‌نیازها

برای دنبال کردن این آموزش به موارد زیر احتیاج خواهید داشت.

  • یک پروژه لاراول x جدید که در دایرکتوری ریشه وب شما قرار داشته باشد.
  • استفاده از سیستم‌عامل لینوکس یا مکینتاش
  • نصب کمپوزر به صورت گلوبال بر روی سیستم
  • استفاده از PHP نسخه‌های بالاتر از 2

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


wget http://download.redis.io/redis-stable.tar.gz

tar xvzf redis-stable.tar.gz

cd redis-stable

make

نهایتاً کارهای زیر را انجام دهید.


cp src/redis-cli /usr/local/bin/

chmod 755 /usr/local/bin/redis-cli#Optionally

sudo cp src/redis-server /usr/local/bin/

chmod 755 /usr/local/bin/redis-cli

حالا با اجرای فرمان از طریق ترمینال، از صحت عملکرد ابزار خود مطمئن شوید.


redis-cli

در صورتی که همه چیز به خوبی پیش رفته باشد، به رابط کاربری خط فرمان Redis هدایت خواهید شد.

نصب PHP Redis

دو گزینه برای نصب همزمان Redis و لاراول وجود دارد. ساده‌ترین و اولین روش، استفاده از کمپوزر برای نصب بسته predis/predis است.


composer require predis/predis

با این وجود، این بسته در لاراول 7.x توسط برنامه‌نویس اصلی کنار گذاشته شده و قرار است که در نسخه‌های بعدی لاراول حذف شود.

گزینه دوم که بیشتر توصیه شده و البته به زمان و کار بیشتری نیاز دارد، نصب افزونه PHPRedis است. این افزونه یک API برای ارتباط با سرور Redis ایجاد می‌کند. PHPRedis برخلاف بسته کمپوزر predis، بیشتر قابل‌اعتماد و سریع است و می‌تواند در اپلیکیشن‌های بزرگتر مورد استفاده قرار گیرد.

در اینجا از PECL برای نصب افزونه PHPRedis استفاده می‌کنیم. برای آنهایی که با این ابزار آشنایی ندارند، باید گفت که PECL منبعی برای افزونه‌های PHP است.

نصب در اوبونتو


sudo apt-get -y install gcc make autoconf libc-dev pkg-config

sudo peclX.Y-sp install redis

نکته: عبارت peclX.Y را با نسخه PHP خودتان جایگزین کنید. برای بررسی این موضوع می‌توانید از فرمان php -v در ترمینال استفاده نمایید. بر این اساس اگر نسخه PHP شما برابر 7.2 باشد، فرمان به صورت sudo pecl7.2-sp install redis درخواهد آمد.

وقتی نصب افزونه با موفقیت به پایان رسید، یک فایل تنظیمات برای افزونه Redis ایجاد کنید. پس باید با فرمان زیر PHP را دوباره راه‌اندازی نمایید.


sudo bash -c "echo extension=redis.so > /etc/php5.X-sp/conf.d/redis.ini"

sudo service php7.X-fpm-sp restart

نصب  در مکینتاش

فرمان زیر را اجرا کنید.


pecl install redis

این فرمان می‌تواند منجر به بروز خطا شده و افزونه را به شکل کامل نصب نکند. در چنین صورتی، فرمان زیر را تایپ و اجرا کنید. در این فرمان، 7.x.x نسخه PHP شماست. همچنین با کمک فرمان ls /usr/local/Cellar/php می‌توانید  لیست تمام فایل‌های موجود در دایرکتوری را مرور کرده و مسیر درست را برای pecl مشخص کنید.


rm /usr/local/Cellar/php/7.x.x/pecl

حالا فایل php.ini بارگذاری‌شده را باید ویرایش کنید. برای مشخص‌کردن فایل php.ini بارگذاری‌شده، یک فایل phpinfo.php بسازید و عبارت <?php echo phpinfo(); را در آن قرار دهید. سپس خروجی را به منظور یافتن نسخه بارگذاری‌شده بررسی کنید و در ادامه، این فایل را ویرایش کرده و خط زیر را در آن حذف کنید.


extension="redis.so"

اکنون فایل php.ini را ذخیره کرده و از آن خارج شوید.

نهایتاً یک فایل /usr/local/etc/php/7.X/conf.d/ext-redis.ini بسازید و با ویرایشگر vim یا nano آن را باز کنید. به خاطر داشته باشید که به جای 7.X باید نسخه PHP خودتان را وارد کنید.


vi /usr/local/etc/php/7.X/conf.d/ext-redis.ini

حالا محتوای زیر را درون فایل جدید وارد کنید.


[redis]

extension="/usr/local/Cellar/php/7.X.Y/pecl/20180731/redis.so"

سرویس php-fpm را دوباره راه‌اندازی کرده و اگر هم از لاراول استفاده می‌کنید، فرمان valet restart را اجرا کنید.

انواع داده‌های Redis

Redis انواع مختلفی از داده دارد که عبارتند از: String، Lists، Sets، Sorted Sets، Hashes & Bitmaps و Hyperlogs.  در اینجا درباره همه انواع داده‌های در دسترس، به جز مورد آخر صحبت خواهیم کرد.

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


redis-cli

داده String

این یکی از پایه‌ای‌ترین نوع داده در دسترس است. رشته‌های String به صورت binary safe هستند. به این معنا که یک رشته Redis می‌تواند هر نوع داده‌ای، مثلاً یک تصویر JPEG یا یک آبجکت کدبندی‌شده در بر داشته باشد.

در اینجا یک نمونه را در خط فرمان Redis بررسی می‌کنیم.


127.0.0.1:6379> SET user:1:name Oluwafemi

127.0.0.1:6379> GET user:1:name

"Oluwafemi"

داده‌های Lists

این نوع داده، در واقع لیستی از رشته‌هاست که بر اساس ورود مرتب شده‌اند و آیتم‌ها را می‌توان به ابتدا یا انتهای لیست اضافه کرد. لاراول باعث می‌شود که این نوع داده برای ذخیره وظایف در یک صرف استفاده گردد. سپس این وظایف مطابق زمان اضافه‌شدن به لیست، اجرا می‌شوند.


127.0.0.1:6379> LPUSH blog:1:tags laravel php redis

127.0.0.1:6379> LRANGE blog:1:tags 0 -1

1) "redis"

2) "php"

3) "laravel"

127.0.0.1:6379> RPUSH blog:1:tags pecl

(integer) 4

127.0.0.1:6379> LRANGE blog:1:tags 0 -1

1) "redis"

2) "php"

3) "laravel"

4) "pecl"

داده‌های Sets

دسته‌ها مجموعه‌ای نامرتب از رشته‌ها هستند که امکان اضافه، حذف و تست وجود اعضا در آنها وجود دارد. در همین حال، امکان تکرار آیتم‌ها در دسته‌ها نیست و در هنگام استخراج آیتم‌ها، ترتیب خاصی در خروجی نخواهد بود.

همچنین امکان انجام برخی عملیات بین دو یا چند دسته، از جمله پیدا کردن اشتراکات یا ترکیب آنها وجود خواهد داشت.


127.0.0.1:6379> SADD user:1:interests horror comedy violent drama inspiring

(integer) 5

127.0.0.1:6379> SMEMBERS user:1:interests

1) "drama"

2) "comedy"

3) "violent"

4) "horror"

5) "inspiring"

127.0.0.1:6379> SISMEMBER user:1:interests drama

(integer) 1

127.0.0.1:6379> SISMEMBER user:1:interests dark

(integer) 0

127.0.0.1:6379> SADD user:2:interests horror drama inspiring

(integer) 3

127.0.0.1:6379> SINTER user:1:interests user:2:interests

1) "drama"

2) "inspiring"

3) "horror"

127.0.0.1:6379> SUNION user:1:interests user:2:interests

1) "violent"

2) "comedy"

3) "drama"

4) "inspiring"

5) "horror"

127.0.0.1:6379> SCARD user:2:interests

(integer) 3

127.0.0.1:6379> SSCAN user:2:interests 0 match horror

1) "0"

2) 1) "horror"

داده‌های Hashes

Redis Hashها تبدیل‌هایی بین میدان رشته‌ها و مقادیر رشته‌ها به شمار می‌روند. بر این اساس، نوع داده مناسبی برای ارائه آبجکت‌ها خواهند بود. در هنگام چت یا گفتگو بین دو کاربر، هر کدام از پیام‌ها و جزئیات آن را می‌توان در یک hashmap بیان کرد.


127.0.0.1:6379> HMSET chats:1 message Hello user_id 1

OK

127.0.0.1:6379> HMSET chats:2 message "You there!" user_id 1

OK

127.0.0.1:6379> HMSET chats:2 message "Yes I am" user_id 2

OK

127.0.0.1:6379> HGET chats:1 message

"Hello"

127.0.0.1:6379> HGET chats:2 user_id

"2"

127.0.0.1:6379> HGETALL chats:2

1) "message"

2) "Yes I am"

3) "user_id"

4) "2"

127.0.0.1:6379> HSET chats:2 message "Edited message"

(integer) 0

127.0.0.1:6379> HGETALL chats:2

1) "message"

2) "Edited message"

3) "user_id"

4) "2"

داده‌های Sorted Sets

دسته‌های مرتب شباهت زیادی به دسته‌های ساده Redis دارند. با این تفاوت که هر کدام از اعضای یک دسته مرتب با یک نمره (Score) همراه است که ترتیب آن را در دسته مشخص می‌کند. به عنوان مثال، می‌توان یک دسته مرتب با نمره از کوچک به بزرگ در اختیار داشت.

به عنوان نمونه، می توانید یک دسته مرتب از پیام‌ها را در تاریخچه گفتگوی دو کاربر تصوّر کنید. به این ترتیب، می‌توان در استخراج پیام‌ها بر اساس مقاطع زمانی اقدام کرد که در اینجا، مقاطع زمانی پیام‌ها به عنوان «نمره» درنظر گرفته می‌شود.


127.0.0.1:6379> ZADD chat_between:user_1:user_2 1588542249 1 1588542252 2 1588542273 3

(integer) 3

127.0.0.1:6379> ZRANGE chat_between:user_1:user_2 0 -1

1) "1"

2) "2"

3) "3"

127.0.0.1:6379> ZRANGE chat_between:user_1:user_2 0 -1 withscores

1) "1"

2) "1588542249"

3) "2"

4) "1588542252"

5) "3"

6) "1588542273"

تقریباً تمام عملیاتی که برای دسته‌های ساده قابل‌انجام هستند، برای یک دسته مرتب نیز کاربرد دارند.

برای مطالعه بیشتر در مورد سری داده‌های Redis می‌توانید به متون رسمی در این رابطه نیز مراجعه کنید.

اپلیکیشن Ecommerce با استفاده از لاراول و Redis

در اینجا فرض می‌شود که شما از قبل یک پروژه لاراول جدید در دایرکتوری ریشه وب‌سایت خود نصب کرده‌اید. سپس باید موارد مربوط به تأییدیه ورود را انجام داده و از طریق مرورگر، از اپلیکیشن خود بازدید نمایید.

در صورتی که چنین کاری را انجام نداده‌اید، فرمان‌های زیر را اجرا نمایید.


composer create-project --prefer-dist laravel/laravel ecommerce

cd ecommerce

composer require laravel/ui

php artisan ui bootstrap --auth

npm install

npm run dev

یک پایگاه Mysql جدید برای ecommerce بسازید.


mysql -u root -p

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> create database ecommerce ;

Query OK, 1 row affected (0.24 sec)

در ویرایشگر PHP خودتان، پروژه لاراول ecommerce را باز کنید. ابزار مورد استفاده در اینجا PHPStorm است. سپس فایل تنظیمات .env پروژه را ویرایش کنید تا با جزئیات پایگاه داده مطابقت داشته باشد.


DB_CONNECTION=mysql

DB_HOST=127.0.0.1

DB_PORT=3306

DB_DATABASE=ecommerce

DB_USERNAME=user

DB_PASSWORD=yourpassword

سپس فرمان php artisan migrate را اجرا کنید تا کاربران و جداول بازیابی پسورد در پایگاه داده ecommerce ساخته شود.

طرح پایگاه داده لاراول و Redis

برای درک بهتر نقش Redis در ذخیره داده‌، در ادامه طرحی از یک پایگاه داده را بررسی می‌کنیم. منظور از «طرح» یا “schema” در اینجا، سازماندهی داده به عنوان علامتی از نحوه تشکیل پایگاه داده است و بنابراین، یک موضوع چندان پیچیده نیست.

اضافه‌کردن یک محصول

خطوط زیر را به فایل روت web.php خودتان اضافه کنید.


Route::get('/products/create', 'ProductController@create')->name('product.new');

Route::post('/products/create', 'ProductController@store')->name('product.store');

Route::get('/products/all', 'ProductController@viewProducts')->name('product.all');

یک فایل جدید resources/view/product/create.blade.php بسازید که حاوی فرم محصولات جدید باشد. سپس محتوای زیر را در آن کپی کنید.


@extends('layouts.app')

@section('content')

<div class="container">

<div class="row justify-content-center">

<div class="col-md-8">

<div class="card">

<div class="card-header">New Product</div>

<div class="card-body">

<form method="POST" action="{{route('product.store')}}">

{{csrf_field()}}

<div class="form-group">

<label for="product_name">Product Name</label>

<input type="text" class="form-control" name="product_name" id="product_name" required placeholder="ex Headset Jack">

</div>

<div class="form-group">

<label for="product_image">Product Image</label>

<input type="text" class="form-control" name="product_image" id="product_image" required placeholder="Image url">

</div>

<div class="form-group">

<label for="tags">Tags</label>

<input type="text" class="form-control" name="tags" id="tags" required placeholder="Separate tags by comma">

</div>

<button type="submit" class="btn btn-primary">New Product</button>

</form>

</div>

</div>

</div>

</div>

</div>

@endsection

سپس یک فایل جدید resources/view/product/browse.blade.php ایجاد کنید تا با استفاده از آن تمام  محصولات اضافه‌شده را نمایش دهید.


@extends('layouts.app')

@section('content')

<div class="container">

<div class="row justify-content-center">

<div class="col-md-12">

@if($products)

<div class="row">

<div class="col-9">

<div class="col-12 mb-1">

<a href="{{route('product.new')}}">Add New Product</a>

</div>

@foreach($products as $product)

<div class="col-4 float-left mb-1">

<div class="card">

<img class="card-img-top" height="260" src="{{$product['image']}}" alt="Card image cap">

<div class="card-body text-center">

<h5 class="card-title">{{$product['name']}}</h5>

</div>

</div>

</div>

@endforeach

</div>

<div class="col-3 border">

@foreach($tags as $tag)

<a class="btn btn-sm btn-primary px-2 py-1 m-1" href="?tag={{$tag}}" role="button">{{$tag}}</a>

@endforeach

</div>

</div>

@else

<div class="card">

<div class="card-header">Browse Products</div>

<div class="col-8">

<div class="alert alert-success mt-2" role="alert">

Empty products! <a href="{{route('product.new')}}">Add Product</a>

</div>

</div>

</div>

@endif

</div>

</div>

</div>

@endsection

نهایتاً ProductController خود را بروزرسانی کنید تا با کد زیر مطابقت داشته باشد.


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use Illuminate\Support\Facades\Redis;

class ProductController extends Controller

{

public function __construct()

{

$this->middleware('auth');

}

public function create()

{

return view('products.create');

}

public function store(Request $request)

{

$tags = explode(',',$request->get('tags'));

$productId = self::getProductId();

if(self::newProduct($productId, [

'name' => $request->get('product_name'),

'image' => $request->get('product_image'),

'product_id' => $productId

])){

self::addToTags($tags);

self::addToProductTags($productId, $tags);

self::addProductToTags($productId, $tags);

}

return redirect()->route('product.all');

}

public function viewProducts()

{

$tags = Redis::sMembers('tags');

$products = self::getProducts();

return view('products.browse')->with([

'products' => $products,

'tags' => $tags

]);

}

/*

* Increment product ID every time

* a new product is added, and return

* the ID to be used in product object

*/

static function getProductId()

{

(!Redis::exists('product_count'))

Redis::set('product_count',0);

&nbsp;

return Redis::incr('product_count');

}

/*

* Create a hash map to hold a project object

* e.g HMSET product:1 product "men jean" id 1 image "img-url.jpg"

* Then add the product ID to a list hold all products ID's

*/

static function newProduct($productId, $data) : bool

{

self::addToProducts($productId);

return Redis::hMset("product:$productId", $data);

}

/*

* A Ordered Set holding all products ID with the

* PHP time() when the product was added as the score

* This ensures products are listed in DESC when fetched

*/

static function addToProducts($productId) : void

{

Redis::zAdd('products', time(), $productId);

}

/*

* A unique Sets of tags

*/

static function addToTags(array $tags)

{

Redis::sAddArray('tags',$tags);

}

/*

* A unique set of tags for a particular product

* eg SADD product:1:tags jean men pants

*/

static function addToProductTags($productId, $tags)

{

Redis::sAddArray("product:$productId:tags",$tags);

}

/*

* A List of products carry this particular tag

* ex1 RPUSH men 1 3

* ex2 RPUSH women 2 4

*/

static function addProductToTags($productId, $tags)

{

foreach ($tags as $tag){

Redis::rPush($tag,$productId);

}

}

/*

* In a real live example, we will be returning

* paginated data by calling the lRange command

* lRange start end

*/

static function getProducts($start = 0, $end = -1) : array

{

$productIds = Redis::zRange('products', $start, $end, true);

$products = [];

foreach ($productIds as $productId => $score)

{

$products[$score]= Redis::hGetAll("product:$productId");

}

return $products;

}

}

حالا اگر در مرورگرتان به ‌آدرس /products/create بروید، با یک فرم ایجاد یک محصول جدید روبرو می‌شوید.

برای ساده‌سازی بیشتر آموزش استفاده از لاراول و Redis، در اینجا لیستی از ۵ محصول به همراه یک تصویر از آنها را در سرور s3 آماده کرده‌ایم تا کار را با آنها شروع کنید. استفاده از آدرس تصاویر تنها به منظور پیشبرد اهداف این آموزش است و مسلماً برای کارهای حرفه‌ای چنین کاری توصیه نمی‌شود.

Sleeveless Jump Suit https://eecommerce.s3.amazonaws.com/jump-suit.jpg women jumpsuit
Men Jean https://eecommerce.s3.amazonaws.com/jean.jpg men pants jean
Pencil Skirt https://eecommerce.s3.amazonaws.com/pencil-skirt.jpg women skirt
Men Sandal https://eecommerce.s3.amazonaws.com/sandal.jpg men footwear sandal
Smock Top https://eecommerce.s3.amazonaws.com/smock-top.jpg women top
T-Shirt https://eecommerce.s3.amazonaws.com/tees.jpg men women top

پس از اضافه کردن تمام محصولات به ترتیب دلخواه، احتمالاً با چیزی شبیه به زیر روبرو می‌شوید.

نمونه نمایش محصول

نمونه نمایش محصول

فیلتر کردن بر اساس برچسب‌ها

گاهی اوقات می‌خواهیم که با کلیک بر روی یک برچسب، به تمام محصولات مرتبط با آن دست پیدا کنیم. برای این منظور، یک متد جدید  به ProductController اضافه کنید.


static function getProductByTags($tag, $start = 0, $end = -1) : array

{

$productIds = Redis::lRange($tag, $start, $end);

$products = [];

foreach ($productIds as $productId) {

$products[] = Redis::hGetAll("product:$productId");

}

return $products;

}

متد viewProducts را بروزرسانی کنید تا از وجود پارامتر آدرس tag مطمئن شوید.


public function viewProducts(Request $request)

{

if($request->has('tag')){

$products = self::getProductByTags(($request->get('tag')));

}else{

$products = self::getProducts();

}

$tags = Redis::sMembers('tags');

return view('products.browse')->with(['products' => $products, 'tags' => $tags]);

}

همان‌طور که مشاهده می‌کنید، امکانات زیادی در استفاده از Redis به عنوان پایگاه داده وجود دارد. به عنوان یک تمرین یا چالش بیشتر می‌توانید نمایش یک محصولات و محصولات مشابه را در زیر آن امتحان کنید.

راهنمایی: محصولات مشابه از تگ‌های یکسان با محصولی که نمایش داده می‌شود، استفاده می‌کنند.