不要再用传统分页

原文地址: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的麻烦,还容易出错。

JSONP初探

web开发中经常会遇到的问题就是跨域,由于浏览器的同源策略,跨域请求会被限制(请求是可以发出去,但是返回的数据拿不到),所以js中有很多解决跨域的方法,其中JSONP是一种比较简单的方式。

浏览器虽然会限制不同域名之前相互请求,但是却允许imgscript等标签的src属性跨域访问。比如

<img src="www.baidu.com/imges/xxx.img">

所以利用这个方法,只要和后端配合一下,可以达到跨域请求数据。方法如下

前端代码:

<html>
<!-- 动态的生成script标签,利用src属性 -->
<script>
var script = document.createElement("script");
script.src = "www.example.com/test.php?callback=sample";
document.body.appendChild(script);

function sample(returnData){
//......
}

</script>
<body></body>
</html>

注script的src,地址最后附带了一个参数 callback=sample,并且有一个具名函数与参数值同名因为script请求的内容会被当做js代码来执行。所以就需要后端在返回数据时做一点处理。

后端代码:

<?php

$callback = $_GET['callback']; // 拿到回调名称
$data = "helloworld";             // 需要返回的数据
echo $callback."('".$data."')";  // 以执行函数的语法形式返回数据

这样前端拿到返回的数据会执行sample函数,参数就是返回的数据,从而达到跨域取得数据。

来自知乎