用php创建一个守护进程

参考文章

示例代码

想到之前看到linux下,nohup和&一起使用可以创建一个后台job,例如:

nohup ping www.google.com & 1>/dev/null 2&>1

意思是让ping命令不接受hangup信号,也就脱离了终端的控制。该命令会在后台变成一个job:

[1]  33765

[1]表示job号码,33765代表进程id。使用jobs可以列出所有的jobs,也可以使用ps -ef|grep查看

bg %num命令可以将一个job后台执行

fg %num命令可以将一个命令前台执行  // num代表job号码

ctrl + z可以将一个job挂起暂停

kill %num 或 kill pid可以终止某个job

关于正则零宽断言的理解

最近项目里需要抓取别的网站的数据,于是重新回顾了一下正则,特别是零宽断言,下面是个人理解和总结。

基本概念说的很难理解,先上个例子:

<?php

preg_match_all("/(\w*)ing/", "aing bing", $mtches1);

preg_match_all("/(\w*)(?=ing)/", "aing bing", $mtches2);

print_r($matches1);
print_r($matches2);

刚开始我以为这两的结果是一样的,但其实不然,结果如下:

Array
(
    [0] => Array
        (
            [0] => aing
            [1] => bing
        )

    [1] => Array
        (
            [0] => a
            [1] => b
        )

)
Array
(
    [0] => Array
        (
            [0] => a
            [1] => 
            [2] => b
            [3] => 
        )

    [1] => Array
        (
            [0] => a
            [1] => 
            [2] => b
            [3] => 
        )

)

还是太天真。

于是从匹配过程上分析一波。

0a1b2i3n4g5 6b7i8n9g : 数字代表位置

第一个:先从位置0开始匹配,后的字符a匹配到\w,继续后面ing虽然能匹配到\w,但是它们是确定字符,所以匹配到第一个aing,于是继续从位置5开始,后面是空格,匹配不到\w,到位置6,b匹配到\w,继续向后,匹配到第二个bing,结束。于是分组1捕获的就是a和b。

第二个:同样从位置0开始,后的字符a匹配到\w,跳到位置1,此时控制权会交给后面的(?=ing),它断言位置1后面是ing字符,一看还真是,于是匹配到a,其实这里分不分组都一样,因为(?=exp)是零宽度,就是只占一个位置,继续从位置1开始往后匹配(因为(?=exp)是零宽),为什么空值成了第二个匹配到的值,我的理解是:因为*可以匹配空,也是零宽度,并且可以匹配到ing,类似于\0这种表示,也是就是说aing可能表示成a\0b\0i\0g\0这种。在往后就跟前面一样了,所以最后结果是a \0 b \0。

如果上面的\w*改成\w+,那么\0就不会被匹配到了。

看着很ok,但其实以上分析是不对的,如果把aing bing改成ainging bing,如果按照以上步骤,那么第二种情况应该是a \0 aing \0 b \0,但运行结果没有a和\0,????

其实先行断言的执行步骤是这样的先从要匹配的字符串中的最右端找到第一个ing(也就是先行断言中的表达式)然后 再匹配其前面的表达式,若无法匹配则继续查找第二个ing 再匹配第二个 ing前面的字符串,若能匹配 则匹配 ,意思是匹配是从最右边开始的,找到ing后,匹配b,找到第一个,再往前走,\0,再往前走aing,最后是\0。

负向零宽断言跟正向的两种情况是相反的。

果然正则是不适合地球人看的。

php7.2 不向后兼容的改动

做一个简单的总结,没有涵盖全部改动,因为不常见。

1. number_format()不会返回-0

因为-0这种写法很古怪,所以number_format(-0.01),会返回0。


2. 对数组强制转成对象时,会将整形key转成string

直接看代码,

$a = [0 => 1]; $a = (object)$a; print_r($a->{0}); //或者 print_r($a->{'0'});

现在允许这么写。但是key已经变成string类型,即’0’。


3. 不可以对不能count的对象调用count函数

没有实现Countable接口的对象都不能调用count函数,比如 count(1),count(‘a’) 在之前都会返回1,但是在7.2版本以后,会返回一个E_WARNING级别的错误,看一个例子:

class _count implements Countable{
    public function count() {
        return 10;
    }
}

$count = new _count();
echo count($count);  // 10

 


4. 可以对__PHP_Incomplete_Class类型的对象调用is_object方法

__PHP_Incomplete_Class,这个东西就是有一个对象,php没有找到定义它的地方,就会把该对象的类型变成这个,举个例子:

$serialized_object='O:1:"a":1:{s:5:"value";s:3:"100";}';

print_r( unserialize($serialized_object) );

// __PHP_Incomplete_Class Object ( [__PHP_Incomplete_Class_Name] => a [value] => 100 )

在解序列化的时候很常见,可以用unserialize_callback_func选项去避免,举个例子:

$serialized_object='O:1:"a":1:{s:5:"value";s:3:"100";}';

ini_set('unserialize_callback_func', 'mycallback'); // 设置回调

print_r( unserialize($serialized_object) );
function mycallback($classname) 
{
    require_once $classname.".php";
}

而在7,2版本对__PHP_Incomplete_Class对象调用is_object(),会返回true。


5. 使用一个未定义的常量,会产生一个E_WARNING级别的错误。

echo NAME;
// Warning: Use of undefined constant NAME

6. bcmod函数对于浮点数的操作

在之前,bcmod对于浮点数的运算类似于%操作符,但是在新版类似于fmod。

echo bcmod(4,3.5);  // 之前会返回1,现在会返回1,5

 

php:AES加密

常用的加密分为对称加密和非对称加密,对称加密就是发送方和接收方都用同一个秘钥进行加密解密,而非对称加密则使用一对公钥和私钥来进行加密,发送发只需要用接收方的公钥将数据加密即可。

AES是一种常见的对称加密算法,英语:Advanced Encryption Standard,又称Rijndael加密,它是一种分块加密方法,换句话说就是将明文块分成一组组小部分然后进行加密再组合,而根据分组大小可分为:AES-128,AES-196,AES-256三种,对应的分组大小分别是128bit,196bit,256bit。在网上转来的一张图:

而如何将这些明文块进行加密组合,又分为四种模式:ECB、CBC、CFB、OFB,除了第一种模式,后面三种模式都需要一个初始向量iv来辅助加密。初始向量的大小也是同区块大小保持一致,是个固定长度的随机字串,初始向量只是为了给加密算法提供一个可用的种子。

还有一点是,如果需要加密的源数据长度不是分组大小的整数倍,那么就需要对数据进行填充,一般的填充算法有PKSC5PKSC7填充算法。举个例子,如果源数据大小是10bytes,如果进行分组,还需要6bytes才能满足分组的要求。那么只需要在后面补上6个6即可:

x x x x x x x x x x 6 6 6 6 6 6

如果刚好不用填充,还需要再补齐区块大小个字节,这么做是为了在去除填充数据时,可以准确无误。

php中有一个mcrypt扩展,使用它提供的加密函数即可进行我们需要的对称加密。

mcrypt_encrypt(): 该方法接受5个参数,分别是算法名称,密钥,待加密源数据,加密模式,以及初始向量。最后一个参数,如果是ECB模式则不是必须的。需要注意的是如果密钥和初始向量的长度不是区块大小的长度,php会返回false。
mcrypt_decrypt(): 参数同加密方法一样,只不过源数据改成已加密后的数据。

php会使用 ‘\0 自动填充数据,你也可以使用自己的填充算法。

下面有一个简单的加密类,是参考网上的代码稍微修改的。使用的是PKSC7填充。

class Aes {
	private $init_key;

	private $auto_padding;

	private $aes_bytes;

	private $block_size;

	private $key;

	private $iv;

	private $map = [
		'128' => MCRYPT_RIJNDAEL_128,
		'196' => MCRYPT_RIJNDAEL_192,
		'256' => MCRYPT_RIJNDAEL_256,
	];

	public function __construct($init_key, $aes_bytes, $auto_padding){
		$this->init_key = $init_key;
		$this->auto_padding = $auto_padding;
		$this->aes_bytes = $aes_bytes;
		$this->block_size = mcrypt_get_block_size($this->map[$this->aes_bytes], MCRYPT_MODE_CBC);

		$this->key = substr( hash('sha256',$this->init_key), 0, $this->block_size );
		$this->iv  = mcrypt_create_iv( mcrypt_get_iv_size($this->map[$this->aes_bytes], MCRYPT_MODE_CBC) );
	}

	//CBC mode
	public function encrypt($data){
		$data = trim($data);
		if(!$this->auto_padding) {
			$data = $this->addPKCS7Padding($data);
		}
		$encrypt = mcrypt_encrypt($this->map[$this->aes_bytes], $this->key, $data, MCRYPT_MODE_CBC, $this->iv);
		return base64_encode( $encrypt );
	}

	public function decrypt($aes_data){
		$aes_data = base64_decode($aes_data);	
		$decrypt =  mcrypt_decrypt($this->map[$this->aes_bytes], $this->key, $aes_data, MCRYPT_MODE_CBC, $this->iv);
		return $this->stripPKCS7Padding( trim($decrypt) );
	}

	// padding
	public function addPKCS7Padding($data){
		$data = trim($data);
		$pad = $this->block_size - (strlen($data) % $this->block_size);
		if( $pad <= $this->block_size ) {
			$padStr = chr($pad) ;
			$data .= str_repeat($padStr, $pad);
		}
		return $data;
	}

	public function stripPKCS7Padding($data){
		$num = ord(substr($data,-1));
		return substr($data,0,-$num);
	}
}
$aes = new Aes('sama','128',false);
$en = $aes->encrypt('helloworldmydear');
$de = $aes->decrypt($en);

测试可行。

apache静态文件缓存的一些配置

在web应用中,合理利用缓存很重要。对于静态文件css/js/image/html等,可以使用http缓存。

每个浏览器都自带了 HTTP 缓存实现功能。您只需要确保每个服务器响应都提供正确的 HTTP 标头指令,以指示浏览器何时可以缓存响应以及可以缓存多久。

一般常用的会用Exprise,Last_Modified等头信息控制缓存。Cache-Control 标头是在 HTTP/1.1 规范中定义的,取代了之前用来定义响应缓存策略的标头(例如 Expires)。所有现代浏览器都支持 Cache-Control,因此,使用它就够了。

  • 每个资源都可通过 Cache-Control HTTP 标头定义其缓存策略
  • Cache-Control 指令控制谁在什么条件下可以缓存响应以及可以缓存多久

具体有一盘文章可以参考,http缓存,这儿就不多细说了。

既然缓存这么重要,那么服务器怎么配置很关键。这儿又有个常见服务器配置文件标准范例,找到你的服务器,参照着配置就行。

从上面来看,主要就两个标志很重要,Cache-Control和Etag,apache如何去配合使用这两个特性呢,假如我的根目录 的所有的css/js/image等静态文件都需要一个月的缓存,而且需要配合Etag的特性去保持更新。

首先看Cache-Control如何设置,在http.conf或者.htaccess(如果允许),加上一点代码:

<IfModule mod_headers.c>
    <File ~ "./(gif|jpg|jpeg|png|css|js)$">
          Header set Cache-Control  "max-age=2592000,public"
    </File>
</IfModule>

意思是说,如果加载了mod_headers这个模块,匹配文件以jpg,css,js等结尾的,在响应头内加上Cache-Control选项,缓存一个月(其实应该按照需求去设置)。用正则匹配的话可以使用FileMatch容器。

先看看不加这段代码。访问一张图片是怎样响应的

可以看到,响应里并没有cache-control,然后我们加上设置,重启,再访问一次看看

已经有了,一个月的缓存时间,很好。

cahce-control已经有了,还剩个Etag,其实apache默认就支持Etag,不用去设置,浏览器也会自动发送If-None-Match头去验证,很多工作都是自动的,但是它也有一些不同的生产方法。

apache支持FileEtag这个命令,官方介绍,它的参数主要有:INode,Size,Mtime,All,None

Inode是指每个文件的在文件系统里的i-node,Mtime指文件上次修改的时间,Size指大小,None就是不在响应里加上Etag标志。All就是FileEtag INode MTime Size

一般不用Inode,因为默认的ETag使用到的Innode会导致相同的文件在分布式服务器上产生的ETag不同。

看上图,我们访问后,产生的Etag : 303f-556eb6039b504,能看出来只使用了两个属性,如果我们想使用All去生成,那么可以:

<Direcrtory doucment_root>
     FileEtag All
</Directory>

重启,在访问一次,

很好发生变化了。变成 “9000000007bd9-303f-556eb6039b504″,显然多了一个。

平常开发中,可能会有不一样的需求,每个文件都可能需要不一样的缓存策略。

杂文一篇,有错望指出。

 

数据结构 – 反转链表

假如我们已经构造了如下链表:

头结点->6->5->4->3->2->1->null

现在要求反转链表,使其变成:

头结点->1->2->3->4->5->6->null

思路分析:既然是反过来,那么就从反过来后的第一个元素入手,即null,让第一个结点的next先指向null,类似: 6->null这个结构,继续这个思路,那下一步就是让5指向刚刚这个结构,依次类推,知道最后一个1指向2->3->4->5->6->null

代码:

// 2. 链表反转: 假设有链表6->5->4->3->2->1,反转链表成为1->2->3->4->5->6
function reverse(LineStruct $linelist) {
	// 第一个结点
	$pre = null;
	$leftNodes = $linelist->next;	
	while($leftNodes) {
		① $temp = $leftNodes->next;
		② $leftNodes->next = $pre;
		③ $pre = $leftNodes;
		④ $leftNodes = $temp;
	}
	$headNode = new LineStruct('',$pre);
	return $headNode;
}

先从第一遍开始分析,$leftNodes为6->5->4->3->2->1->null,之所以叫leftNodes,抽象表示剩下的结点,① 我们先用$temp保存6的下一个结点,即5->4->3->2->1->null,② 然后让6指向预先定义的pre,第一个当然是null,这个pre后面会改变。得到的结构是6->null,③ 接着我们用pre来保存这个结构,最后也是最重要的一部步,④将$leftNodes用$temp的值替换,那么下次循环就是从5这个结点开始了,先保存剩下的结点,,即4->3->2->1->null,然后5会先指向刚刚的pre,即6->null,变成5->6->null,这个结构一会再保存为pre,然后再讲$leftNodes置为$temp,直到最后一个$leftNodes = null,跳出循环,这时候的$pre就是1->2->3->4->5->6->null。

所以整个思路里最重要的就是保存当前结点的下一个结点,即保存剩下的结点。

 

不要再用传统分页

原文地址:http://blog.csdn.net/yejr/article/details/70039403

在开发项目的过程中随时可能遇到深度分页的情况,比如:

select * from table order by id desc limit 200000,10;

如果explain这个语句,结果是这样

id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE yd_stock ALL NULL NULL NULL NULL 233355 Using filesort
 没有使用任何的索引,全表扫描了20万数据。这个就会使查询的效率下降,针对这种情况可以优化。
 推荐使用inner join,比如将sql改成

select * from table a inner join (select id from table order by id desc limit 200000,10) b using(id)

这个sql的执行耗时比上一个提高了几十倍。explain后面的子句

id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE yd_stock index NULL PRIMARY 3 NULL 200010 Using index
 使用了主键索引,所以这个查询会很快,自然整个语句的执行速度就上去了

php中的socket一点总结

socket俗称套接字,是网络通信编程的核心。php开发中也会用到,关于socket在php中的应用,做一点总结。

socket在TCP/IP协议中,属于应用层和传输层中间,相当于一层接口,将复杂的协议内容藏在后面。

既然socket用于网络通信,那么就以聊天室为例子:

首先搭建一个聊天室,我们需要一个服务端,作为一个平台,用于让用户连接,我们可以用socket搭建一个服务端。

先贴上代码

<?php
/*
   聊天室服务器端
 */

/* 防止连接超时 */
set_time_limit(0);

$host = "127.0.0.1";
$port = 8888;
$reads = [];
$write = $except = null;

// 建立socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if(!is_resource($socket)) die("init socket fail");

// 可以重复利用本地ip
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);

// 绑定地址和端口
socket_bind($socket, $host, $port);
// backlog队列中最多有十个排队等待处理的链接
socket_listen($socket, 10);

$reads[] = $socket;
while(true){
	// 最初有一个服务器端监听的socket
	// 当数组中的某个有活动时,才会返回
	// 返回时剔除了$read里不可操作的套接字
	// 并不是服务端监听的套接字会变成可读,用户连接的套接字也会变成可读
	// 如果是服务器监听套接字有活动,创建一个新的socket用于处理用户的请求
	// 如果是用户连接套接字有活动,处理用户消息
	$readUps = $reads;
	$return = @socket_select($readUps, $write, $except, NULL);
	if($return === false) {
		die("Failed to listen for clients: ".socket_strerror(socket_last_error()));
	}

	foreach($readUps as $k => $_socket){
		// 有活动的是服务器监听的套接字
		if($_socket === $socket){
			// 相当于用户连接建立的套接字,用于处理用户传来的数据
			$connect = socket_accept($socket);
			$reads[] = $connect;
			continue;
		}
		// 获取用户消息
		// $data为引用传递
		if(!socket_recv($_socket, $data, 1024, 0) || !$data){
			socket_close($_socket);
			continue;
		}
		echo $data;die;
		// preg_match("#Sec-WebSocket-Key: (.*?)\r\n#", $buffer, $match) && $key = $match[1];

		// $key .= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
		// $key = sha1($key);
		// $key = pack('H*', $key);
		// $key = base64_encode($key);

		// $upgrade =
		//         "HTTP/1.1 101 Switching Protocols\r\n" .
		//         "Upgrade: websocket\r\n" .
		//         "Connection: Upgrade\r\n" .
		//         "Sec-WebSocket-Accept: {$key}\r\n\r\n";
	}
	sleep(1);
}

这段代码中主要一个是socket_select还有一个就是socket_accept

首先socket_select:

socket_select 接受三个套接字数组,分别检查数组中的套接字是否处于可以操作的状态(返回时只保留可操作的套接字)
使用最多的是 $read,因此以读为例
在套接字数组 $read 中最初应保有一个服务端监听套接字
每当该套接字可读时,就表示有一个用户发起了连接。此时你需要对该连接创建一个套接字,并加入到 $read 数组中
当然,并不只是服务端监听的套接字会变成可读的,用户套接字也会变成可读的,此时你就可以读取用户发来的数据了
socket_select 只在套接字数组发生了变化时才返回。也就是说,一旦执行到 socket_select 的下一条语句,则必有一个套接字是需要你操作的

再来是socket_accept:

此函数接受唯一参数,即前面socket_create创建的socket文件(句柄)。返回一个新的资源,或者FALSE。本函数将会通知socket_listen(),将会传入一个连接的socket资源。一旦成功建立socket连接,将会返回一个新的socket资源,用于通信。如果有多个socket在队列中,那么将会先处理第一个。关键就是这里:如果没有socket连接,那么本函数将会等待,直到有新socket进来。

其实就是,如果有一个客户端连接服务器了,那么就创建一个新的socket用于跟服务器端交流

如果前面不用socket_select在没有socket的时候阻塞住程序,那么就卡在这里永远无法结束了。

服务器端代码好了,下面就是客户端,代码如下:

<?php
/*
   聊天室客户端
 */

/* 防止连接超时 */
set_time_limit(0);

$host = "127.0.0.1";
$port = 8888;

// 建立socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if(!is_resource($socket)) die("init socket fail");

echo "trying connect $host on port : $port".PHP_EOL;
if(!socket_connect($socket, $host,$port)) die("connenct fail");
echo "success!";
 
socket_write($socket, "hello", 5);

socket_close($socket);

我们连接服务端,然后发送了一个hello,窗口下已经能看到输出了

这里有个地方需要注意,如果我们在服务端的代码这块加上输出

$return = @socket_select($readUps, $write, $except, NULL);var_dump($readUps);

你会发现有两个输出,而且是不一样的socket,?????

因为client连接server时,socket_select检测到服务端监听的socket有活动,所有socket_select返回了,代码往下执行了,接着socket_accept新建了一个socket,紧接着client又socket_write,向server发送信息,socket_select检测到刚刚accept新建的socket(也就是客户端过来的连接)有活动,于是socket_select再次返回,于是代码继续执行。所以会输出两次。

php命名空间的一点总结

现在很多框架都是采用mvc模式,所以大部分框架都有命名空间的概念,比如

<?php
namespace test;
use core\control;
class testControl extends control{}

这样做主要是为了区分文件,而不至于出现载入两个相同名称类等问题,在开发中经常时间长了就容易混淆,所以这里做个总结。

  1. use关键字不等于自动加载,其实框架里很多地方使用use,便可以使用对应的类,不是因为use有加载的功能,而是框架实现了__autoload这个魔术方法。

2. use会自动起别名,默认是类名。

3. 文件如果有namespace关键字,那么整个文件便处于对应的域下,在使用定义的类时要带上空间名,这里经常会出问题。

下面举个例子说明:

同一目录下有1.php和2.php两个文件,如下

1.php:

<?php
namespace one;
class one{
	static public function index(){
		echo "helloworld";
	}
}

2.php

<?php
namespace two;
use one\one;

class two{
	public function __construct(){
		one::index();
	}

	static public function dl($className){
		if($className == "one\one") require "1.php";
	}
}
spl_autoload_register(array('two','dl'));
$two = new two(); 

一运行会报错


Fatal error: Uncaught exception 'LogicException' with message 'Passed array does not specify an existing static method (class 'two' not found)' in D:\Xampp\htdocs\2.php:14 Stack trace: #0 D:\Xampp\htdocs\2.php(14): spl_autoload_register(Array) #1 {main} thrown in D:\Xampp\htdocs\2.php on line 14

提示找不到two这个类,很显然,因为two类是在two这个命名空间类,所以应该改成

spl_autoload_register(array('two\two','dl'));

这个类似于目录的绝对路径,前面其实还有个\符号,但是一般都省略,但是如果不use而是直接调用,那么这个\就不能省略。

但是为什么

one::index()

这样写不报错,照理说应该是会理解成two\one才对,其实use会自动起别名,

use one\one;

等同于

use one\one as one;

所以上面的写法会理解成

one\one::index()

 

 

 

ElasticSearch接触后的一点心得

最近项目需要使用elasticsearch来加速数据的搜索,上网找了一下教程,发现不是那么简单。光安装这一块就费了很大功夫。

中文文档安装head插件文档索引时报错

首先在windows下安装elasticsearch,必须的条件就是java环境,然后去官网下载压缩包解压,配置环境变量,然后执行elasticsearch.bat,访问http://localhost:9200测试是否启动。

启动的时候可能会报错,大概是java虚拟环境的内存分配不合理。如图

更改config/jvm.options,增加

-Xms512m
-Xmx1g

 

然后是head插件的安装,5.*以上版本bin/elasticseach-plugin.bat没法直接在命令行下使用。只能去github上clone项目后,再自行安装。

安装后访问,http://localhost:9100测试,这儿可能会提示集群未连接,这是因为浏览器限制了跨域,打开config/elasticsearch.yml,增加以下两句接即可。

 http.cors.enabled: true
 http.cors.allow-origin: "*"

一切就绪开始使用时,又出现问题,为某个文档索引时,命令如下:

curl -X PUT ""http://localhost:9200/megacorp/employee/1?pretty" -d "{"name":"laokiea","age":20}"

执行就会报错failed_to_parse,应该是解析json的时候出错。查询得知,windows使用双引号,需要加\,类似于转义。

elasticsearch有php的api,研究了下,可以很轻松的建立索引,索引文档等,但是需要composer依赖。下载安装composer,编写composer.json

{
    "require": {
        "elasticsearch/elasticsearch": "~5.0"
    }
}

然后composer install命令,即可在项目文件夹下创建vendor目录

举例针对一个产品表,10万条数据,全部索引,代码(用的TP框架)如下:


require "././././vendor/autoload.php";
public function createIndexAndBatchIndex()
    {
    	$params['index'] = "sys_config";
    	$client = \Elasticsearch\ClientBuilder::create()->build();
    	$client->indices()->delete($params);
    	$client->indices()->create($params);

    	//索引文档
    	$Model = M("ranzhi_100001.product","yd_");
    	$total = $Model->field(["count(*)"=>"total"])->find()["total"];
    	for($i=0;$i<ceil($total/5000);$i++){
    		$begin = 5000*$i;
    		$end = 5000*($i+1);
    		$products = $Model->field(["id","sname","name"])->limit($begin,$end)->select();
    		$documents = ['body'=> []];
    		$j = 1;
    		foreach ($products as $product) {
    			$documents['body'][] = ["index" => ["_index" => "sys_config", "_type" => 'config', "_id" => $begin+$j]];
    			$documents['body'][] = ["sname"=>$product['sname'],"name" => $product['name']];
    			$j++;
    		}
    		$client->bulk($documents);
    		unset($documents);
    	}
    	echo "all has been indexed";
    }

结果如图

100000+数据全部被索引。


03.01 总结:

通常项目里使用elasticsearch,做法是,拷贝需要搜索的数据到elasticsearch,然后用其做搜索查询,但是可能会有 并发操作的问题,举个例子,比如有100件商品,A,B两个人同时去卖一个商品,

那可能就会造成A,B 都以为只减少了一个产品, 但实际上却减少了两个。这有点类似数据库的锁问题。

这里可以有两个方法解决:

第一种,对文档的操作通常会有版本号,比如创建一个文档返回的结果里,_version是1,然后再去执行一次,这次已经变成更新了,_version也会随之增加成2,如果这时候我们再去访问一次但是在地址后面带上?version=1,表示说只希望对version为1的文档进行操作。执行就会返回409错误。再回到上面的问题,A或B在做更新时,先取得最新的文档信息,然后再请求时附带上信息里的_version信息,如果有别人已经修改过,那么_version信息也会被更改,那么此次请求肯定是不成功的。

第二种,还是类似上面的做法用版本号去控制,只不过不是使用系统分配的版本号,而是使用自定义的,比如你的系统里有某些信息(时间戳等)可以充当。自定义的版本号必须是正数且大于0,请求时在查询字符串后面带上?version=your version&version_type=external。


3.6总结:

今天开始尝试编写代码。首先第一个问题就是分词,elasticsearch自带好几种分词器,对中文也有chinese分词,但是效果不是很好,所以采用ik分词器。对中文分词实现的很好。

先去github上clone项目。再利用maven打包,mvn -package,打包成jar文件,放入es目录下的plugins下,重启es,报错,提示ik版本与es版本不符合,于是改成直接下载tar压缩包,地址:https://github.com/medcl/elasticsearch-analysis-ik/releases,选择对应版本,解压后直接放入es目录下的plugins/ik,无需打包。

然后测试:

curl -X GET "http://localhost:9200/_analyze?analyzer=ik_max_word&text=你好啊&pretty"

出现乱码并没有按照中文分词,原因是windows下curl默认使用的编码是gbk。需要接受utf-8的编码。于是写一段代码转成%xx的格式:

<?php
$str = "你好啊";
$utf = '';
$str = iconv($str, "GB2312","UTF-8");
for($i=0;$i<strlen($str);$i++){
// %表示转义
$utf .= sprintf("%%%02X",ord(substr($str,$i,1)));
}

再测试成功,说明中文分词可以。


3.7总结

放上索引数据的脚本:

 #!/usr/bin/env php
<?php

$v_autoload =  "../../vendor/autoload.php";
require $v_autoload;

// 以下都是测试数据
$user = "root";
$pass = "";
$dbname = "ranzhi_100001";
$host = "127.0.0.1";
$batchNum = 5000;
$es_hosts = ["localhost:9200"];

$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "set names utf8mb4",
];
try{$pdo = new PDO("mysql:dbname=".$dbname.";host=".$host,$user,$pass,$options);}
catch(PDOException $e){
     die("PDO init fail : ".$e->getMessage());
}

$countSql = "SELECT count(*) as count from yd_stock";
$count = $pdo->query($countSql)->fetch(PDO::FETCH_OBJ)->count;

try{$client = \Elasticsearch\ClientBuilder::create()->setHosts($es_hosts)->build();}
catch(Exception $e){
     die("Elasticsearch init fail : ".$e->getMessage());
}
// 为库存产品相关产品建立索引
$index = [
     "index" => '1pei',
     "body" => [
          "settings" => [
               "number_of_shards" => 5,   // 索引数据分配到三个主分片上
               "number_of_replicas" => 2, // 每一个主分片都有两个复制分片
          ],
          "mappings" => [
               // 对stockProduct类型映射,(只对可能用于搜索的属性索引)
               "stockProduct" => [
                    // 查询到的文档主体以json格式保存在_source字段中
                    "_source" => ["enabled" => true],

                    // 此选项表示在该类型下索引文档,id默认从sid这个字段中生成
                    "_id" => ["path" => "sid"],

                    // 搜索禁用_all,如果启用,类似一个独立的字段,分词器采用ik
                    "_all" => [
                         "enabled" => false,
                         "analyzer" => 'ik_max_word',
                    ],
                    // 此设置表示如果该类型增加了新的字段,会抛出一个异常,但是可以在内部对象中设置成true
                    "dynamic" => "strict",

                    // 建立一个动态模板用于查询相同类型下的相同字段,但是类型不同的情况
                    "dynamic_templates" => [
                         "es" => [
                              "match" => "*_es",
                              // 规定匹配的字段只能是string类型
                              "match_mapping_type" => "string",
                              "mapping" => [
                                   "type" => "string",
                                   "index" => "analyzed",
                                   "analyzer" => "ik_max_word",
                              ],
                         ],
                         "es_path" => [
                              // 用于内部对象
                              "path_match" => "info.name",
                              "match_mappping_type" => "string",
                              "mapping" => [
                                   "type" => "string",
                                   "index" => "analyzed",
                                   "analyzer" => "ik_max_word",
                              ],
                         ],
                    ],

                    "properties" => [
                         "sid" => ["type" => "long"],
                         "pid" => ["type" => "long"],
                         "partid" => ["type" => "long"],
                         "productName" => ["type" => "text","index" => "analyzed","analyzer" => 'ik_max_word'],
                         "sname" => ["type" => "text","index" => "analyzed","analyzer" => 'ik_max_word'],
                         "pn" => ["type" => "text"],
                         "oem" => ["type" => "text"],
                         "manufacturer" => ["type" => "text","index" => "analyzed","analyzer" => 'ik_max_word'],
                         "madein" => ["type" => "text","index" => "analyzed","analyzer" => 'ik_max_word'],
                         "dwgno" => ["type" => "text"],
                         "materialNumber" => ["type" => "text"],
                         "inStockNo" => ["type" => "text"],
                         "standard" => ["type" => "text","index" => "analyzed","analyzer" => 'ik_max_word'],
                         "unit" => ["type" => "text","index" => "analyzed","analyzer" => 'ik_max_word'],
                         "store" => ["type" => "long"],
                         "shelf" => ["type" => "long"],
                         "count" => ["type" => "long"],
                         "dynacount" => ["type" => "long"],
                         "chainID" => ["type" => "long"],
                         "provider" => ["type" => "long"],
                         "providerName" => ["type" => "text","index" => "analyzed","analyzer" => 'ik_max_word'],
                         "storeName" => ["type" => "text","index" => "analyzed","analyzer" => 'ik_max_word'],
                         "shelfName" => ["type" => "text","index" => "analyzed","analyzer" => 'ik_max_word'],
                         "partGroupName" => ["type" => "text","index" => "analyzed","analyzer" => 'ik_max_word'],
                    ],
               ],
          ],
     ],
];
try{if($client->indices()->getSettings(["index"=>"1pei"])) goto _index;}
catch(Exception $e){}
$client->indices()->create($index);

_index:
// 索引数据
$allDataSql = "SELECT t4.name as partGroupName, s.id as sid, p.id as pid, p.partid, p.name as productName, p.sname,p.pn,p.oem, p.manufacturer, p.madein, p.dwgno, p.materialNumber, p.standard, p.unit,s.store, store.name as storeName,shelf.name as shelfName,s.shelf,s.count,s.dynacount, s.inStockNo, s.provider,c.name as providerName,s.chainID FROM yd_stock s LEFT JOIN yd_product p on s.pid = p.id LEFT JOIN ranzhico.scm_parts t3 on p.partid = t3.id LEFT JOIN ranzhico.scm_parts t4 on t4.id = t3.parent LEFT JOIN crm_customer c on s.provider = c.id LEFT JOIN yd_store store on s.store = store.id LEFT JOIN yd_shelf shelf on shelf.id = s.shelf RIGHT JOIN (SELECT id FROM yd_stock limit %d,%d) b on b.id = s.id";
$properties = array_keys($index['body']['mappings']['stockProduct']['properties']);

for($i=0;$i < ceil($count/$batchNum); $i++){
     $beginTime = time();
     $begin = $batchNum * $i;
     $end = $batchNum * ($i+1);
     $stmt = $pdo->prepare(sprintf($allDataSql, $begin, $batchNum));
     $stmt->execute();
     $result =  $stmt->fetchAll(PDO::FETCH_OBJ);
     $documents = ['body'=> []];
     foreach ($result as $id => $stock) {
          $documents['body'][] = ["index" => ["_index" => "1pei", "_type" => 'stockProduct', "_id" => $stock->sid,"timestamp" => time(),]];
          foreach($properties as $property){
               $document[$property] = $stock->$property;
          }
          $documents['body'][] = $document;
     }
     $client->bulk($documents);
     unset($documents,$result);
     $currentMemory = memory_get_usage()/1024/2024;
     echo "[$i] time success from [$begin] to [$end], spent [".(time()-$beginTime)."]s, current memory is [".$currentMemory."M]".PHP_EOL;
}
echo "data indexed complately".PHP_EOL;

 

测试脚本:,成功索引。

查询数据:,中文分词可以使用。

研究了5.0接口文档,其中一个future(并发)用法值得尝试一下,如下

$params = [
		    'index' => '1pei',
		    'type' => 'stockProduct',
		    'id' => 433138,
		    'client' => [
		        'future' => 'lazy'
		    ]
		];
$future = $client->get($params);
// 此时返回的不是相应体,而是一个future对象

可以同时多个请求,并在一个数组内将所有的结果返回。


3.8总结

继续研究php api,最后给了一个插件用来转换curl和php客户端的dsl,挺方便的,依赖composer,执行命令安装

composer require ongr\elasticsearch-dsl

比如需要以下查询


        $boolQuery = new \ONGR\ElasticsearchDSL\Query\Compound\BoolQuery();
        $geoQuery = new \ONGR\ElasticsearchDSL\Query\TermLevel\termQuery("store",2);
        $matchQuery = new \ONGR\ElasticsearchDSL\Query\FullText\matchQuery("sname","离合器");
        $boolQuery->add($geoQuery,\ONGR\ElasticsearchDSL\Query\Compound\BoolQuery::FILTER);
        $boolQuery->add($matchQuery, \ONGR\ElasticsearchDSL\Query\Compound\BoolQuery::MUST);
      
        $search = new \ONGR\ElasticsearchDSL\Search();
        $search->addQuery($boolQuery);
      
    	$params = [
    		"index" => "1pei",
    		"type" => "stockProduct",
    		"body" => $search->toArray(),
    		// "client" => [
    		// 	"verbose" => true,
    		// ]
    	];

构造的结果类似

{
    "query": {
        "bool": {
            "must": [
                {
                    "match": {
                        "sname" : "离合器",
                    }
                }
            ],
            "filter": [
                {
                    "term": {
                        "store": "2",
                    }
                }
            ]
        }
    }
}
省去了自己编写dsl的麻烦,还容易出错。