一、需求介绍
一篇文章或者帖子的浏览次数的统计,如果只是每次增加一个浏览量 就到数据库新增/修改一个数据,请求频繁 用户量一多就出问题了。
二、解决方案
1、每次增加一个访问量就在缓存中去进行更改
2、达到一定数量后刷新改变Mysql数据库,这样数据也是准确的 效率也比直接每次刷新数据库要高出许多
本次教程是以测试项目为案例来实现此功能,如需在正式项目中开发,请自行集成!
三、功能实践
1、创建新项目:laravel new test
,在 web.php
中添加如下路由:
Route::get('/post/{id}', 'PostController@showPost');
2、创建控制器和对应方法:
php artisan make:controller PostController
3、创建数据库test
和数据表posts
,并修改 .env
文件:
随便插点数据进去。
4、加入事件监听:
在app/providers/EventServiceProvider
中的$listen
加入
protected $listen = [
'App\Events\PostViewEvent' => [
'App\Listeners\PostEventListener',
],
];
5、生成事件监听:
php artisan event:generate
6、在PostViewEvent.php
中写入如下代码:
<?php
namespace App\Events;
use App\Post;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class PostViewEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $ip;
public $post;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Post $post, $ip)
{
$this->post = $post;
$this->ip = $ip;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
7、在 app\Listeners\PostEventListener
中加入如下代码:
<?php
namespace App\Listeners;
use App\Events\PostViewEvent;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Post;
use Illuminate\Support\Facades\Redis;
class PostEventListener
{
/**
* 一个帖子的最大访问数
*/
const postViewLimit = 2;
/**
* 同一用户浏览同一个帖子的过期时间
*/
const ipExpireSec = 200;
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param PostViewEvent $event
* @return void
*/
public function handle(PostViewEvent $event)
{
$post = $event->post;
$ip = $event->ip;
// $ip = "127.2.1.31";
$id = $post->id;
//首先判断下ipExpireSec = 200秒时间内,同一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)
{
$ipPostViewKey = 'post:ip:limit:'.$id;
//Redis命令SISMEMBER检查集合类型Set中有没有该键,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;
}
/**
* 达到要求更新数据库的浏览量
* @param $id
* @param $count
*/
public function updateModelViewCount($id, $count)
{
//访问量达到300,再进行一次SQL更新
$post = Post::find($id);
$post->view_count += $count;
$post->save();
}
/**
* 不同用户访问,更新缓存中浏览次数
* @param $id
* @param $ip
*/
public function updateCacheViewCount($id, $ip)
{
$cacheKey = 'post:view:'.$id;
//这里以Redis哈希类型存储键,就和数组类似,$cacheKey就类似数组名 如果这个key存在
if(Redis::command('HEXISTS', [$cacheKey, $ip])){
//哈希类型指令HINCRBY,就是给$cacheKey[$ip]加上一个值,这里一次访问就是1
$save_count = Redis::command('HINCRBY', [$cacheKey, $ip, 1]);
//redis中这个存储浏览量的值达到30后,就去刷新一次数据库
if($save_count == self::postViewLimit){
$this->updateModelViewCount($id, $save_count);
//本篇post,redis中浏览量刷进MySQL后,就把该篇post的浏览量清空,重新开始计数
Redis::command('HDEL', [$cacheKey, $ip]);
Redis::command('DEL', ['laravel:post:cache:'.$id]);
}
}else{
//哈希类型指令HSET,和数组类似,就像$cacheKey[$ip] = 1;
Redis::command('HSET', [$cacheKey, $ip, '1']);
}
}
}
8、在PostController
控制器中添加如下代码:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
use App\Post;
use App\Events\PostViewEvent;
use Illuminate\Support\Facades\Cache;
class PostController extends Controller
{
private $cacheExpires = 60;//post文章数据缓存时间 s
public function showPost(Request $request,$id){
//存入缓存中,该键值key='post:cache'.$id生命时间60分钟
$post = Cache::remember('post:cache:'.$id, $this->cacheExpires, function () use ($id) {
return Post::whereId($id)->first();
});
//获取客户端请求的IP
$ip = $request->ip();
// $ip = "127.0.1.1";//手动更改ip 以不同ip访问,计数
//触发浏览次数统计时间
event(new PostViewEvent($post, $ip));
dump("当前预览文章的名称:".$post->title);
dump("当前预览数:".$post->view_count);
}
}
9、重启终端,浏览器访问 http://127.0.0.1:8000
,你会看到 redis
报错,接下来执行如下代码,安装redis
扩展。
注意:执行下面的命令之前,请确保你的
redis
已经安装好了。
composer require predis/predis
10、启动redis
,执行命令:redis-server
。
11、再次访问浏览器,你会看到如下结果:
最后:测试,上面的代码设置的是200
秒时间内,同一IP访问多次,仅仅作为1次访问量,所以等待200
秒后再次刷新,浏览数加1
。
Well Done!!!