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

استفاده از لاراول و Redis
مواردی که در این آموزش فراخواهیم گرفت:
- نحوه نصب Redis و گزینههایی که در این رابطه برای شما وجود دارند.
- انواع دادههای Redis همراه با مثالهای مرتبط
- ایجاد یک eCommerce ساده با استفاده از لاراول به همراه Redis برای ذخیره داده
- استفاده از Laravel Websocket به همراه Redis queue برای نمایش بروزرسانی لحظهای به کاربران بدون نیاز به تازهسازی صفحه
پیشنیازها
برای دنبال کردن این آموزش به موارد زیر احتیاج خواهید داشت.
- یک پروژه لاراول x جدید که در دایرکتوری ریشه وب شما قرار داشته باشد.
- استفاده از سیستمعامل لینوکس یا مکینتاش
- نصب کمپوزر به صورت گلوبال بر روی سیستم
- استفاده از PHP نسخههای بالاتر از 2
قبل از شروع هر کاری، باید کلاینت Redis را حتماً در سیستم خودتان نصب کنید. بهترین راه توصیه شده، کامپایل مستقیم آن از منبع است. برای این منظور فرمانهای زیر را به ترتیب در ترمینال خود وارد کنید.
1 2 3 4 5 6 7 | wget http: //download.redis.io/redis-stable.tar.gz tar xvzf redis-stable.tar.gz cd redis-stable make |
نهایتاً کارهای زیر را انجام دهید.
1 2 3 4 5 6 7 | 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 |
حالا با اجرای فرمان از طریق ترمینال، از صحت عملکرد ابزار خود مطمئن شوید.
1 | redis-cli |
در صورتی که همه چیز به خوبی پیش رفته باشد، به رابط کاربری خط فرمان Redis هدایت خواهید شد.
نصب PHP Redis
دو گزینه برای نصب همزمان Redis و لاراول وجود دارد. سادهترین و اولین روش، استفاده از کمپوزر برای نصب بسته predis/predis است.
1 | composer require predis/predis |
با این وجود، این بسته در لاراول 7.x توسط برنامهنویس اصلی کنار گذاشته شده و قرار است که در نسخههای بعدی لاراول حذف شود.
گزینه دوم که بیشتر توصیه شده و البته به زمان و کار بیشتری نیاز دارد، نصب افزونه PHPRedis است. این افزونه یک API برای ارتباط با سرور Redis ایجاد میکند. PHPRedis برخلاف بسته کمپوزر predis، بیشتر قابلاعتماد و سریع است و میتواند در اپلیکیشنهای بزرگتر مورد استفاده قرار گیرد.
در اینجا از PECL برای نصب افزونه PHPRedis استفاده میکنیم. برای آنهایی که با این ابزار آشنایی ندارند، باید گفت که PECL منبعی برای افزونههای PHP است.
نصب در اوبونتو
1 2 3 | 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 را دوباره راهاندازی نمایید.
1 2 3 | sudo bash -c "echo extension=redis.so > /etc/php5.X-sp/conf.d/redis.ini" sudo service php7.X-fpm-sp restart |
نصب در مکینتاش
فرمان زیر را اجرا کنید.
1 | pecl install redis |
این فرمان میتواند منجر به بروز خطا شده و افزونه را به شکل کامل نصب نکند. در چنین صورتی، فرمان زیر را تایپ و اجرا کنید. در این فرمان، 7.x.x نسخه PHP شماست. همچنین با کمک فرمان ls /usr/local/Cellar/php میتوانید لیست تمام فایلهای موجود در دایرکتوری را مرور کرده و مسیر درست را برای pecl مشخص کنید.
1 | rm /usr/local/Cellar/php/7.x.x/pecl |
حالا فایل php.ini بارگذاریشده را باید ویرایش کنید. برای مشخصکردن فایل php.ini بارگذاریشده، یک فایل phpinfo.php بسازید و عبارت <?php echo phpinfo(); را در آن قرار دهید. سپس خروجی را به منظور یافتن نسخه بارگذاریشده بررسی کنید و در ادامه، این فایل را ویرایش کرده و خط زیر را در آن حذف کنید.
1 | extension= "redis.so" |
اکنون فایل php.ini را ذخیره کرده و از آن خارج شوید.
نهایتاً یک فایل /usr/local/etc/php/7.X/conf.d/ext-redis.ini بسازید و با ویرایشگر vim یا nano آن را باز کنید. به خاطر داشته باشید که به جای 7.X باید نسخه PHP خودتان را وارد کنید.
1 | vi /usr/local/etc/php/7.X/conf.d/ext-redis.ini |
حالا محتوای زیر را درون فایل جدید وارد کنید.
1 2 3 | [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 آزمایش میکنیم. بنابراین، یک ترمینال جدید باز کرده و فرمان زیر را اجرا کنید.
1 | redis-cli |
داده String
این یکی از پایهایترین نوع داده در دسترس است. رشتههای String به صورت binary safe هستند. به این معنا که یک رشته Redis میتواند هر نوع دادهای، مثلاً یک تصویر JPEG یا یک آبجکت کدبندیشده در بر داشته باشد.
در اینجا یک نمونه را در خط فرمان Redis بررسی میکنیم.
1 2 3 4 5 | 127.0.0.1:6379> SET user:1:name Oluwafemi 127.0.0.1:6379> GET user:1:name "Oluwafemi" |
دادههای Lists
این نوع داده، در واقع لیستی از رشتههاست که بر اساس ورود مرتب شدهاند و آیتمها را میتوان به ابتدا یا انتهای لیست اضافه کرد. لاراول باعث میشود که این نوع داده برای ذخیره وظایف در یک صرف استفاده گردد. سپس این وظایف مطابق زمان اضافهشدن به لیست، اجرا میشوند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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
دستهها مجموعهای نامرتب از رشتهها هستند که امکان اضافه، حذف و تست وجود اعضا در آنها وجود دارد. در همین حال، امکان تکرار آیتمها در دستهها نیست و در هنگام استخراج آیتمها، ترتیب خاصی در خروجی نخواهد بود.
همچنین امکان انجام برخی عملیات بین دو یا چند دسته، از جمله پیدا کردن اشتراکات یا ترکیب آنها وجود خواهد داشت.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | 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 بیان کرد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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) همراه است که ترتیب آن را در دسته مشخص میکند. به عنوان مثال، میتوان یک دسته مرتب با نمره از کوچک به بزرگ در اختیار داشت.
به عنوان نمونه، می توانید یک دسته مرتب از پیامها را در تاریخچه گفتگوی دو کاربر تصوّر کنید. به این ترتیب، میتوان در استخراج پیامها بر اساس مقاطع زمانی اقدام کرد که در اینجا، مقاطع زمانی پیامها به عنوان «نمره» درنظر گرفته میشود.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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
در اینجا فرض میشود که شما از قبل یک پروژه لاراول جدید در دایرکتوری ریشه وبسایت خود نصب کردهاید. سپس باید موارد مربوط به تأییدیه ورود را انجام داده و از طریق مرورگر، از اپلیکیشن خود بازدید نمایید.
در صورتی که چنین کاری را انجام ندادهاید، فرمانهای زیر را اجرا نمایید.
1 2 3 4 5 6 7 8 9 10 11 | 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 بسازید.
1 2 3 4 5 6 7 | 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 پروژه را ویرایش کنید تا با جزئیات پایگاه داده مطابقت داشته باشد.
1 2 3 4 5 6 7 8 9 10 11 | 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 خودتان اضافه کنید.
1 2 3 4 5 | 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 بسازید که حاوی فرم محصولات جدید باشد. سپس محتوای زیر را در آن کپی کنید.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | @ 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 ایجاد کنید تا با استفاده از آن تمام محصولات اضافهشده را نمایش دهید.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | @ 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 خود را بروزرسانی کنید تا با کد زیر مطابقت داشته باشد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | <?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); 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 اضافه کنید.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 مطمئن شوید.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 به عنوان پایگاه داده وجود دارد. به عنوان یک تمرین یا چالش بیشتر میتوانید نمایش یک محصولات و محصولات مشابه را در زیر آن امتحان کنید.
راهنمایی: محصولات مشابه از تگهای یکسان با محصولی که نمایش داده میشود، استفاده میکنند.