Laravel5.2之Redis保存页面浏览量

2022-01-10 09:25:19 浏览数 (1)

说明:本文主要讲述使用Redis作为缓存加快页面访问速度。同时,会将开发过程中的一些截图和代码黏上去,提高阅读效率。

备注:最近在学习github上别人的源码时,发现好多在计算一篇博客页面访问量view_count时都是这么做的:利用Laravel的事件监听器监听IP访问该post,然后页面每访问一次,都刷新一次MySQL(假设MySQL)中post表的view_count字段,如果短时间内大量的IP来访问,那效率就不是很高了。何不用Redis来做缓存,等到该post达到一定浏览页面后再刷新下MySQL,效率也很高。

开发环境:Laravel5.1 MAMP PHP7 MySQL5.5

Redis依赖包安装与配置

Redis就和MySQL一样,都是数据库,只不过MySQL是磁盘数据库,数据存储在磁盘里,而Redis是内存数据库,数据存储在内存里,不持久化的话服务器断电数据就被抹掉了。Redis数据存储类型比较多,包括:字符串类型哈希类型列表类型集合类型有序集合类型,而不像MySQL主要只有三类:字符串类型数字类型日期类型。Redis可作缓存系统、队列系统。

Redis服务端安装

首先得在主机上装下Redis服务端,以MAC为例,Windows/Linux安装也很多教程:

代码语言:javascript复制
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客户端依赖包:

代码语言:javascript复制
composer require predis/predis

predis是用PHP语言写的一个redis客户端包,Laravel的Redis模块依赖于这个包。phpredis是C语言写的一个PHP扩展,和predis功能差不多,只不过作为扩展效率高些,phpredis可以作为扩展装进PHP语言中,不过这里没用到,就不装了。

推荐Laravel开发插件三件套,提高开发效率,可以参考之前写的Laravel5.2之Seeder填充数据小技巧:

代码语言:javascript复制
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
        */
        BarryvdhDebugbarServiceProvider::class,
        MpociotLaravelTestFactoryHelperTestFactoryHelperServiceProvider::class,
        BarryvdhLaravelIdeHelperIdeHelperServiceProvider::class,

配置下config/cache.php文件把缓存驱动设为redis,还有redis自身配置在config/database.php文件中:

代码语言:javascript复制
//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,
        ],

    ],

Redis存储浏览量字段

先做个post表,建个post迁移文件再设计表字段值,包括seeder填充假数据,可以参考下这篇文章Laravel学5.2之Seeder填充数据小技巧,总之表字段如下:

代码语言:javascript复制
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');
    }
}

做一个控制器和一个路由:

代码语言:javascript复制
php artisan make:controller PostController
代码语言:javascript复制
Route::get('post/{id}', 'PostController@showPostCache');

利用Laravel的事件模块,来定义一个IP访问事件类,然后在事件监听器类里做一些逻辑处理如把访问量存储在Redis里。Laravel的事件监听这么做:在EventServiceProvider里定义事件和对应的监听器,然后输入指令:

代码语言:javascript复制
//app/Providers/EventServiceProvider.php
protected $listen = [
        'AppEventsPostViewCount' => [
            'AppListenersPostEventListener',
        ],
       ] 

//指令
php artisan event:generate       

在app/Event和app/Listeners会生成事件类和监听器类。

在PostController写上showPostCache方法:

代码语言:javascript复制
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作为参数传入,然后再定义事件类:

代码语言:javascript复制
//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;
    }

顺便也把视图简单写下吧:

代码语言:javascript复制
<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>

然后重点写下事件监听器逻辑:

代码语言:javascript复制
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数据存储类型总结下,到时见。

0 人点赞