前言:网站性能优化的场景需求
对于已有的mysql主从项目,应对数据量大时往往采取分库分表的做法,为了缩短页面响应采用一主多从的 主写+从读 的读写分离架构。
使用redis等级:一使用redis作为php的缓存层,存储常用、相对固定的公共数据;二:添加redis长用缓存,组成 mysql写+ redis读 的架构;三:甚至直接采用 redis读+写 的架构。mysql迁移redis需要后台程序的紧密配合。“读写分离”容易出现数据不一致的问题。
*面试*
优化相关
传统前后端优化途径:
1. 前端
减少请求次数:css精灵(小图合并到大图);
data-image(data-icon:src=data:image/jpg;base64;xx,小图合并到js文件);
2. 网关
web资源防盗链refer监测;
nginx限流,nginx负载均衡、nginx缓存静态资源、图片服务器、gzip等;
http2.0;
3. 后端、数据库
升级到php7,使用服务模式启动网站;
使用redis、memcache缓存;
mysql优化;
进阶:
1. 网关
mysql迁移redis,分布式集群部署;
2. 后端
添加针对高并发的消息队列,多线程、协程化,使用连接池;
mysql慢查询分析
分布式部署常见问题:
登录session共享问题;读写分离的同步数据问题。
另外还各种诸如图片数据库、对象存储等。
要同时在多台服务器上处理比如:库存超卖、订单支付问题需要频繁的过程校验,所以对并行任务串行化、使用一台机器、单一线程处理这种一致性问题最为稳妥。应对大数据量的情况采用消息队列,平衡服务器压力。
1.swoole的消息队列相关
利用swoole_table+协程可以使用队列,把这些驻内存的数据放到mysql、redis可持久化。
a.swoole_table共享内存表
服务器端:
//参考官方 https://wiki.swoole.com/wiki/page/292.html
$table = new Swoole\Table(1024); //$size参数指定表格的最大行数,必须为2的指数,如1024,8192,65536等
$table->column('fd', swoole_table::TYPE_INT);
$table->column('from_id', swoole_table::TYPE_INT);
$table->column('data', swoole_table::TYPE_STRING, 1024);
$table->create();
$serv = new Swoole\Server('127.0.0.1', 9501, SWOOLE_BASE, SWOOLE_SOCK_TCP);
//将table保存在serv对象上
$serv->table = $table;
$serv->on('receive', function ($serv, $fd, $from_id, $data) {
$pre = substr($data, 0, 1);
$data = substr($data, 1);
if($pre == \Swoole\Table::TYPE_STRING){
/*
foreach ($serv->table as $row) var_dump($row);
print_r($serv->table);
*/
$table = [];
foreach ($serv->table as $k => $row) {
$row['key'] = $k;
$table[] = $row;
}
$count = count($serv->table);
print_r("当前表行数". $count);
$serv->send($fd, json_encode($table, JSON_UNESCAPED_UNICODE));
}elseif($pre == \Swoole\Table::TYPE_FLOAT){
$key = $data;
$exist = $serv->table->exist($key);
if($exist){
$row = $serv->table->get($key);
$exist = $serv->table->del($key);
$data = json_decode($row['data'], true);
$serv->send($fd, "消费:" .($exist===true?'true':'false'). ' '. $data['order'].PHP_EOL);
}
}elseif($pre == \Swoole\Table::TYPE_INT){
$ret = $serv->table->set($fd, array('from_id' => $from_id, 'data' => $data, 'fd' => $fd));
$data = json_decode($data, true);
$serv->send($fd, "服务器:". ($ret===true?'true':'false'). " from ".$fd.' order:'.$data['order']);
}else{
print_r($data);
$serv->send($fd, 'others');
}
});
$serv->start();
客户端:
for($i=1; $i<=50; $i++){
go(function () use($i) {
$client = new \Swoole\Client(SWOOLE_SOCK_TCP);
usleep(mt_rand(10,1000));
$res = $client->connect('127.0.0.1', 9501, 0.5);
if (!$res) {
exit("connect failed. Error: {$client->errCode}\n");
}
echo "客户端:". $i. PHP_EOL;
$data = ['order'=>$i, 'data'=>md5(mt_rand(10,1000).time())];
echo \Swoole\Table::TYPE_INT. json_encode($data, JSON_UNESCAPED_UNICODE). PHP_EOL;
$client->send(\Swoole\Table::TYPE_INT. json_encode($data, JSON_UNESCAPED_UNICODE));
//echo $client->recv(). PHP_EOL;
$client->close();
});
}
$client = new \Swoole\Client(SWOOLE_SOCK_TCP);
$res = $client->connect('127.0.0.1', 9501, 0.5);
if (!$res) {
exit("connect failed. Error: {$client->errCode}\n");
}
$client->send(\Swoole\Table::TYPE_STRING. "GET_TABLE_INFO");
$table = $client->recv();
$client->close();
$table = json_decode($table, true);
while($row = array_pop($table)){
go(function () use($row) {
$client = new \Swoole\Client(SWOOLE_SOCK_TCP);
usleep(mt_rand(10,1000));
$res = $client->connect('127.0.0.1', 9501, 0.5);
if (!$res) {
exit("connect failed. Error: {$client->errCode}\n");
}
echo "处理{$row['fd']}发来的{$row['data']}". PHP_EOL;
$client->send(\Swoole\Table::TYPE_FLOAT. $row['key']);
$client->close();
});
}
swoole的协程对于普通处理效果不明显,而当任务中包含有mysql、redis、curl等的远程连接时才体现异步非阻塞的高效率。异步编程要始终考虑执行顺序的问题,上面例子的客户端中,sleep是真阻塞而不是异步的,下半部分读取swoole_table表回调报告完成、可以单独放到文件请求。
b.swoole+redis/mysql的消息处理
同上理,好处是多了持久化。
2.rabbitmq消息队列
特点:被动接受任务,不拒绝。选择持久化配置,若未完成,全部退出/php中断退出 再启动还会继续执行。
a.安装RabbitMQ消息队列
rabbitmq官方docker说明,参考《Rabbitmq集群高可用部署详细》这里部署普通模式。网络加入之间建立的mybridge,使用固定ip。
[]:~/tmp/dk/rabbitmq# docker pull rabbitmq
#docker run --name rabbit --network mybridge -e RABBITMQ_ERLANG_COOKIE='123456' -d rabbitmq
#docker cp rabbit:/etc/rabbitmq/rabbitmq.conf ./复制出配置文件
[]:~/tmp/dk/rabbitmq# docker run --name rbt1 -p 15672:15672 \
--hostname rbt1 \
--network mybridge --ip=172.1.12.13 \
-v /root/tmp/dk/rabbitmq/rabbitmq.conf \
-v /root/tmp/dk/rabbitmq/data13:/var/lib/rabbitmq/mnesia \
-e RABBITMQ_ERLANG_COOKIE='123456' \
-e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=123456 \
-d rabbitmq
docker run --name rbt2 --hostname rbt2 \
--network mybridge --ip=172.1.12.14 \
-v /root/tmp/dk/rabbitmq/rabbitmq.conf \
-v /root/tmp/dk/rabbitmq/data14:/var/lib/rabbitmq/mnesia \
-e RABBITMQ_ERLANG_COOKIE='123456' \
-e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=123456 \
-d rabbitmq
普通集群模式
[]:~/tmp/dk/rabbitmq# docker exec -it rbt1 bash
root@rbt1:/# rabbitmqctl stop_app
Stopping rabbit application on node rabbit@rbt1 ...
root@rbt1:/# rabbitmqctl join_cluster --ram rabbit@rbt2
Clustering node rabbit@rbt1 with rabbit@rbt2
root@rbt1:/# rabbitmqctl start_app
Starting node rabbit@rbt1 ...
completed with 0 plugins.
root@rbt1:/# rabbitmqctl cluster_status
Cluster status of node rabbit@rbt1 ...
[{nodes,[{disc,[rabbit@rbt2]},{ram,[rabbit@rbt1]}]},
{running_nodes,[rabbit@rbt2,rabbit@rbt1]},
{cluster_name,<<"rabbit@rbt2">>},
{partitions,[]},
{alarms,[{rabbit@rbt2,[]},{rabbit@rbt1,[]}]}]
root@rbt1:/# rabbitmq-plugins enable rabbitmq_management
然后可以登录访问 http://remote_host:15672/ 管理页面。
php添加amqp扩展,Dockerfile添加(cffycls/php7:1.8):
apk add rabbitmq-c-dev
pecl install amqp
b.消息添加和消费测试
用法参考一,参考二:phpstrom中输入new AMQPConnection(),按Ctrl和鼠标确认键进入amqp.php方法用法注释(php.net官方暂时找不到)。
生产者(publish.php):
date_default_timezone_set("Asia/Shanghai");
//配置信息
$conn_args = array(
'host' => '172.1.12.13',
'port' => '5672',
'login' => 'root',
'password' => '123456',
'vhost'=>'/'
);
$k_route = 'k_route_1'; //路由key,用来绑定交换机和队列
$e_name = 'e_switches'; //交换机名称
//创建连接和channel
$conn = new AMQPConnection($conn_args);
if (!$conn->connect()) {
die("Cannot connect to the broker!PHP_EOL");
}
$channel = new AMQPChannel($conn);
echo "<font color='red'>生产者</font>PHP_EOL已连接成功!准备发布信息...".PHP_EOL;
//创建交换机对象
$ex = new AMQPExchange($channel);
$ex->setName($e_name);
$ex->setType(AMQP_EX_TYPE_DIRECT); // 设置交换机类型
$ex->setFlags(AMQP_DURABLE); // 设置交换机是否持久化消息
//发送消息
$channel->startTransaction(); //开始事务
for($i=1; $i<=50000; ++$i){
usleep(100);//休眠1秒
$message = "消息数据".$i. ' '.date("Y-m-d H:i:s A");
echo "消息发送返回:".$ex->publish($message, $k_route).PHP_EOL;
}
$channel->commitTransaction(); //提交事务
$conn->disconnect();
消费者(consumer.php):
date_default_timezone_set("Asia/Shanghai");
//配置信息
$conn_args = array(
'host' => '172.1.12.13',
'port' => '5672',
'login' => 'root',
'password' => '123456',
'vhost'=>'/'
);
$k_route = 'k_route_1'; //路由key,用来绑定交换机和队列
$e_name = 'e_switches'; //交换机名称
$q_name = 'q_queue'; //队列名
//创建连接和channel
$conn = new AMQPConnection($conn_args);
if (!$conn->connect()) {
die("Cannot connect to the broker!".PHP_EOL);
}
$channel = new AMQPChannel($conn);
echo "<font color='red'>消费者</font>:".PHP_EOL ."已连接成功!准备接收信息...".PHP_EOL;
//创建交换机
$ex = new AMQPExchange($channel);
$ex->setName($e_name);
//direct类型:[AMQP_EX_TYPE_DIRECT,AMQP_EX_TYPE_FANOUT, AMQP_EX_TYPE_HEADERS or AMQP_EX_TYPE_TOPIC]
$ex->setType(AMQP_EX_TYPE_DIRECT);
$ex->setFlags(AMQP_DURABLE); //持久化
//创建队列
$q = new AMQPQueue($channel);
$q->setName($q_name);
$q->setFlags(AMQP_DURABLE); //持久化
$q->declareQueue();
//绑定交换机与队列,并指定路由键
$q->bind($e_name, $k_route);
//阻塞模式接收消息
echo "阻塞模式接收消息:".PHP_EOL;
while(True){
$q->consume(function ($envelope, $queue) {
$msg = $envelope->getBody();
echo '收到:'. $msg.PHP_EOL; //处理消息
sleep(1);//接收模拟处理休眠1秒
}, AMQP_AUTOACK); //ACK应答
}
$conn->disconnect();
在页面可以执行,只在cli交互模式下的才显示收到的结果,(发布者再web访问、消费者在php-cli处理)。上面开启事务:所有的消息 被接收才开始处理。
只要保持有一个以上的消费者在工作,任务就会继续进行,这是中间加入新的消费者(2个同时执行):
[]:~/tmp/dk/html# php rabbitmq/consumer.php
<font color='red'>消费者</font>:
已连接成功!准备接收信息...
阻塞模式接收消息:
收到:消息数据7201 2019-07-08 16:08:05 PM
收到:消息数据7202 2019-07-08 16:08:05 PM
... ...
像redis、mysql一样,使用swoole协程并发会有明显提速。
3.秒杀/红包场景
可以使用redis消息队列专门处理秒杀问题,核心是解决库存超卖问题,测试。
a.服务端sale.php:添加促销信息,初始化消息队列
require "../vendor/autoload.php";
//从集群当中读取
$servers = ['172.1.50.11:6379', '172.1.50.12:6379', '172.1.50.13:6379', '172.1.50.21:6379'];
define('PROMOTION_KEY_PREFIX', 'promotionList_');
define('TIME_LIMIT_PREFIX', 'timeLimit_');
//一、初始化:查出所有节点分布,lPush、lRange函数准备,缓存基本信息
$redisServers = [];
$slotNodes = [];
foreach ($servers as $addr){
//随机
$r = new Redis();
$server=explode(':',$addr);
$r->connect($server[0], (int) $server[1]);
$r->auth('123456');
$redisServers[$addr] = $r;
if(empty($slotInfo)){
//单一节点可以看到所有存在槽的节点
$slotInfo = $r->rawCommand('cluster','slots');
foreach ($slotInfo as $ix => $value){
$slotNodes[$value[2][0].':'.$value[2][1].' '.($ix+1)]=[$value[0], $value[1]];
}
}
}
$crc = new \Predis\Cluster\Hash\CRC16();
//注意method=lRange时args传参是($key, $start, $end)
$opt = function ($method, $key, ...$args) use (&$redisServers, &$slotNodes, &$crc) {
$code = $crc->hash($key) % 16384;
foreach ($slotNodes as $addr => $boundry){
if( $code>=$boundry[0] && $code<=$boundry[1] ){
$host =explode(' ', $addr)[0];
if(empty($args)){
return $redisServers[$host]->$method($key);
}elseif(count($args)==1){
return $redisServers[$host]->$method($key, $args[0]);
}else{ //...
return $redisServers[$host]->$method($key, $args);
}
}
}
};
//二、添加数据:集中添加商品,初始化消息队列
/**
* @param String $key 商品key
* @param int $stock 库存
*/
$createQueue = function (String $key, int $stock) use (&$opt) {
for ($i=0; $i<$stock; $i++){
$opt('lPush', $key, $i);
}
};
//第一个任务
go(function () use (&$opt, &$limit1, &$createQueue){
$limit1 = [
'goods_id'=>'9505900000',
];
$createQueue(PROMOTION_KEY_PREFIX. $limit1['goods_id'], 20);
$has = $opt('lLen', PROMOTION_KEY_PREFIX. $limit1['goods_id']);
print_r('创建goods1 '. $has. ' 个'. PROMOTION_KEY_PREFIX. $limit1['goods_id'].PHP_EOL);
$has = 0;
swoole_timer_tick(10, function ($timer_id) use (&$opt, &$limit1, &$has){
$now = $opt('lLen', PROMOTION_KEY_PREFIX. $limit1['goods_id']);
if($has != $now) {
$has = $now;
if ($now) {
print_r('goods1 还有个' . $has . '剩余' . PHP_EOL);
} else {
print_r('OK DONE1!'. PHP_EOL . PROMOTION_KEY_PREFIX . $limit1['goods_id'] . PHP_EOL);
Swoole\Timer::clear($timer_id);
}
}
});
});
//第二个任务:增加时段限制
go(function () use (&$opt, &$limit1, &$has, &$createQueue){
$limit2 = [
'goods_id'=>'2302500000',
'start'=>date("Y-m-d H:i:s", strtotime('+ 1 minutes')),
'end'=>date("Y-m-d H:i:s", strtotime('+ 5 minutes')),
];
$createQueue(PROMOTION_KEY_PREFIX. $limit2['goods_id'], 500);
//保存时间条件
$opt('lPush', TIME_LIMIT_PREFIX. $limit2['goods_id'], json_encode($limit2, JSON_UNESCAPED_UNICODE));
$has = $opt('lLen', PROMOTION_KEY_PREFIX. $limit2['goods_id']);
print_r('创建goods2 '. $has. ' 个'. PROMOTION_KEY_PREFIX. $limit2['goods_id'].PHP_EOL);
$has = 0;
swoole_timer_tick(2000, function ($timer_id2) use (&$opt, &$limit2, &$has){
$now = $opt('lLen', PROMOTION_KEY_PREFIX. $limit2['goods_id']);
if($has != $now) {
$has = $now;
if ($now) {
print_r('goods2 还有个' . $has . '剩余' . PHP_EOL);
} else {
print_r('OK DONE2!'. PHP_EOL . PROMOTION_KEY_PREFIX . $limit2['goods_id'] . PHP_EOL);
Swoole\Timer::clear($timer_id2);
}
}
});
});
b.客户端buy.php:消费数据
//每次购买请求的客户端工作--swoole常驻内存,缓存运行第一步
//一、初始化:查出所有节点分布,lPush、lRange函数准备,同服务端(略)
//二、消费数据:模拟分散请求
$goods1 = '9505900000'; //商品已知
$goods2 = '2302500000';
//无限制:100人抢 -- goods1=20个
go(function () use (&$opt, &$limit1, &$has, &$createQueue, $goods1){
$cart = [];
$cart['key'] = PROMOTION_KEY_PREFIX. $goods1;
for ($i=0; $i<100; $i++) {
$cart['user'] = 'tom'.$i;
go(function () use($cart, &$opt){
co::sleep(mt_rand(1,500)*0.001); //增加redis网络连接耗时
$state = $opt('lPop', $cart['key']);
if($state){
print_r($cart['user'] .' 购买成功!' .PHP_EOL);
return true;
}else{
//print_r('--已被抢光 ' .$cart['user'] .PHP_EOL);
print_r('*');
return false;
}
});
}
});
//时段限制:1000人抢 -- goods2=500个
go(function () use (&$opt, &$limit1, &$has, &$createQueue, $goods2){
$cart = [];
$cart['key'] = PROMOTION_KEY_PREFIX. $goods2;
//获取时间条件
$limit2 = json_decode($opt('lIndex', TIME_LIMIT_PREFIX. $goods2, 0), true);
for ($i=0; $i<10000; $i++) {
$cart['user'] = 'jack'.$i;
go(function () use($cart, &$opt, &$limit2){
$min = strtotime($limit2['start']);
$max = strtotime($limit2['end']);
co::sleep(mt_rand(2,6*60)); //请求分散 2s-6min/(1-5min)
$cur = time();
if($cur<$min){
print_r('--尚未开始 ' .$cart['user'] .PHP_EOL);
return true;
}elseif($min<=$cur && $cur<=$max){
co::sleep(mt_rand(100,500)*0.001); //增加redis网络连接耗时
$state = $opt('lPop', $cart['key']);
if($state){
echo $cart['user'] .' 购买成功!' .PHP_EOL;
return true;
}else{
echo '.';
//echo '++已被抢光 ' .$cart['user'] .json_encode([$limit2,date('Y-m-d H:i:s',$cur)], JSON_UNESCAPED_UNICODE) .PHP_EOL;
return false;
}
}else{
echo '++已被抢光 ' .$cart['user'] .json_encode([$limit2,date('Y-m-d H:i:s',$cur)], JSON_UNESCAPED_UNICODE) .PHP_EOL;
return false;
}
});
}
});
c.运行结果
[]:~/tmp/dk/html/php-swoole# php sale.php
创建goods1 20 个promotionList_9505900000
创建goods2 500 个promotionList_2302500000
goods1 还有个20剩余
goods2 还有个500剩余
goods1 还有个20剩余
goods2 还有个500剩余
goods1 还有个10剩余
OK DONE!
promotionList_9505900000
goods2 还有个500剩余
goods2 还有个496剩余
goods2 还有个441剩余
goods2 还有个383剩余
goods2 还有个314剩余
goods2 还有个263剩余
goods2 还有个208剩余
goods2 还有个144剩余
goods2 还有个88剩余
goods2 还有个39剩余
OK DONE!
promotionList_2302500000
[]:~/tmp/dk/html/php-swoole# php buy.php
#达到预期效果,篇幅过长略
小结
这里swoole+redis异步对秒杀执行速度很快,大量协程时注意内存占用。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。