梅梅的火星
地球太危险,我还是攒钱回火星吧!
地球太危险,我还是攒钱回火星吧!
二 24th
众所周知,几乎所有大型项目的最终效率瓶颈,都来自于如何更高效的进行数据查询,不论是使用何种数据源。
而在 MySQL 中,大部分效率低下的查询,都是因为没有正确的使用索引。
可以说,合理有效的使用索引将会对 MySQL 的效率优化起到决定性的作用。
原则索引是为了优化查询效率而存在的,正确的设置索引,将会使查询效率有质的飞跃。
索引并不是设置的越多越好,设置过多的索引,将会大大的影响非 SELECT 查询的执行效率。
将作为查询和排序条件次数最多的字段作为索引,是我们最常使用的设置原则。
技巧索引也会被用来做其他的事情,比如我们经常使用唯一索引,来做一些特殊的规则限定,比如地图中的坐标唯一。
不过我始终认为,这个应该是程序应该做的事情,而数据结构的限制,只应该是最后的保障。
索引的使用,也存在一些“潜规则”,如果不弄清楚,很可能提高效率的愿望没有达到,查询反而变慢了。
以下的几种情况,对应字段的索引是无法生效的:
查询条件里有不等号SELECT * FROM `t_user` WHERE `id` != 2 中,id 的索引将不会被使用。
查询条件里使用了函数SELECT * FROM `t_user` WHERE DAY(`regTime`) = 6 中,regTime 的索引将不会被使用。
在JOIN操作中,主键和外键的数据类型不同SELECT `C` . * , `U`.`name` AS `userName` FROM `t_user_city` AS `C` LEFT JOIN `t_user` AS `U` ON `U`.`id` = `C`.`userId` 中,如果 t_user 表的 id 字段和 t_user_city 表的 userId 字段数据类型不同,那么这两个字段的索引将不会被使用。
查询条件里使用比较操作符 LIKE 和 REGEXP,只有在搜索模板的第一个字符不是通配符的情况下才能使用索引。①SELECT * FROM `t_user` WHERE `name` LIKE ‘123%’ 和 ②SELECT * FROM `t_user` WHERE `name` LIKE ‘%123’ 中,① 查询能使用 name 的索引,而 ② 查询却不行。
使用索引查询出的记录数量超过全表记录的30%,MySQL 会遍历全表。SELECT * FROM `t_user` WHERE `isNovice` = 1 中,t_user 表所有的记录都会被遍历,因为 isNovice 只有 0 和 1 这两个值,记录数各占 50%,设置 isNovice 为索引没有任何意义。
索引的优化是双向的,不仅仅是数据库结构如何设计、索引如何定义,在程序中如何构造查询语句也是至关重要的。
除了注意以上的几条规则以外,还有一些构造的技巧,也可以帮助你提升查询的效率。
尝试使用 IN 代替 OR①SELECT * FROM `t_user` WHERE `id` = 2 OR `id` = 4 OR `id` = 6 OR `id` = 8 OR `id` = 10 和 ②SELECT * FROM `t_user` WHERE `id` IN (2, 4, 6, 8, 10) 中,② 的效率明显比 ① 要高的多。
避免在查询条件中使用函数①SELECT * FROM `t_user` WHERE DATE(`regTime`) = ‘2010-02-01’ 和 ②SELECT * FROM `t_user` WHERE `regTime` > ‘2010-02-01’ AND `regTime` < ‘2010-02-02’ 中,② 的效率明显比 ① 要高的多。
使用简单的程序处理来代替数据库处理SELECT `userId`, SQRT(POW(`unitX`, 2) + POW(`unitY`, 2)) AS `distance` FROM `t_map_unit` WHERE `userId` = 2 中,计算距离的操作完全可以由程序来做,或者在需要的时候再用程序计算。为了图一时方便,让数据库来运算,是得不偿失的。
尽量避免使用联合查询①SELECT `C` . * , `U`.`name` AS `userName` FROM `t_user_city` AS `C` LEFT JOIN `t_user` AS `U` ON `U`.`id` = `C`.`userId` 和 ②SELECT * FROM `t_user_city` 、③SELECT `name` FROM `t_user` WHERE `id` IN (……) ,我更建议使用后者,实际上通过增加简单的程序处理就可以实现,但是效率上区别确是很大的,特别是当 2 个表的记录都很多的时候。两个查询有时候比一个更快,这完全取决于查询语句的复杂度。
我们要深刻的明白一个道理,只让数据库提供和保存数据就好,尽量不要让它做其他的事情。
它很脆弱,它很容易累,它对你很重要。哪怕多写数十行程序只能减少一次查询,也值得一试。
控制优化的过程是持续的,能及时的发现问题、解决问题,是控制项目风险的精要所在。
我们可以通过很多方法来发现数据库查询的种种问题。
比如频繁的 SHOW FULL PROCESSLIST,然后记录那些频繁出现或者出现 copy tmp table 的语句,并解决它们。
但是这毫无疑问是个笨方法,我们可以使用更便捷的方式记录这些有问题的语句。
修改 MySQL 配置,记录查询速度较慢的查询在 my.cnf 中增加如下配置选项:
long_query_time = 1 log-slow-queries = /data/mysql/slow.log
这样我们就能通过查看 /data/mysql/slow.log 这个文件,找到所有查询时间超过 1 秒的查询语句。
修改 MySQL 配置,记录没有使用索引的查询在 my.cnf 中增加如下配置选项:
log-slow-queries = /data/mysql/slow.log log-queries-not-using-indexes
这样 /data/mysql/slow.log 将会记录所有没有使用索引的查询语句。
两个选项可以一起使用,这样我们基本上就可以抓住大多数的“问题查询”了。
再针对这些查询进行分析和研究,修改查询或修改索引设置,最终让它们不再在这个日志文件中出现。
如果最终这个日志文件中除了必要的遍历查询以外再无其它内容,你的项目至少在数据库上已经没有效率问题了。
另外,对于频繁进行数据修改操作的表,索引可能会损坏。
索引损坏后,将不会起任何作用,周期性的检查和优化是非常必要的。
希望本文能对你有所帮助,欢迎来信交流优化经验。
一 12th
关于 XHProf
XHProf 是 FaceBook 开发的一个函数级别的 PHP 分层分析器。
数据收集部分是一个基于 C 的 PHP 扩展,分析报告是一系列基于 PHP 的 HTML 导航页面。
XHProf 能统计每个函数的调用次数、内存使用、CPU占用等多项重要的数据。
并且 XHProf 还能比较两个统计样本,或从多个数据样本中汇总结果。
XHProf 是分析 PHP 程序执行效率的利器,能让我们得到更底层的的分析数据。
安装 XHProf
XHProf 目前的最新版本是 0.92,你可以从它的主页(http://pecl.php.net/package/xhprof)下载它的最新版本。
例如,我的 Web 目录是 /data/zivn.me,那么我可以通过下面的命令来安装 XHProf 。
wget http://pecl.php.net/get/xhprof-0.9.2.tgz tar zxf xhprof-0.9.2.tgz cd xhprof-0.9.2 cp -r xhprof_html xhprof_lib /data/zivn.me/ cd extension phpize ./configure make make install
当然,我们还需要修改 php.ini 来确保 XHProf 的正常运行。
我们需要先建立一个目录用来存储分析数据文件,例如 /data/zivn.me/xhprof/。
[xhprof] extension=xhprof.so xhprof.output_dir=/data/zivn.me/xhprof/
使用 XHProf
使用 XHProf 非常简单,我们只需要修改少许代码即可实现。
例如,我们需要分析代码执行时关于 CPU 和内存的数据。
xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY); // Test Code Start ………… // Test Code End $xhprofData = xhprof_disable(); include_once "/data/zivn.me/xhprof_lib/utils/xhprof_lib.php"; include_once "/data/zivn.me/xhprof_lib/utils/xhprof_runs.php"; $xhprofRuns = new XHProfRuns_Default(); $xhprofRuns->save_run($xhprofData, "xhprof");
这样,当我们运行程序之后,会在 /data/zivn.me/xhprof/ 之中生成分析数据文件。
文件名类似于 4b4c239a86593.xhprof ,我们可以通过改变 save_run 的参数,来改变文件后缀。
由于分析可能会影响响应速度,通常我们会加上一个随机数,随机取样,而不是分析所有用户请求的执行过程。
$randKey = mt_rand(1, 10000);
if ($randKey == 1)
{
xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY);
}
// Test Code Start
…………
// Test Code End
if ($randKey == 1)
{
$xhprofData = xhprof_disable();
include_once "/data/zivn.me/xhprof_lib/utils/xhprof_lib.php";
include_once "/data/zivn.me/xhprof_lib/utils/xhprof_runs.php";
$xhprofRuns = new XHProfRuns_Default();
$xhprofRuns->save_run($xhprofData, "xhprof");
}
查看 XHProf 分析数据
例如,我的域名是 zivn.me,现在我们就可以通过 web 来查看详细的分析数据了。
我们想要查看 4b4c239a86593.xhprof 这个报告的详细信息,查看链接如下:
http://zivn.me/xhprof_html/index.php?run=4b4c239a86593&source=xhprof
我们想比较 4b4c239a86593.xhprof 和 4b4c2645794f0.xhprof 两个报告,查看链接如下:
http://zivn.me/xhprof_html/index.php?run1=4b4c239a86593&run2=4b4c2645794f0&source=xhprof
我们可以看到精确到函数级的分析数据,包括调用次数、CPU、内存等,还可以不断的向下跟踪。
相信如此详细的数据,将会给程序优化工作,带来巨大的帮助和便利。
十二 17th
在程序设计中我们经常会碰到要解决机率或权重相关的问题,例如经常碰到的中奖问题。
有一个百货公司办了一个抽奖活动,一等奖的中奖率是3%,二等奖是5%,三等奖是10%,纪念奖是20%。
处理这个需求,一般的程序员会采用如下的逻辑:
$randKey = mt_rand(1, 100);
$prizeLevel = 0;
if ($randKey <= 3)
{
$prizeLevel = 1;
}
elseif ($randKey <= 3 + 5)
{
$prizeLevel = 2;
}
elseif ($randKey <= 3 + 5 + 10)
{
$prizeLevel = 3;
}
elseif ($randKey <= 3 + 5 + 10 + 20)
{
$prizeLevel = 4;
}
这段程序似乎有些复杂,我们可以简化一下它:
$prizeInfo = array(
0 => 62,
1 => 3,
2 => 5,
3 => 10,
4 => 20
);
$randKey = mt_rand(1, 100);
$radix = 0;
foreach ($prizeInfo as $prizeLevel => $prizeOdds)
{
$radix += $prizeOdds;
if ($randKey <= $radix)
{
break;
}
}
权重和机率类似,只是概率总值不固定,我们只想直观的用一个数字的大小来表示想给它多大的机会。
我们修改一下需求,一等奖的权重是5,二等奖是10,三等奖是20,纪念奖是50,没有奖励是100。
实际上,稍微修改一下逻辑,我们就可以处理这个问题了:
$prizeInfo = array(
0 => 100,
1 => 5,
2 => 10,
3 => 20,
4 => 50
);
$randKey = mt_rand(1, array_sum($prizeInfo));
$radix = 0;
foreach ($prizeInfo as $prizeLevel => $prizeOdds)
{
$radix += $prizeOdds;
if ($randKey <= $radix)
{
break;
}
}
但是实际上,我们在处理更复杂的需求时会发现,即使我们使用了 PHP 中号称更好用线性分布更平均的 mt_rand,统计结果看起来,也不是那么随机,或许这是源于 PHP 本身的问题。
我在设计杀死怪物时按照权重掉落物品的逻辑时,就遇到了这样的问题。
稍作处理后,结果稍微满意了些,基本上,我只是扩大了随机范围,扰乱了权重的顺序。
$prizeInfo = array(
0 => 100,
1 => 5,
2 => 10,
3 => 20,
4 => 50
);
$prizeLevels = array_keys($prizeInfo);
shuffle($prizeLevels);
$randKey = rand(1, array_sum($prizeInfo));
$radix = 0;
foreach ($prizeLevels as $prizeLevel)
{
$radix += $prizeInfo[$prizeLevel];
if ($randKey <= $radix)
{
break;
}
}
这段逻辑基本上可以应付大多数的权重和机率问题!
当然,如果你有更复杂更精确的需求,你可能需要自己设计一个特别的算法。
十二 15th
首先,我们构建一个简单的 I18n 工具类 I18n.php:
class I18n
{
/**
* 默认语言
*
* @var string
*/
const DEFAULT_LOCALE = 'zh_CN';
/**
* 默认编码
*
* @var string
*/
const DEFAULT_CHARSET = 'UTF-8';
/**
* 单实例对象序列
*
* @var array
*/
private static $instances = array();
/**
* 当前语言
*
* @var string
*/
private static $locale;
/**
* 当前域
*
* @var string
*/
private $domain;
/**
* 格式化索引
*
* @param string $key
* @return string
*/
public static function quote($key)
{
return '/\{'.$key.'\}/';
}
/**
* 构造函数
*
* @param string $domain
*/
private function __construct($domain)
{
$this->domain = $domain;
bind_textdomain_codeset($this->domain, self::DEFAULT_CHARSET);
bindtextdomain($this->domain, LOCALE_DIR);
}
/**
* 获取格式化后的文本
*
* @param string $key
* @param array $params
* @return string
*/
public function _($key, $params = null)
{
$text = dgettext($this->domain, $key);
if (empty($params))
{
return $text;
} else {
return preg_replace(
array_map('I18n::quote', array_keys($params)),
array_values($params),
$text
);
}
}
/**
* 取得一个单实例I18n对象
*
* @param string $domain
* @return I18n
*/
public static function getInstance($domain)
{
if (empty(self::$locale))
{
self::setLocale(self::DEFAULT_LOCALE);
}
if (!array_key_exists($domain, self::$instances))
{
self::$instances[$domain] = new I18n($domain);
}
return self::$instances[$domain];
}
/**
* 设置I18n语言
*
* @param string $locale
*/
public static function setLocale($locale)
{
self::$locale = $locale;
setlocale(LC_ALL, $locale);
}
}
然后我们再生成 2 个用于测试的 po 文件。
首先是中文的语言文件 locale\zh_CN\LC_MESSAGES\message.po:
msgid ""
msgstr ""
"Project-Id-Version: message\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-06-12 17:26+0800\n"
"PO-Revision-Date: 2009-12-15 20:39+0800\n"
"Last-Translator: Zivn \n"
"Language-Team: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Poedit-Language: Chinese\n"
"X-Poedit-Country: CHINA\n"
"X-Poedit-SourceCharset: utf-8\n"
msgid "testMsg"
msgstr "这是一条测试信息!你好,{name}!"
msgid "userName"
msgstr "火星梅梅"
然后是英文的语言文件 locale\en_US\LC_MESSAGES\message.po:
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2008-06-12 17:26+0800\n"
"PO-Revision-Date: 2009-12-15 20:40+0800\n"
"Last-Translator: Zivn \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Poedit-Language: English\n"
"X-Poedit-Country: AMERICAN SAMOA\n"
"X-Poedit-SourceCharset: utf-8\n"
msgid "testMsg"
msgstr "This is a test message!Hello,{name}!"
msgid "userName"
msgstr "zivn"
记得要生成二进制的 mo 文件哦。
好了,最后是我们用来测试的主文件:
define('ROOT_PATH', realpath('.'));
define('LOCALE_DIR', ROOT_PATH . DIRECTORY_SEPARATOR . 'locale');
$i18n = I18n::getInstance('message');
// Output: 这是一条测试信息!你好,火星梅梅!
echo $i18n->_('testMsg', array('name' => $i18n->_('userName')))."\n";
// Change Locale
I18n::setLocale('en_US');
// Output: This is a test message!Hello,zivn!
echo $i18n->_('testMsg', array('name' => $i18n->_('userName')))."\n";
输出了预期的中文和英文文本,是不是非常的简单呢?