发布之前对文章中的一些代码做了重命名和删除。你懂的。姑且之前的接口叫做old-share-api ,新接口叫做new-share-api。
写本文,只是想通过一个比较简单的接口,来记录下优化的想法。这个简单的接口,大概做的事情就是页面的分享,生成一个图片,分享到朋友圈或者发给好友。
重构
重构测试
线上环境:
old-share-api(lumen框架,php7) 4核8g
new-share-api (yaf,php7) 2核4g
测试代码片段(循环发送请求):
$uid = 790729;
while($start < $end) {// 发送一百个请求
$start++;
$uid++;
$params = [
'uid'=>$uid,
'link'=>'https://m.yanshinian.com/index.html?jsonData=/act_html/cache/data/7f690e132491_43656.json',
'pid'=>14,
'request_token'=>'000092659614c41548570563',
];
$result = $client->post($url, ['form_params' => $params])->data();
echo \GuzzleHttp\json_encode($result);
}
代码说明:
页面4365 的分享模块,没有加ad_id,所以只是简单的,拿shareKey,生成二维码。
为什么$uid 累计发送请求呢?因为用同一个uid 第二次请求的时候会走缓存。old从数据库拿之前分享的。new-share从redis拿。
返回结果值:
{
"status":0,
"msg":"分享成功",
"data":{
"shareImage":"https://img-cdn.yanshinian.com/o_1cs8fvits1o2nlim1rhs1uqg5v.jpg?watermark/1/image/aHR0cHM6Ly9pbWFnZS1fd3VkwuandvMTY2/dissolve/100/gravity/NorthWest/dx/545/dy/950/ws/0%7cimageView2/2/q/80/format/jpg/quality/75!%7cimageslim",
"pageUrl":"https://m.yanshinian.com/index.html?jsonData=/act_html/cache/data/7f690e132491_43656.json",
"secne":"ye32d72L",
"ad_material_info":[
]
}
}
线上测试结果(尽可能量化)
下面是不走缓存的情况下(走缓存的没测,一个取redis,一个取mysql,就不测了)
old-share
统计了23次请求,平均 1.3秒多。
cat /home/www/logs/nginx/old-share.yanshinian.com.access.log | grep "690e132491_4365" | awk -F'\001' '{sum+=$7;lineCount+=1} END {print "lineCount= " lineCount; print "sum= " sum; print "average = " sum/NR}'
lineCount= 23
sum= 30.427
average = 1.32291
new-share 50次请求 0.4秒(当然抽出23次还是快)
cat /home/www/logs/nginx/new-share.yanshinian.com.access.log | grep "690e132491_4365" | awk -F'\001' '{sum+=$7;lineCount+=1} END {print "lineCount= " lineCount; print "sum= " sum; print "average = " sum/NR}'
lineCount= 50
sum= 20.908
average = 0.41816
重构前的思考
xmind 梳理图 仅供参考
思考如下:
1.接口影响范围
比如,这个接口调用的点有多少个,列出来,每个调用点可能传的参数不一样。
如果参数不一样,那么做的事情不一样,能否拆成几个接口。
2.梳理逻辑
把没用的逻辑拿掉。
把函数瘦身。
梳理出耗时的点。
3.重构的预期(要做测试)
afp分享慢,重构就是为了快。重构之后要做测试。
回顾下代码
old 片段
// 查询当前活动页是否已被分享
$pageShareRecord = DB::connection($this->db)->table('share_record')
->select('id', 'module', 'image', 'page_url', 'updated_at', 'add_data', 'scene')
->where(['page_id' => $page_id, 'ad_uid' => $uid])
->orderBy('id', 'desc')->first();
/**
* 对比module_sort,区分当前活动页是都变动,发生变动返回新的连接
* 一期上线之后,后续添加trackId用来统计,trackId格式变动,用来处理旧的分享trackId错误的情况:没有trackId、错误trackId
*/
if (
strnatcmp($pageInfo['module_sort'], $pageShareRecord['module']) != 0 ||
strpos($pageShareRecord['page_url'], '&trackId') === false ||
strpos($pageShareRecord['page_url'], ':_:') !== false
) {
// 获取 shareKey
$shareKey = $this->getShareKey($uid);
// 此路径不再走 静态页中 lua 环节
$uri = str_replace("cache/data/", "", strstr(strstr($link, 'act_html'), '.json', true) . '.json');
// 因json内容过大,curl get 可能获取到的数据不完整,使用 guzzle 代替
$client = new \GuzzleHttp\Client();
$host = env('ADMIN_HOST');
$response = $client->request('GET', $host . '/' . $uri . '?request_token=' . $request_token);
if ($response->getStatusCode() != 200) {
throw new \Exception("guzzle request file content error");
}
$originJsonData = $response->getBody()->getContents();
// 获取活动页中商品数据,使用 trackId,组装商品链接
$pageProductInfo = json_decode($originJsonData)), true);
if (!empty($pageProductInfo['module']) && is_array($pageProductInfo['module'])) {
foreach ($pageProductInfo['module'] as $k => &$v) {
if ($v['type_id'] == 16 && $is_promotion) { // 这里取出 分享模块的数据,有分享图
$image = $v['value']['qr_bg_img'];
if (isset($v['value']['ad_id']) && $v['value']['ad_id']) {
$productList = $this->getProductListByApi([$v['value']['ad_id']], $uid, $trackId);
$v['value']['ad_material_info']['ad_sales_url'] = $productList['data']['data'][$v['value']['ad_id']];
$addData = $v['value']['ad_material_info'];
}
}
}
} else {
throw new \Exception("module parameter does not exist [curl afp result] : " . json_encode($pageInfo, JSON_UNESCAPED_UNICODE));
}
$link = str_replace('/data', '/page/share', $link) . '&trackId=' . $trackId . '&shareKey=' . $shareKey; //替换完路径,nginx lua走其他处理
$pid = $request->input('pid', 0);
$hostConfig = config('host');
if (isset($hostConfig[$pid])) {
$tempArr = explode('.com', $link);
$link = $hostConfig[$pid] . $tempArr[1];
}
if ($is_promotion) {
$scene = '';
if ($res = self::wxaCodeImge($link, $image, $uid, $pid)) {
list($imageUrl, $scene) = $res;
} else {
$imageUrl = self::_promotionImge($link, $image, $uid);
}
$image = $imageUrl;
new-share 片段
// 2 根据用户id 拿去数据库的缓存
$userShareInfoKey = sprintf(RedisKey::USER_SHARE_INFO, $uid, $pageId);
$userShareInfo = json_decode(RedisHelper::get($userShareInfoKey), true);
// 3 根据页面id 拿取 页面分享模块的 缓存
$pageShareInfo = json_decode(RedisHelper::get(sprintf(RedisKey::PAGE_INFO, $pageId)), true);
// 4 比较签名 这个签名 是分享模块缓存数据的签名,用户分享后的缓存数据也需要 带上,为的是以后对比判断
if (isset($userShareInfo["sign"]) && $userShareInfo["sign"] == $pageShareInfo["sign"]) { // 签名相等,说明分享模块数据没有变更过,直接返回分享的数据
$this->success($userShareInfo, "分享成功");
}
// 如果签名不同 走分享流程
// 获取shareKey
$shareKey = UserModel::getShareKey($uid);
if ($shareKey == "") {
throw new Exception("shareKey异常");
}
$link = str_replace('/data', '/page/share', $link) . '&trackId=' . $trackId . '&shareKey=' . $shareKey; //替换完路径,nginx lua走其他处理
// 1 拿到背景图
// 2 生成 小程序码 或 二维码
$scene = '';
if ($pid == 14) {
$xcxCodeResult = Image::getXcxcode($link, $shareKey);
$image = $xcxCodeResult['imgUrl'].'&imageView2/2/w/166';
$scene = $xcxCodeResult['sence'];
} else {
$image = Image::getQrCode($link);
}
$shareImage = Image::getSharePageImage($pageShareInfo['share_module']['qr_bg_img'], $image);// 水印图
$result = [
'shareImage'=>$shareImage,
'pageUrl'=>$link,
'secne'=> $scene,
'ad_material_info'=>[],
];
// 把分享模块的sign 值附上
$result['sign'] = $pageShareInfo['sign']; // 设置签名
RedisHelper::setex($userShareInfoKey, ($pageShareInfo['end_time']), json_encode($result));
$this->success($result, '分享成功');
具体优化了哪些呢?
1.减少了一次网络请求
$client->request('GET', $host . '/' . $uri . '?request_token=' . $request_token);
拿页面的json数据,最初这么做两个目的
- 主要是拿通过request_token 获取动态的数据,然后生成新的页面(后来复用一个页面,生成不同页面的需求就废了)
- 其次拿页面中分享模块的数据
优化后:
分享模块数据存入redis。从redis拿。
2.减少了两次mysql查询操作
DB::connection($this->db)->table('page')->select('module_sort', 'start_time')->where('id', $page_id)->first();
DB::connection($this->db)->table('share_record')
->select('id', 'module', 'image', 'page_url', 'updated_at', 'add_data', 'scene')
->where(['page_id' => $page_id, 'ad_uid' => $uid])
->orderBy('id', 'desc')->first();
if (
strnatcmp($pageInfo['module_sort'], $pageShareRecord['module']) != 0 ||
strpos($pageShareRecord['page_url'], '&trackId') === false ||
strpos($pageShareRecord['page_url'], ':_:') !== false
)
这样做的目的,是为了比较页面是否变更过。如果页面变更了(strnatcmp($pageInfo['module_sort'], $pageShareRecord['module']) != 0
)。用户下一次分享,不从缓存中(mysql)拿数据,重新生成一次分享数据。
优化之后:
缓存用了redis。对比通过sign字段。页面分享数据,在发布的时候缓存到redis,对数据加密保存一个sign字段。用户在手机分享后,把这个sign也放到自己缓存中。下次做sign对比。
虽然mysql少了。redis对应的就增加了。
- 代码拆分
这个就不说了。
文摘
1.《系统运维之为什么每个团队存在大量的烂代码》
优化性能的2个观点:
- 优化主要部分,把一次网络I/O改为内存计算带来的收益远大于我们捯饬编译器优化之类的东西。
- 性能优化之后要有量化数据,明确说出优化后哪个指标提升了多少。
具体优化措施,无外乎以下几类:
- 让计算靠近存储
- 优化算法的时间复杂度
- 减少无用的操作
- 并行计算
参考资料:
- 书《高可用架构(第一卷)》中秦迪的三篇文章《系统运维之为什么每个团队存在大量的烂代码》、《系统运维之评价烂代码优劣的方法》、《系统运维之如何应对烂代码》 文章还不错。那本书上有他四篇,另一篇《微博对大规模、高负载系统问题的排查方法》
- 《优秀程序设计的18大原则》https://m.imooc.com/article/1836