說明:本文主要講述使用Redis做爲緩存加快頁面訪問速度。同時,做者會將開發過程當中的一些截圖和代碼黏上去,提升閱讀效率。php
備註:做者最近在學習github上別人的源碼時,發現好多在計算一篇博客頁面訪問量view_count時都是這麼作的:利用Laravel的事件監聽器監聽IP訪問該post,而後頁面每訪問一次,都刷新一次MySQL(假設MySQL)中post表的view_count字段,若是短期內大量的IP來訪問,那效率就不是很高了。何不用Redis來作緩存,等到該post達到必定瀏覽頁面後再刷新下MySQL,效率也很高。css
開發環境:Laravel5.1+MAMP+PHP7+MySQL5.5
html
Redis就和MySQL同樣,都是數據庫,只不過MySQL是磁盤數據庫,數據存儲在磁盤裏,而Redis是內存數據庫,數據存儲在內存裏,不持久化的話服務器斷電數據就被抹掉了。Redis數據存儲類型比較多,包括:字符串類型
、哈希類型
、列表類型
、集合類型
和有序集合類型
,而不像MySQL主要只有三類:字符串類型
、數字類型
和日期類型
。Redis可做緩存系統、隊列系統。jquery
首先得在主機上裝下Redis服務端,以MAC爲例,Windows/Linux安裝也不少教程:laravel
brew install redis //設置電腦啓動時也啓動redis-server ln -sfv /usr/local/opt/redis/*.plist ~/Library/LaunchAgents //經過launchctl啓動redis-server launchctl load ~/Library/LaunchAgents/homebrew.mxcl.redis.plist //或者經過配置文件啓動 redis-server /usr/local/etc/redis.conf //中止redis-server launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.redis.plist //卸載redis-server $ brew uninstall redis $ rm ~/Library/LaunchAgents/homebrew.mxcl.redis.plist //測試是否安裝成功,出現pong,輸入redis-cli進入redis自帶的終端客戶端 redis-cli ping
主機安裝完,就能夠在Laravel環境安裝下PHP的Redis客戶端依賴包:git
composer require predis/predis
predis是用PHP語言寫的一個redis客戶端包,Laravel的Redis模塊依賴於這個包。
phpredis是C語言寫的一個PHP擴展,和predis功能差很少,只不過做爲擴展效率高些,phpredis能夠做爲擴展裝進PHP語言中,不過這裏沒用到,就不裝了。github
推薦Laravel開發插件三件套,提升開發效率,能夠參考做者寫的Laravel學習筆記之Seeder填充數據小技巧:redis
composer require barryvdh/laravel-debugbar --dev composer require barryvdh/laravel-ide-helper --dev composer require mpociot/laravel-test-factory-helper --dev //config/app.php /** *Develop Plugin */ Barryvdh\Debugbar\ServiceProvider::class, Mpociot\LaravelTestFactoryHelper\TestFactoryHelperServiceProvider::class, Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
配置下config/cache.php文件把緩存驅動設爲redis,還有redis自身配置在config/database.php文件中:數據庫
//config/cache.php //'default' => 'redis', 'default' => env('CACHE_DRIVER', 'file'),//或者改下.env文件 'redis' => [ 'driver' => 'redis', 'connection' => 'default',//改成鏈接的實例,就默認鏈接'default'實例 ], //config/database.php 'redis' => [ 'cluster' => false, //就作一個實例,名爲'default'實例 'default' => [ 'host' => env('REDIS_HOST', 'localhost'), 'password' => env('REDIS_PASSWORD', null), 'port' => env('REDIS_PORT', 6379), 'database' => 0, ], ],
先作個post表,建個post遷移文件再設計表字段值,包括seeder填充假數據,能夠參考下這篇文章Laravel學習筆記之Seeder填充數據小技巧,總之表字段以下:bootstrap
class CreatePostsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->integer('category_id')->unsigned()->comment('外鍵'); $table->string('title')->comment('標題'); $table->string('slug')->unique()->index()->comment('錨點'); $table->string('summary')->comment('概要'); $table->text('content')->comment('內容'); $table->text('origin')->comment('文章來源'); $table->integer('comment_count')->unsigned()->comment('評論次數'); $table->integer('view_count')->unsigned()->comment('瀏覽次數'); $table->integer('favorite_count')->unsigned()->comment('點贊次數'); $table->boolean('published')->comment('文章是否發佈'); $table->timestamps(); $table->foreign('category_id') ->references('id') ->on('categories') ->onUpdate('cascade') ->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('posts', function(Blueprint $tabel){ $tabel->dropForeign('posts_category_id_foreign'); }); Schema::drop('posts'); } }
作一個控制器和一個路由:
php artisan make:controller PostController
Route::get('post/{id}', 'PostController@showPostCache');
利用Laravel的事件模塊,來定義一個IP訪問事件類,而後在事件監聽器類裏作一些邏輯處理如把訪問量存儲在Redis裏。Laravel的事件監聽這麼作:在EventServiceProvider裏定義事件和對應的監聽器,而後輸入指令:
//app/Providers/EventServiceProvider.php protected $listen = [ 'App\Events\PostViewCount' => [ 'App\Listeners\PostEventListener', ], ] //指令 php artisan event:generate
在app/Event和app/Listeners會生成事件類和監聽器類。
在PostController寫上showPostCache方法:
const modelCacheExpires = 10; public function showPostCache(Request $request, $id) { //Redis緩存中沒有該post,則從數據庫中取值,並存入Redis中,該鍵值key='post:cache'.$id生命時間10分鐘 $post = Cache::remember('post:cache:'.$id, self::modelCacheExpires, function () use ($id) { return Post::whereId($id)->first(); }); //獲取客戶端IP $ip = $request->ip(); //觸發瀏覽量計數器事件 event(new PostViewCount($post, $ip)); return view('browse.post', compact('post')); }
這裏Cache上文已經配置了以redis做爲驅動,這裏取IP,這樣防止同一IP短期內刷新頁面增長瀏覽量,event()或Event::fire()觸發事件,把$post和$ip做爲參數傳入,而後再定義事件類:
//app/Events/PostViewCount.php /** * @var Post */ public $post; /** * @var string */ public $ip; /** * Create a new event instance. * * @param Post $post * @param string $ip */ public function __construct(Post $post, $ip) { $this->post = $post; $this->ip = $ip; }
順便也把視圖簡單寫下吧:
<html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 上述3個meta標籤*必須*放在最前面,任何其餘內容都*必須*跟隨其後! --> <title>Bootstrap Template</title> <!-- 新 Bootstrap 核心 CSS 文件 --> <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css"> <style> html,body{ width: 100%; height: 100%; } *{ margin: 0; border: 0; } .jumbotron{ margin-top: 10%; } </style> </head> <body> <div class="container"> <div class="row"> <div class="col-xs-12 col-md-12"> <div class="jumbotron"> <h1>Title:{{$post->title}}</h1> <span class="glyphicon glyphicon-eye-open" aria-hidden="true">{{$post->view_count}} views</span> <h2>Summary:{{$post->summary}}</h2> <p>Content:{{$post->content}}</p> </div> </div> </div> </div> <!-- jQuery文件。務必在bootstrap.min.js 以前引入 --> <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script> <!-- 最新的 Bootstrap 核心 JavaScript 文件 --> <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <script> </script> </body> </html>
而後重點寫下事件監聽器邏輯:
class PostEventListener { /** * 同一post最大訪問次數,再刷新數據庫 */ const postViewLimit = 30; /** * 同一用戶瀏覽同一post過時時間 */ const ipExpireSec = 300; /** * Create the event listener. * */ public function __construct() { } /** * Handle the event. * 監聽用戶瀏覽事件 * @param PostViewCount $event * @return void */ public function handle(PostViewCount $event) { $post = $event->post; $ip = $event->ip; $id = $post->id; //首先判斷下ipExpireSec = 300秒時間內,同一IP訪問屢次,僅僅做爲1次訪問量 if($this->ipViewLimit($id, $ip)){ //一個IP在300秒時間內訪問第一次時,刷新下該篇post的瀏覽量 $this->updateCacheViewCount($id, $ip); } } /** * 一段時間內,限制同一IP訪問,防止增長無效瀏覽次數 * @param $id * @param $ip * @return bool */ public function ipViewLimit($id, $ip) { // $ip = '1.1.1.6'; //redis中鍵值分割都以:來作,能夠理解爲PHP的命名空間namespace同樣 $ipPostViewKey = 'post:ip:limit:'.$id; //Redis命令SISMEMBER檢查集合類型Set中有沒有該鍵,該指令時間複雜度O(1),Set集合類型中值都是惟一 $existsInRedisSet = Redis::command('SISMEMBER', [$ipPostViewKey, $ip]); if(!$existsInRedisSet){ //SADD,集合類型指令,向ipPostViewKey鍵中加一個值ip Redis::command('SADD', [$ipPostViewKey, $ip]); //並給該鍵設置生命時間,這裏設置300秒,300秒後同一IP訪問就當作是新的瀏覽量了 Redis::command('EXPIRE', [$ipPostViewKey, self::ipExpireSec]); return true; } return false; } /** * 更新DB中post瀏覽次數 * @param $id * @param $count */ public function updateModelViewCount($id, $count) { //訪問量達到300,再進行一次SQL更新 $postModel = Post::find($id); $postModel->view_count += $count; $postModel->save(); } /** * 不一樣用戶訪問,更新緩存中瀏覽次數 * @param $id * @param $ip */ public function updateCacheViewCount($id, $ip) { $cacheKey = 'post:view:'.$id; //這裏以Redis哈希類型存儲鍵,就和數組相似,$cacheKey就相似數組名,$ip爲$key.HEXISTS指令判斷$key是否存在$cacheKey中 if(Redis::command('HEXISTS', [$cacheKey, $ip])){ //哈希類型指令HINCRBY,就是給$cacheKey[$ip]加上一個值,這裏一次訪問就是1 $incre_count = Redis::command('HINCRBY', [$cacheKey, $ip, 1]); //redis中這個存儲瀏覽量的值達到30後,就往MySQL裏刷下,這樣就不須要每一次瀏覽,來一次query,效率不高 if($incre_count == self::postViewLimit){ $this->updateModelViewCount($id, $incre_count); //本篇post,redis中瀏覽量刷進MySQL後,把該篇post的瀏覽量鍵抹掉,等着下一次請求從新開始計數 Redis::command('HDEL', [$cacheKey, $ip]); //同時,抹掉post內容的緩存鍵,這樣就不用等10分鐘後再更新view_count了, //如該篇post在100秒內就達到了30訪問量,就在3分鐘時更新下MySQL,並把緩存抹掉,下一次請求就從MySQL中請求到最新的view_count, //固然,100秒內view_count仍是緩存的舊數據,極端狀況300秒內都是舊數據,而緩存裏已經有了29個新增訪問量 //實際上也能夠這樣作:在緩存post的時候,能夠把view_count單獨拿出來存入鍵值裏如single_view_count,每一次都是給這個值加1,而後把這個值傳入視圖裏 //或者平衡設置下postViewLimit和ipExpireSec這兩個參數,對於view_count這種實時性要求不高的能夠這樣作來着 //加上laravel前綴,由於Cache::remember會自動在每個key前加上laravel前綴,能夠看cache.php中這個字段:'prefix' => 'laravel' Redis::command('DEL', ['laravel:post:cache:'.$id]); } }else{ //哈希類型指令HSET,和數組相似,就像$cacheKey[$ip] = 1; Redis::command('HSET', [$cacheKey, $ip, '1']); } } }
這裏推薦下一本Redis入門書《Redis入門指南》(做者也是咱北航的,軟件學院的,竟然比我小一屆,慚愧。。不過俺們也參與寫過書,哈哈,只是參與,呵呵),快的話看個一兩天就能看完,也就基本入門了。還推薦一個Redis客戶端:Redis Desktop Manager,能夠在客戶端裏看下各個鍵值:
頁面視圖中能夠利用上面推薦的barryvdh/laravel-debugbar插件觀察下請求過程產生的數據。第一次請求時會有一次query,而後從緩存裏取值沒有query了,直到把緩存中view_count刷到MySQL裏再有一次query:
It is working!!!
不知道有沒有說清楚,有疑問或者指正的地方請留言交流吧。
總結:研究Redis和Cache模塊的時候,還看到能夠利用Model Observer模型觀察器來監聽事件自動刷新緩存,晚上在研究下吧,這兩天也順便把Redis數據存儲類型總結下,到時見。
歡迎關注Laravel-China。