使用laravel自定义命令批量导入数据到redis集合里

laravel通过自定义命令导入数据到redis集合里

[TOC]

背景

目前网站浏量很大数据库压力也上来了,经过排查发现都是一些垃圾流量,主要入口是搜索和tag。目前的解决办法是:

  1. 搜索改为post请求,加入csrf token,这个不多说,

加了csrf token之后,除了人为搜索,基本上其它的一些垃圾的扫描器和蜘蛛都可以屏蔽掉了。当然模拟表单提交除外。

  1. tag整理成一个tag库,在通过tag检测时要先判断下tag,否则不入库查询

这里惟一的问题就是tag检测的问题,目前大概有7万个tag,放在数据库里检测不太合适,故放在redis里,用redis的无序集合来存放

思路

  • 使用laravel自定义命令,把整理好的tag数据同步至redis里
  • 在相应的程序部分加入判断逻辑
  • 用定时任务定时执行写好的laravel命令,用于同步数据

实践

环境信息

  • laravel 5.6
  • php 7.1+
  • redis 3.2
  • laravel predis扩展包
  • mysql 5.7+

相关配置

Redis配置

1
2
3
4
5
6
7
8
9
10
11
12
#config/database.php
'redis' => [
'client'=>'predis', //设置驱动程序为predis包
...
//新建一个redis连接,视自己情况而定,可以使用默认的
'tags' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 3,
],
],

Tags Model

1
2
// 创建tags模型
php artisan make:model Tags
1
2
3
4
5
6
7
8
9
10
11
12
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tags extends Model
{
//以下两个属性视自己的实际情况而定
protected $table = 'tags';
protected $connection = 'slave-one';
}

Tags表结构

1
2
3
4
5
6
7
8
CREATE TABLE `tags` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`tag` varchar(255) DEFAULT NULL,
`num` double DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `ClusteredIndex-20180907-093936` (`id`),
KEY `NonClusteredIndex-20180907-093926` (`tag`)
) ENGINE=InnoDB AUTO_INCREMENT=74587 DEFAULT CHARSET=utf8;

创建同步数据命令

1
php artisan make:command SyncTags

创建成功后,会自动在 app/Console/Commands/SyncTags.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
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Tags;
use Illuminate\Support\Facades\Redis;
class SyncTags extends Command
{
/**
* The name and signature of the console command.
* 命令的名字 即当前的可以使用 php artisan sync:tags 来执行命令
* @var string
*/
protected $signature = 'sync:tags';

/**
* The console command description.
* 命令说明 使用php artisan list 第二列可以看到对于命令的描述
* @var string
*/
protected $description = '更新Tags';

/**
* redis 实例
* @var Predis
*/
protected $redis;

/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
// 实例化redis并指定数据库连接
$this->redis = Redis::connection('tags');
}

/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//执行前的时间
$btime = time();
//更新之前集合里的tag数量
$update_before = $this->redis->scard('tags');
$this->info("更新之前Tags总数为:{$update_before}");
//------------------region--------------//
// Tags::chunk(500,function ($tags){
// foreach ($tags as $t){
// $this->redis->sAdd('tags',$t->tag);
// }
// });
//------------------endregion------------//
Tags::chunk(500,function ($tags){
$this->redis->pipeline(function()use($tags){
foreach ($tags as $t){
$this->redis->sAdd('tags',$t->tag);
}
});
});
//更新之后的tag数量
$update_after = $this->redis->scard('tags');
//新增多少个tag数量
$success = $update_after - $update_before;
//执行结束时间
$etime = time();
//总耗时
$time = $etime - $btime;
//打印结果
$this->info("更新之后Tags总数为:{$update_after}");
$this->info("更新成功{$success}个Tag");
$this->info("总耗时{$time}秒");
}
}

运行测试

1
2
3
4
php artisan sync:tags

[Symfony\Component\Console\Exception\CommandNotFoundException]
There are no commands defined in the "update" namespace.

如果我们运行得到上面的提示,是说明我们还没有把命令注册进去,需要在这个地方做下修改,否则命令无法执行

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
#app/Console/Kernel.php

<?php
namespace App\Console;

use App\Console\Commands\SyncTags;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
//在这里把我们写的命令添加进来,就可以了
SyncTags::class
];

/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')
// ->hourly();
}

/**
* Register the Closure based commands for the application.
*
* @return void
*/
protected function commands()
{
require base_path('routes/console.php');
}
}

只需要在Kernel类里的$commands数组里注册下你自己创建的命令类即可
运行尝试下

主要功能分析

chunk()方法

1
2
3
Tags::chunk(500,function($tags){
//todo logic
});

chunk 是组块的意思,方法的第一个参数是每次处理多少条结果,我这里设置的是500,处理完之后,会继续处理下个500条数据,直到把所有的数据处理完。

这个方法很有用,尤其是在大量数据处理的时候,目前我的tag有7.4万,如果我一次性拿出来完,占用内存是相当高,如果一次只拿出500个,相对于一次拿出7.4万的内存占用,性能高太多,当然也有人说,我用循环,一次拿一条来处理,但是要频繁的打开关闭数据库连接,对于数据库的压力也是很大的,所以目前这个方法是最优的选择

注意:在一些自更新的场景这个方法会有一些问题,所以在使用的时候,具体的可以参考下这篇文章 https://www.cnblogs.com/blog-dyn/p/7396592.html

Redis的命令

注:集合是不允许有重复数据

这里有用到几个redis 集合的命令

1
2
3
4
5
6
sadd('set1','value');
//把value添加到集合set1里,成功true 否则false
scard('set1');
//统计set1里有多少元素
sismember('set1','value')
//判断value是否属于集合set1

其它redis 关于集合的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
srem('set1','value');
//从集合set1中删除value
srem('set1');
//弹出集合set1中的第一个元素,被弹出的元素,集合里已不存在被弹出的元素
smove('set1', 'set2', 'ab');
// 移动'set1'中的'ab'到'set2', 返回true or false;此时 'set1'集合不存在 'ab' 这个值
smembers('set1');
//返回集合set1里所有元素
sinter('set1','set2');
//求两个集合的交集
sinterstore('set1','set2');
//等同于将'set2'的内容copy到'set1'中,并返回set1的结果
sinterstore('foo', array('set1', 'set2'));
// 把set1,set2的内容添加到foo里,并返回foo的结果
srandmember('set1')
//随机返回集合里的一个元素

提高redis的插入效率

我在上面贴的代码里,有一段是注释的,regoin块里的代码,先看下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Tags::chunk(500,function ($tags){
foreach ($tags as $t){
$this->redis->sAdd('tags',$t->tag);
}
});


Tags::chunk(500,function ($tags){
$this->redis->pipeline(function()use($tags){
foreach ($tags as $t){
$this->redis->sAdd('tags',$t->tag);
}
});
});

上面的惟一区别就是使用了redis的 pipeline(管道),这样做的好处是可以建立一个长连接,类似于上面chunk的概念,我打开一个长连接,一直把命令执行完毕再关闭,否则每执行一条redis命令,要建立一个连接。也是非常消耗资源的。我也做了对比,如下图:

时间消耗上少了3秒钟

判断tags是否存在

1
2
3
4
5
6
public function checkTag($kw){
$kw = trim($kw);
if(empty($kw)) return false;
$redis = Redis::connection('tags');
return $redis->sismember('tags',$kw);
}

定时任务

想到定时任务,熟悉linux的同学可能立刻想到的是 crontab,这个没有错,但是laravel提供了一种更方法的处理方法,不需要我们在crontab里加一大堆的定时任务,只需要加一条,其它的由laravel来处理,接下来我们来看看

1
2
3
crontab -e

* * * * * php /path-to-your-project/artisan schedule:run >> /dev/null 2>&1

然后在这个文件(app/Console/Kernel.php)里添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use ...
use App\Console\Commands\SyncTags;
class Kernel extends ConsoleKernel
{

protected $commands = [
SyncTags::class,//这个地方一定要注意,否则命令无法运行
];


protected function schedule(Schedule $schedule)
{
//下面表示每小时运行下这个命令
$schedule->command('sync:tags')
->hourly();
}

....
}

除了hourly()Laravel还提供了很多很好用的方法来让你控制运行的时间,这样就不用在crontab里写一大堆命令了。

具体的一些细节可以参照:http://laravelacademy.org/post/9000.html

结束

OK! 到此结束