<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">博客园_个人博客已迁移至codinglabs.org，博客园不再更新</title><subtitle type="text">个人博客已迁移至codinglabs.org，欢迎访问</subtitle><id>http://feed.cnblogs.com/blog/u/35418/rss</id><updated>2012-01-06T15:27:47Z</updated><author><name>T2噬菌体</name><uri>http://www.cnblogs.com/leoo2sk/</uri></author><generator>CNBlogs BlogServer</generator><link rel="alternate" type="text/html" href="http://www.cnblogs.com/leoo2sk/"/><link rel="self" type="application/atom+xml" href="http://feed.cnblogs.com/blog/u/35418/rss"/><entry><id>http://www.cnblogs.com/leoo2sk/archive/2011/11/09/write-daemon-with-php.html</id><title type="text">如何使用PHP编写daemon process</title><summary type="text">今天下午在segmentfault.com看到一个提问，提问标题是“PHP怎么做服务化”，其中问道php是不是只能以web方式调用。其实很多人对PHP的使用场景都有误解，认为php只能用于编写web脚本，实际上，从PHP4开始，php的使用场景早已不限于处理web请求。 从php的架构体系来说，php分为三个层次：sapi、php core和zend engine。php core本身和web没有任何耦合，php通过sapi与其它应用程序通信，例如mod_php就是为apache编写的sapi实现，同样，fpm是一个基于fastcgi协议的sapi实现，这些sapi都是与web server配合用于处理web请求的。但是也有许多sapi与web无关，例如cli sapi可以使得在命令行环境下直接执行php，embed sapi可以将php嵌入其它语言（如Lua）那样。这里我并不打算详细讨论php的架构体系和sapi的话题，只是说明从架构体系角度目前的php早已被设计为支持各种环境，而非为web独有。 除了架构体系的支持外，php丰富的扩展模块也为php在不同环境发挥作用提供了后盾，例如</summary><published>2011-11-09T09:20:00Z</published><updated>2011-11-09T09:20:00Z</updated><author><name>T2噬菌体</name><uri>http://www.cnblogs.com/leoo2sk/</uri></author><link rel="alternate" href="http://www.cnblogs.com/leoo2sk/archive/2011/11/09/write-daemon-with-php.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/leoo2sk/archive/2011/11/09/write-daemon-with-php.html"/><content type="html">&lt;p&gt;今天下午在&lt;a href="http://segmentfault.com"&gt;segmentfault.com&lt;/a&gt;看到一个&lt;a href="http://segmentfault.com/question/585/php%E6%80%8E%E4%B9%88%E5%81%9A%E6%9C%8D%E5%8A%A1%E5%8C%96" target="_blank"&gt;提问&lt;/a&gt;，提问标题是&amp;ldquo;PHP怎么做服务化&amp;rdquo;，其中问道php是不是只能以web方式调用。其实很多人对PHP的使用场景都有误解，认为php只能用于编写web脚本，实际上，从PHP4开始，php的使用场景早已不限于处理web请求。 从php的架构体系来说，php分为三个层次：sapi、php core和zend engine。php core本身和web没有任何耦合，php通过sapi与其它应用程序通信，例如mod_php就是为apache编写的sapi实现，同样，fpm是一个基于fastcgi协议的sapi实现，这些sapi都是与web server配合用于处理web请求的。但是也有许多sapi与web无关，例如cli sapi可以使得在命令行环境下直接执行php，embed sapi可以将php嵌入其它语言（如Lua）那样。这里我并不打算详细讨论php的架构体系和sapi的话题，只是说明从架构体系角度目前的php早已被设计为支持各种环境，而非为web独有。 除了架构体系的支持外，php丰富的扩展模块也为php在不同环境发挥作用提供了后盾，例如本文要提到的&lt;a href="http://cn.php.net/manual/en/book.pcntl.php" target="_blank"&gt;pcntl模块&lt;/a&gt;和&lt;a href="http://cn.php.net/manual/en/book.posix.php" target="_blank"&gt;posix模块&lt;/a&gt;配合可以实现基本的进程管理、信号处理等操作系统级别的功能，而&lt;a href="http://cn.php.net/manual/en/book.sockets.php" target="_blank"&gt;sockets模块&lt;/a&gt;可以使php具有socket通信的能力。因此php完全可以用于编写类似于shell或perl常做的工具性脚本，甚至是具有server性质的daemon process。 为了展示php如何编写daemon server，我用php编写了一个简单的http server，这个server以daemon process的形式运行。当然，为了把重点放在如何使用php编写daemon，我没有为这个http server实现具体业务逻辑，但它可以监听指定端口，接受http请求并返回给客户端一条固定的文本，整个过程通过socket实现，全部由php编写而成。&lt;/p&gt;&#xD;
&lt;!--more--&gt;&#xD;
&lt;p&gt;&lt;strong&gt;代码实例&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;下面是这个程序的完整代码：&lt;/p&gt;&#xD;
&lt;pre &gt;&amp;lt;?php&#xD;
&#xD;
//Accpet the http client request and generate response content.&#xD;
//As a demo, this function just send "PHP HTTP Server" to client.&#xD;
function handle_http_request($address, $port)&#xD;
{&#xD;
	$max_backlog = 16;&#xD;
	$res_content = "HTTP/1.1 200 OK&#xD;
Content-Length: 15&#xD;
Content-Type: text/plain; charset=UTF-8&#xD;
&#xD;
PHP HTTP Server";&#xD;
	$res_len = strlen($res_content);&#xD;
&#xD;
	//Create, bind and listen to socket&#xD;
	if(($socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === FALSE)&#xD;
	{&#xD;
		echo "Create socket failed!\n";&#xD;
		exit;&#xD;
	}	&#xD;
&#xD;
	if((socket_bind($socket, $address, $port)) === FALSE)&#xD;
	{&#xD;
		echo "Bind socket failed!\n";&#xD;
		exit;&#xD;
	}&#xD;
	&#xD;
	if((socket_listen($socket, $max_backlog)) === FALSE)&#xD;
	{&#xD;
		echo "Listen to socket failed!\n";&#xD;
		exit;&#xD;
	}&#xD;
&#xD;
	//Loop&#xD;
	while(TRUE)&#xD;
	{&#xD;
		if(($accept_socket = socket_accept($socket)) === FALSE)&#xD;
		{&#xD;
			continue;&#xD;
		}&#xD;
		else&#xD;
		{&#xD;
			socket_write($accept_socket, $res_content, $res_len);	&#xD;
			socket_close($accept_socket);&#xD;
		}&#xD;
	}&#xD;
}&#xD;
&#xD;
//Run as daemon process.&#xD;
function run()&#xD;
{&#xD;
	if(($pid1 = pcntl_fork()) === 0)&#xD;
	//First child process&#xD;
	{&#xD;
		posix_setsid(); //Set first child process as the session leader.&#xD;
		&#xD;
		if(($pid2 = pcntl_fork()) === 0)&#xD;
		//Second child process, which run as daemon.&#xD;
		{&#xD;
			//Replaced with your own domain or address.&#xD;
			handle_http_request('www.codinglabs.org', 9999); &#xD;
		}&#xD;
		else&#xD;
		{&#xD;
			//First child process exit;&#xD;
			exit;&#xD;
		}&#xD;
	}&#xD;
	else&#xD;
	{&#xD;
		//Wait for first child process exit;&#xD;
		pcntl_wait($status);&#xD;
	}&#xD;
}&#xD;
&#xD;
//Entry point.&#xD;
run();&#xD;
&#xD;
?&amp;gt;&lt;/pre&gt;&#xD;
&lt;p&gt;这里我假设各位对Unix环境编程都比较了解，所以不做太多细节的解释，只梳理一下。简单来看，这个程序主要由两个部分组成，handle_http_request函数负责处理http请求，其编写方法与用C编写的tcp server类似：创建socket、绑定、监听，然后通过一个循环处理每个connect过来的客户端，一旦accept到一个连接...&lt;/p&gt;&#xD;
&lt;p style="font-size: 16px; border: 1px solid #FF6600; padding: 6px; color: #ff0000; background-color: ffeedd;"&gt;个人博客最新地址为&lt;a href="http://www.codinglabs.org"&gt;www.codinglabs.org&lt;/a&gt;，阅读全文请点击&lt;a href="http://www.codinglabs.org/html/write-daemon-with-php.html"&gt;http://www.codinglabs.org/html/write-daemon-with-php.html&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;div style="display: none;"&gt;而只是一个简单的同步阻塞tcp server。 run函数负责将整个程序变为daemon process，方法和Unix环境下C的方法很类似，通过两次fork，第一次fork后调用setsid将子进程1变为session leader，这样就可以让子进程2与其祖先detach，即使祖先进程结束了它也会继续运行（托孤给init进程）。相关细节我不再赘述，对Unix进程相关不熟悉的朋友可以参考《&lt;a href="http://www.kohala.com/start/apue.html" target="_blank"&gt;Advanced Programming in the UNIX Environment&lt;/a&gt;》一书。 注意，在这里&lt;a href="http://cn.php.net/manual/en/function.pcntl-fork.php" target="_blank"&gt;pcntl_fork&lt;/a&gt;对应Unix中的fork，&lt;a href="http://cn.php.net/manual/en/function.pcntl-wait.php" target="_blank"&gt;pcntl_wait&lt;/a&gt;对应wait，而&lt;a href="http://cn.php.net/manual/en/function.posix-setsid.php" target="_blank"&gt;posix_setsid&lt;/a&gt;对应setsid，更多函数可以参考PHP Manual中的pcntl和fork模块相关内容。&#xD;
&lt;p&gt;&lt;strong&gt;检验&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;下面在命令行下启动这个脚本：&lt;/p&gt;&#xD;
&lt;pre &gt;php httpserver.php&lt;/pre&gt;&#xD;
&lt;p&gt;用ps命令可以看到我们已经启动了一个daemon进程： &lt;a href="http://www.codinglabs.org/wp-content/uploads/2011/10/image7.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="image" src="http://www.codinglabs.org/wp-content/uploads/2011/10/image_thumb3.png" alt="image" width="780" height="58" border="0" /&gt;&lt;/a&gt; 这里我绑定的是我博客的域名www.codinglabs.org，端口是9999，可以按需要进行修改。 下面我先用curl命令看下这个http server是否正常运行： &lt;a href="http://www.codinglabs.org/wp-content/uploads/2011/10/image9.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="image" src="http://www.codinglabs.org/wp-content/uploads/2011/10/image_thumb4.png" alt="image" width="765" height="112" border="0" /&gt;&lt;/a&gt; 看来是没问题，再到浏览器中看一下： &lt;a href="http://www.codinglabs.org/wp-content/uploads/2011/10/image14.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="image" src="http://www.codinglabs.org/wp-content/uploads/2011/10/image_thumb5.png" alt="image" width="575" height="113" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;结语&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;当然，这个程序算不上真正的http server，即使作为一个daemon process，也是不完善的，很多必要的事情如修改执行目录（php中可以通过chroot实现）、信号绑定、日志功能等等都没有去做，不过作为一个demo，它已经足够说明php不只是可以编写动态网页处理脚本。如果有的朋友有兴趣，可以使用php将我上面说的功能为这个的http server加上。 还有一点要说明的是，pcntl和sockets模块默认是不安装的，如果在安装php时没有通过参数指定安装，则需要单独安装这两个扩展模块。&lt;/p&gt;&#xD;
&lt;/div&gt;&lt;img src="http://www.cnblogs.com/leoo2sk/aggbug/2243308.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/leoo2sk/archive/2011/11/09/write-daemon-with-php.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/leoo2sk/archive/2011/11/07/zend-thread-safety.html</id><title type="text">深入研究PHP及Zend Engine的线程安全模型</title><summary type="text">在阅读PHP源码和学习PHP扩展开发的过程中，我接触到大量含有“TSRM”字眼的宏。通过查阅资料，知道这些宏与Zend的线程安全机制有关，而绝大多数资料中都建议按照既定规则使用这些宏就可以，而没有说明这些宏的具体作用。不知道怎么回事总是令人不舒服的，因此我通过阅读源码和查阅有限的资料简要了解一下相关机制，本文是我对研究内容的总结。 本文首先解释了线程安全的概念及PHP中线程安全的背景，然后详细研究了PHP的线程安全机制ZTS（Zend Thread Safety）及具体的实现TSRM，研究内容包括相关数据结构、实现细节及运行机制，最后研究了Zend对于单线程和多线程环境的选择性编译问题。</summary><published>2011-11-07T05:56:00Z</published><updated>2011-11-07T05:56:00Z</updated><author><name>T2噬菌体</name><uri>http://www.cnblogs.com/leoo2sk/</uri></author><link rel="alternate" href="http://www.cnblogs.com/leoo2sk/archive/2011/11/07/zend-thread-safety.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/leoo2sk/archive/2011/11/07/zend-thread-safety.html"/><content type="html">&lt;p&gt;在阅读PHP源码和学习PHP扩展开发的过程中，我接触到大量含有&amp;ldquo;TSRM&amp;rdquo;字眼的宏。通过查阅资料，知道这些宏与Zend的线程安全机制有关，而绝大多数资料中都建议按照既定规则使用这些宏就可以，而没有说明这些宏的具体作用。不知道怎么回事总是令人不舒服的，因此我通过阅读源码和查阅有限的资料简要了解一下相关机制，本文是我对研究内容的总结。 本文首先解释了线程安全的概念及PHP中线程安全的背景，然后详细研究了PHP的线程安全机制ZTS（Zend Thread Safety）及具体的实现TSRM，研究内容包括相关数据结构、实现细节及运行机制，最后研究了Zend对于单线程和多线程环境的选择性编译问题。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;线程安全&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;线程安全问题，一言以蔽之就是多线程环境下如何安全存取公共资源。我们知道，每个线程只拥有一个私有栈，共享所属进程的堆。在C中，当一个变量被声明在任何函数之外时，就成为一个全局变量，这时这个变量会被分配到进程的共享存储空间，不同线程都引用同一个地址空间，因此一个线程如果修改了这个变量，就会影响到全部线程。这看似为线程共享数据提供了便利，但是PHP往往是每个线程处理一个请求，因此希望每个线程拥有一个全局变量的副本，而不希望请求间相互干扰。 早期的PHP往往用于单线程环境，每个进程只启动一个线程，因此不存在线程安全问题。后来出现了多线程环境下使用PHP的场景，因此Zend引入了Zend线程安全机制（Zend Thread Safety，简称ZTS）用于保证线程的安全。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;ZTS的基本原理及实现&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;基本思想&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;说起来ZTS的基本思想是很直观的，不是就是需要每个全局变量在每个线程都拥有一个副本吗？那我就提供这样的机制： 在多线程环境下，申请全局变量不再是简单声明一个变量，而是整个进程在堆上分配一块内存空间用作&amp;ldquo;线程全局变量池&amp;rdquo;，在进程启动时初始化这个内存池，每当有线程需要申请全局变量时，通过相应方法调用TSRM（Thread Safe Resource Manager，ZTS的具体实现）并传递必要的参数（如变量大小等等），TSRM负责在内存池中分配相应内存区块并将这块内存的引用标识返回，这样下次这个线程需要读写此变量时，就可以通过将唯一的引用标识传递给TSRM，TSRM将负责真正的读写操作。这样就实现了线程安全的全局变量。下图给出了ZTS原理的示意图： &lt;a href="http://www.codinglabs.org/wp-content/uploads/2011/11/image.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://www.codinglabs.org/wp-content/uploads/2011/11/image_thumb.png" alt="image" width="384" height="243" border="0" /&gt;&lt;/a&gt;Thread1和Thread2同属一个进程，其中各自需要一个全局变量Global Var，TSRM为两者在线程全局内存池中（黄色部分）各自分配了一个区域，并且通过唯一的ID进行标识，这样两个线程就可以通过TSRM存取自己的变量而互不干扰。 下面通过具体的代码片段看一下Zend具体是如何实现这个机制的。这里我用的是PHP5.3.8的源码。 TSRM的实现代码在PHP源码的&amp;ldquo;TSRM&amp;rdquo;目录下。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;数据结构&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;TSRM中比较重要的数据结构有两个：tsrm_tls_entry和tsrm_resource_type。下面先看tsrm_tls_entry。 tsrm_tls_entry定义在TSRM/TSRM.c中：&lt;/p&gt;&#xD;
&lt;pre &gt;typedef struct _tsrm_tls_entry tsrm_tls_entry;&#xD;
&#xD;
struct _tsrm_tls_entry {&#xD;
	void **storage;&#xD;
	int count;&#xD;
	THREAD_T thread_id;&#xD;
	tsrm_tls_entry *next;&#xD;
}&lt;/pre&gt;&#xD;
&lt;p&gt;每个tsrm_tls_entry结构负责表示一个线程的所有全局变量资源，其中thread_id存储线程ID，count记录全局变量数，next指向下一个节点。storage可以看做指针数组，其中每个元素是一个指向本节点代表线程的一个全局变量。最终各个线程的tsrm_tls_entry被组成一个链表结构，并将链表头指针赋值给一个全局静态变量tsrm_tls_table。注意，因为tsrm_tls_table是一个货真价实的全局变量，所以所有线程会共享这个变量，这就实现了线程间的内存管理一致性。tsrm_tls_entry和tsrm_tls_table结构的示意图如下： &lt;a href="http://www.codinglabs.org/wp-content/uploads/2011/11/image1.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://www.codinglabs.org/wp-content/uploads/2011/11/image_thumb1.png" alt="image" width="418" height="286" border="0" /&gt;&lt;/a&gt;tsrm_resource_type的内部结构相对简单一些：&lt;/p&gt;&#xD;
&lt;pre &gt;typedef struct {&#xD;
	size_t size;&#xD;
	ts_allocate_ctor ctor;&#xD;
	ts_allocate_dtor dtor;&#xD;
	int done;&#xD;
} tsrm_resource_type;&lt;/pre&gt;&#xD;
&lt;p&gt;上文说过tsrm_tls_entry是以线程为单位的（每个线程一个节点），而tsrm_resource_type以资源（或者说全局变量）为单位，每次一个新的资源被分配时，就会创建一个tsrm_resource_type。所有tsrm_resource_type以数组（线性表）的方式组成tsrm_resource_table，其下标就是这个资源的ID。每个tsrm_resource_type存储了此资源的大小和构造、析构方法指针。某种程度上，tsrm_resource_table可以看做是一个哈希表，key是资源ID，value是tsrm_resource_type结构。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;实现细节&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;这一小节分析TSRM一些算法的实现细节。因为整个TSRM涉及代码比较多，这里拣其中具有代表性的两个函数分析。 第一个值得注意的是tsrm_startup函数，这个函数在进程起始阶段被sapi调用，用于初始化TSRM的环境。由于tsrm_startup略长，这里摘录出我认为应该注意的地方：&lt;/p&gt;&#xD;
&lt;pre &gt;/* Startup TSRM (call once for the entire process) */&#xD;
TSRM_API int tsrm_startup(int expected_threads, int expected_resources, int debug_level, char *debug_filename)&#xD;
{&#xD;
	/* code... */&#xD;
&#xD;
	tsrm_tls_table_size = expected_threads;&#xD;
&#xD;
	tsrm_tls_table = (tsrm_tls_entry **) calloc(tsrm_tls_table_size, sizeof(tsrm_tls_entry *));&#xD;
	if (!tsrm_tls_table) {&#xD;
		TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate TLS table"));&#xD;
		return 0;&#xD;
	}&#xD;
	id_count=0;&#xD;
&#xD;
	resource_types_table_size = expected_resources;&#xD;
	resource_types_table = (tsrm_resource_type *) calloc(resource_types_table_size, sizeof(tsrm_resource_type));&#xD;
	if (!resource_types_table) {&#xD;
		TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate resource types table"));&#xD;
		free(tsrm_tls_table);&#xD;
		tsrm_tls_table = NULL;&#xD;
		return 0;&#xD;
	}&#xD;
&#xD;
	/* code... */&#xD;
&#xD;
	return 1;&#xD;
}&lt;/pre&gt;&#xD;
&lt;p&gt;其实tsrm_startup的主要任务就是初始化上文提到的两个数据结构。第一个比较有意思的是它的前两个参数：expected_threads和expected_resources。这两个参数由sapi传入，表示预计的线程数和资源数，可以看到tsrm_startup会按照这两个参数预先分配空间（通过calloc）。因此TSRM会首先分配可容纳expected_threads个线程和expected_resources个资源的。要看各个sapi默认会传入什么，可以看各个sapi的源码（在sapi目录下），我简单看了一下： &lt;a href="http://www.codinglabs.org/wp-content/uploads/2011/11/image2.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://www.codinglabs.org/wp-content/uploads/2011/11/image_thumb2.png" alt="image" width="636" height="435" border="0" /&gt;&lt;/a&gt;可以看到比较常用的sapi如mod_php5、php-fpm和cgi都是预分配一个线程和一个资源，这样是因为不愿浪费内存空间，而且多数情况下PHP还是运行于单线程环境。 这里还可以看到一个id_count变量，这个变量是一个全局静态变量，其作用就是通过自增产生资源ID，这个变量在这里被初始化为0。所以TSRM产生资源ID的方式非常简单：就是一个整形变量的自增。 第二个需要仔细分析的就是ts_allocate_id，编写过PHP扩展的朋友对这个函数肯定不陌生，这个函数...&lt;/p&gt;&#xD;
&lt;p style="font-size: 16px; border: 1px solid #FF6600; padding: 6px; color: #ff0000; background-color: ffeedd;"&gt;个人博客最新地址为&lt;a href="http://www.codinglabs.org"&gt;www.codinglabs.org&lt;/a&gt;，阅读全文请点击&lt;a href="http://www.codinglabs.org/html/zend-thread-safety.html"&gt;http://www.codinglabs.org/html/zend-thread-safety.html&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;div style="display: none;"&gt;&#xD;
&lt;pre &gt;/* allocates a new thread-safe-resource id */&#xD;
TSRM_API ts_rsrc_id ts_allocate_id(ts_rsrc_id *rsrc_id, size_t size, ts_allocate_ctor ctor, ts_allocate_dtor dtor)&#xD;
{&#xD;
	int i;&#xD;
&#xD;
	TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtaining a new resource id, %d bytes", size));&#xD;
&#xD;
	tsrm_mutex_lock(tsmm_mutex);&#xD;
&#xD;
	/* obtain a resource id */&#xD;
	*rsrc_id = TSRM_SHUFFLE_RSRC_ID(id_count++);&#xD;
	TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Obtained resource id %d", *rsrc_id));&#xD;
&#xD;
	/* store the new resource type in the resource sizes table */&#xD;
	if (resource_types_table_size &amp;lt; id_count) {&#xD;
		resource_types_table = (tsrm_resource_type *) realloc(resource_types_table, sizeof(tsrm_resource_type)*id_count);&#xD;
		if (!resource_types_table) {&#xD;
			tsrm_mutex_unlock(tsmm_mutex);&#xD;
			TSRM_ERROR((TSRM_ERROR_LEVEL_ERROR, "Unable to allocate storage for resource"));&#xD;
			*rsrc_id = 0;&#xD;
			return 0;&#xD;
		}&#xD;
		resource_types_table_size = id_count;&#xD;
	}&#xD;
	resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].size = size;&#xD;
	resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].ctor = ctor;&#xD;
	resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].dtor = dtor;&#xD;
	resource_types_table[TSRM_UNSHUFFLE_RSRC_ID(*rsrc_id)].done = 0;&#xD;
&#xD;
	/* enlarge the arrays for the already active threads */&#xD;
	for (i=0; i&amp;lt;tsrm_tls_table_size; i++) {&#xD;
		tsrm_tls_entry *p = tsrm_tls_table[i];&#xD;
&#xD;
		while (p) {&#xD;
			if (p-&amp;gt;count &amp;lt; id_count) {&#xD;
				int j;&#xD;
&#xD;
				p-&amp;gt;storage = (void *) realloc(p-&amp;gt;storage, sizeof(void *)*id_count);&#xD;
				for (j=p-&amp;gt;count; j&amp;lt;id_count; j++) {&#xD;
					p-&amp;gt;storage[j] = (void *) malloc(resource_types_table[j].size);&#xD;
					if (resource_types_table[j].ctor) {&#xD;
						resource_types_table[j].ctor(p-&amp;gt;storage[j], &amp;amp;p-&amp;gt;storage);&#xD;
					}&#xD;
				}&#xD;
				p-&amp;gt;count = id_count;&#xD;
			}&#xD;
			p = p-&amp;gt;next;&#xD;
		}&#xD;
	}&#xD;
	tsrm_mutex_unlock(tsmm_mutex);&#xD;
&#xD;
	TSRM_ERROR((TSRM_ERROR_LEVEL_CORE, "Successfully allocated new resource id %d", *rsrc_id));&#xD;
	return *rsrc_id;&#xD;
}&lt;/pre&gt;&#xD;
&lt;p&gt;rsrc_id最终存放的就是新资源的ID。其实这个函数的一些实现方式让我比较费解，首先是返回ID的方式。因为rsrc_id是按引入传入的，所以最终也就应该包含资源ID，那么最后完全不必在return *rsrc_id，可以返回一个预订整数表示成功或失败（例如1成功，0失败），这里有点费两遍事的意思，而且多了一次寻址。另外&amp;ldquo;*rsrc_id = TSRM_SHUFFLE_RSRC_ID(id_count++); &amp;rdquo;让我感觉很奇怪，因为TSRM_SHUFFLE_RSRC_ID被定义为&amp;ldquo;((rsrc_id)+1)&amp;rdquo;，那么这里展开就是：&lt;/p&gt;&#xD;
&lt;pre &gt;*rsrc_id = ((id_count++)+1)&lt;/pre&gt;&#xD;
&lt;p&gt;为什么不写成这样呢：&lt;/p&gt;&#xD;
&lt;pre &gt;*rsrc_id = ++id_count&lt;/pre&gt;&#xD;
&lt;p&gt;真是怪哉。 好的，且不管实现是否合理，我们先继续研究这个函数吧。 首先要将id_count自增，生成一个新的资源ID，然后为这个新资源创建一个tsrm_resource_type并放入resource_type_table，接着遍历所有线程（注意是所有）为每一个线程的tsrm_tls_entry分配这个线程全局变量需要的内存空间（p-&amp;gt;storage[j] = (void *) malloc(resource_types_table[j].size); ）。 这里需要注意，对于每一次ts_allocate_id调用，Zend会遍历所有线程并为每一个线程分配相应资源，因为ts_allocate_id实际是在MINIT阶段被调用，而不是在请求处理阶段被调用的。换言之，TSRM会在进程建立时统一分配好线程全局资源，关于这个下文会专门描述。 抽象来看，可以将整个线程全局资源池看做一个矩阵，一个维度为线程，一个维度为id_count，因此任意时刻所有线程全局变量的数量为&amp;ldquo;线程数*id_count&amp;rdquo;。tsrm_tls_entry和tsrm_resource_type可以看做这个矩阵在两个维度上的索引。 通过分析可以看出，每次调用ts_allocate_id的代价是很大的，由于ts_allocate_id并没有预先分配算法，每次在id_count维度申请一个新的变量，就涉及两次realloc和N次malloc（N为线程数），申请M个全局变量的代价为： 2 * M * t(realloc) + N * M * t(malloc) 因此要尽量减少ts_allocate_id的调用次数。正因这个原因，在PHP扩展开发中提倡将一个模块所需的全局变量声明为一个结构体然后一次性申请，而不要分开申请。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;ZTS与生命周期&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;这里需要简单提一下PHP的生命周期。 PHP的具体生命周期模式取决于sapi的实现，但一般都会有MINIT、RINIT、SCRIPT、RSHUTDOWN和MSHUTDOWN五个典型阶段，不同的只是各个阶段的执行次数不同。例如在CLI或CGI模式下，这五个阶段顺序执行一次，而在Apache或FastCGI模式下往往一个MINIT和MSHUTDOWN中间对应多个RINIT、SCRIPT、RSHUTDOWN。关于PHP生命周期的话题我回头写文单独研究，这里只是简单说一下。 MINIT和MSHUTDOWN是PHP Module的初始化和清理阶段，往往在进程起始后和结束前执行，在这两个阶段各个模块的MINIT和MSHUTDOWN方法会被调用。而RINIT、SCRIPT、RSHUTDOWN是每一次请求都会触发的一个小周期。在多线程模式中，PHP的生命周期如下： &lt;a href="http://www.codinglabs.org/wp-content/uploads/2011/11/image3.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://www.codinglabs.org/wp-content/uploads/2011/11/image_thumb3.png" alt="image" width="380" height="456" border="0" /&gt;&lt;/a&gt;在这种模式下，进程启动后仅执行一次MINIT。之所以要强调这一点，是因为TSRM的全局变量资源分配就是在MINIT阶段完成的，后续阶段只获取而不会再请求新的全局变量，这就不难理解为什么在ts_allocate_id中每次id_count加一需要遍历所有线程为每个线程分配相同的资源。到这里，终于可以看清TSRM分配线程全局变量的全貌： 进程启动后，在MINIT阶段启动TSRM（通过sapi调用tsrm_startup），然后在遍历模块时调用每一个模块的MINIT方法，模块在MINIT中告知TSRM要申请多少全局变量及大小（通过ts_allocate_id），TSRM在内存池中分配并做好登记工作（tsrm_tls_table和resource_types_table），然后将凭证（资源ID）返回给模块，告诉模块以后拿着这个凭证来取你的全局变量。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;ZTS在单线程和多线程环境的选择性编译&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;上文说过，很多情况下PHP还是被用于单线程环境，这时如果还是遵循上述行为，显然过于折腾。因为在单线程环境下不存在线程安全问题，全局变量只要简单声明使用就好，没必要搞那么一大堆动作。PHP的设计者考虑到的这一点，允许在编译时指定是否开启多线程支持，只有当在configure是指定--enable-maintainer-zts选项或启用多线程sapi时，PHP才会编译线程安全的代码。具体来说，当启用线程安全编译时，一个叫ZTS的常量被定义，PHP代码在每个与线程安全相关的地方通过#ifdef检查是否编译线程安全代码。 在探究相关细节前我先说一些自己的看法，对于ZTS多线程和单线程环境选择性编译设计上，我个人觉得是非常失败的。因为良好的设计应该隔离变化，换言之ZTS有义务将选择性编译相关的东西隔离起来，而不让其污染到模块的编写，这个机制对模块开发应该是透明的。但是ZTS的设计者仿佛生怕大家不知道有这个东西，让其完全污染了整个PHP，模块开发者不得不面对一堆奇奇怪怪的TSRM宏，着实让人非常不爽。所以下面我就带着悲愤的心情研究一下这块内容。 为了看看模块是如何实现选择性编译代码的，我们建立一个空的PHP扩展模块。到PHP源码的ext目录下执行如下命令：&lt;/p&gt;&#xD;
&lt;pre &gt;./ext_skel --extname=zts_research&lt;/pre&gt;&#xD;
&lt;p&gt;ext_skel是一个脚手架程序，用于创建PHP扩展模块。此时会看到ext目录下多了个zts_research目录。ext_skel为为什么生成了一个模块的架子，并附带了很多提示性注释。在这个目录下找到php_zts_research.h并打开，比较有趣的是一下一段代码：&lt;/p&gt;&#xD;
&lt;pre &gt;/* &#xD;
    Declare any global variables you may need between the BEGIN&#xD;
    and END macros here:     &#xD;
&#xD;
ZEND_BEGIN_MODULE_GLOBALS(zts_research)&#xD;
    long  global_value;&#xD;
    char *global_string;&#xD;
ZEND_END_MODULE_GLOBALS(zts_research)&#xD;
*/&lt;/pre&gt;&#xD;
&lt;p&gt;很明显这里提示了定义全局变量的方法：用ZEND_BEGIN_MODULE_GLOBALS和ZEND_END_MODULE_GLOBALS两个宏包住所有全局变量。下面看一下这两个宏，这两个宏定义在Zend/zend_API.h文件里：&lt;/p&gt;&#xD;
&lt;pre &gt;#define ZEND_BEGIN_MODULE_GLOBALS(module_name)		\&#xD;
	typedef struct _zend_##module_name##_globals {&#xD;
#define ZEND_END_MODULE_GLOBALS(module_name)		\&#xD;
	} zend_##module_name##_globals;&lt;/pre&gt;&#xD;
&lt;p&gt;原来这两个宏只是将一个模块的所有全局变量封装为一个结构体定义，名称为zend_module_name_globals。关于为什么要封装成结构体，上文有提到。 php_zts_research.h另外比较有意思的一处就是：&lt;/p&gt;&#xD;
&lt;pre &gt;#ifdef ZTS&#xD;
#define ZTS_RESEARCH_G(v) TSRMG(zts_research_globals_id, zend_zts_research_globals *, v)&#xD;
#else&#xD;
#define ZTS_RESEARCH_G(v) (zts_research_globals.v)&#xD;
#endif&lt;/pre&gt;&#xD;
&lt;p&gt;zts_research_globals是zts_research模块全局变量结构的变量名称，类型为zend_module_name_globals，在哪定义的稍后会研究。这里ZTS_RESEARCH_G就是这个模块获取全局变量的宏，如果ZTS没有定义（非线程安全时），就直接从这个结构中获取相应字段，如果线程安全开启时，则使用TSRMG这个宏。&lt;/p&gt;&#xD;
&lt;pre &gt;#define TSRMG(id, type, element)	(((type) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(id)])-&amp;gt;element)&lt;/pre&gt;&#xD;
&lt;p&gt;这个宏就不具体细究了，因为实在太难懂了，基本思想就是使用上文提到的TSRM机制从线程全局变量池中获取对应的数据，其中tsrm_ls可以看作是线程全局变量池的指针，获取变量的凭证就是资源ID。 看到这里可能还有点晕，例如zts_research_globals这个变量哪来的？zts_research_globals_id又是哪来的？为了弄清这个问题，需要打开ext/zts_research/zts_research.c这个文件，其中有这样的代码：&lt;/p&gt;&#xD;
&lt;pre &gt;/* If you declare any globals in php_zts_research.h uncomment this:&#xD;
ZEND_DECLARE_MODULE_GLOBALS(zts_research)&#xD;
*/&lt;/pre&gt;&#xD;
&lt;p&gt;提示很清楚，如果在php_zts_research.h中定义了任何全局变量则将这段代码的注释消除，看来这个ZEND_DECLARE_MODULE_GLOBALS宏就是关键了。然后在Zend/zend_API中有这样的代码：&lt;/p&gt;&#xD;
&lt;pre &gt;#ifdef ZTS&#xD;
&#xD;
#define ZEND_DECLARE_MODULE_GLOBALS(module_name)							\&#xD;
	ts_rsrc_id module_name##_globals_id;&#xD;
&#xD;
/* code... */&#xD;
&#xD;
#else&#xD;
&#xD;
#define ZEND_DECLARE_MODULE_GLOBALS(module_name)							\&#xD;
	zend_##module_name##_globals module_name##_globals;&#xD;
&#xD;
/* code... */&#xD;
&#xD;
#endif&lt;/pre&gt;&#xD;
&lt;p&gt;当线程安全开启时，这里实际定义了一个整形的资源ID（ts_rsrc_id 被typedef定义为int），而当线程安全不开启时，则直接定义一个结构体。在这个模块中分别对应zts_research_globals_id和zts_research_globals。 到这里思路基本理顺了：如果ZTS没有被启用，则直接声明一个全局变量结构体，并直接通过存取其字段实现全局变量存取；如果ZTS开启，则定义一个整形变量作为资源ID，然后通过ts_allocate_id函数向TSRM申请一块内存放置结构体（需要程序员手工在MINIT函数中实现，脚手架生成的程序中没有），并通过TSRM存取数据。 最后一个疑问：tsrm_ls在哪里？如果通过上述方法从TSRM中取数据，那么一定要知道线程全局变量内存池的指针tsrm_ls。这就是我说过的最污染PHP的地方，如果你阅读过PHP源码或编写过模块，对以下四个宏肯定眼熟：TSRMLS_D，TSRMLS_DC，TSRMLS_DTSRMLS_C，TSRMLS_CC。实际在PHP内部每次定义方法或调用方法时都要在参数列表最后加上其中的一个宏，其实就是为了将tsrm_ls传给函数以便存取全局变量，这四个宏的定义如下：&lt;/p&gt;&#xD;
&lt;pre &gt;#ifdef ZTS&#xD;
&#xD;
#define TSRMLS_D	void ***tsrm_ls&#xD;
#define TSRMLS_DC	, TSRMLS_D&#xD;
#define TSRMLS_C	tsrm_ls&#xD;
#define TSRMLS_CC	, TSRMLS_C&#xD;
&#xD;
#else&#xD;
&#xD;
#define TSRMLS_D	void&#xD;
#define TSRMLS_DC&#xD;
#define TSRMLS_C&#xD;
#define TSRMLS_CC&#xD;
&#xD;
#endif&lt;/pre&gt;&#xD;
&lt;p&gt;在没有开启ZTS时，四个宏被定义为空，但这时在定义PHP方法或调用方法时依旧将宏加在参数列表后，这是为了保持代码的一致性，当然，因为在非ZTS环境下根本不会用到tsrm_ls，所以没有任何问题。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;本文研究了PHP和Zend的线程安全模型，应该说我个人觉得Zend内核中ZTS的实现巧妙但不够优雅，但目前在开发PHP模块时总免不了常与之打交道。这块内容相对偏门，几乎没有资料对ZTS和TSRM进行详细解释，但是透彻了解ZTS机制对于在PHP模块开发中正确合理使用全局变量是很重要的。希望本文对读者有所帮助。&lt;/p&gt;&#xD;
&lt;/div&gt;&lt;img src="http://www.cnblogs.com/leoo2sk/aggbug/2239168.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/leoo2sk/archive/2011/11/07/zend-thread-safety.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/leoo2sk/archive/2011/10/02/nginx-memc-and-srcache.html</id><title type="text">使用memc-nginx和srcache-nginx模块构建高效透明的缓存机制</title><summary type="text">为了提高性能，几乎所有互联网应用都有缓存机制，其中Memcache是使用非常广泛的一个分布式缓存系统。众所周知，LAMP是非常经典的Web架构方式，但是随着Nginx的成熟，越来越多的系统开始转型为LNMP（Linux+Nginx+MySQL+PHP with fpm），这是因为Nginx采用基于事件机制的I/O多路复用思想设计，在高并发情况下其性能远远优于默认采用prefork模式的Apache，另外，相对于Apache，Nginx更轻量，同时拥有大量优秀的扩展模块，使得在Nginx上可以实现一些美妙的功能。传统上，PHP中使用memcache的方法是使用php-memcache或php-memached扩展操作memcache，然而在Nginx上有构建更高效缓存机制的方法，本文将首先介绍这种机制，然后介绍具体的操作步骤方法，最后将对这种机制和传统的PHP操作memcache的性能进行一个benchmark。</summary><published>2011-10-02T15:10:00Z</published><updated>2011-10-02T15:10:00Z</updated><author><name>T2噬菌体</name><uri>http://www.cnblogs.com/leoo2sk/</uri></author><link rel="alternate" href="http://www.cnblogs.com/leoo2sk/archive/2011/10/02/nginx-memc-and-srcache.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/leoo2sk/archive/2011/10/02/nginx-memc-and-srcache.html"/><content type="html">&lt;p&gt;为了提高性能，几乎所有互联网应用都有缓存机制，其中&lt;a href="http://memcached.org" target="_blank"&gt;Memcache&lt;/a&gt;是使用非常广泛的一个分布式缓存系统。众所周知，LAMP是非常经典的Web架构方式，但是随着&lt;a href="http://wiki.nginx.org/" target="_blank"&gt;Nginx&lt;/a&gt;的成熟，越来越多的系统开始转型为LNMP（Linux+Nginx+MySQL+PHP with fpm），这是因为Nginx采用基于事件机制的I/O多路复用思想设计，在高并发情况下其性能远远优于默认采用prefork模式的Apache，另外，相对于Apache，Nginx更轻量，同时拥有大量优秀的扩展模块，使得在Nginx上可以实现一些美妙的功能。&lt;/p&gt;&#xD;
&lt;p&gt;传统上，PHP中使用memcache的方法是使用&lt;a href="http://pecl.php.net/package/memcache" target="_blank"&gt;php-memcache&lt;/a&gt;或&lt;a href="http://pecl.php.net/package/memcached" target="_blank"&gt;php-memached&lt;/a&gt;扩展操作memcache，然而在Nginx上有构建更高效缓存机制的方法，本文将首先介绍这种机制，然后介绍具体的操作步骤方法，最后将对这种机制和传统的PHP操作memcache的性能进行一个benchmark。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;Nginx的Memc和SR Cache模块&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;缓存策略的改进&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;我们知道，Nginx的核心设计思想是事件驱动的非阻塞I/O。Nginx被设计为可以配置I/O多路复用策略，在Unix系统中传统的多路复用是采用select或poll，但是这两个方法的问题是随着监听socket的增加，性能会下降，因为在linux内核中是采用轮询的方式判断是否可以触发事件，换句话说算法的复杂度为O(N)，而在较新的linux内核中引入了复杂度为O(1)的epoll，因此Nginx在Linux下默认采用epoll，而在FreeBSD下默认采用kqueue作为I/O策略。&lt;/p&gt;&#xD;
&lt;p&gt;即便是这样，传统的缓存策略仍可能造成效率低下，因为传统上是通过PHP操作memcache的，要执行PHP代码，Nginx就必然要和FastCGI通信，同时也要进入PHP的生命周期，因此SAPI、PHP Core和Zend Engine的一系列逻辑会被执行。更糟糕的是，fpm和PHP可能会阻塞，因此破坏了Nginx的非阻塞性。下图展示了&lt;span style="color: #ff0000;"&gt;在memcache命中时&lt;/span&gt;整个处理过程。&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110021513265641.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110021513272968.png" alt="image" width="644" height="265" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;可以看到，即使memcache命中，还是要进入PHP的生命周期。我们知道，目前很多互联网应用都使用RESTful规范进行设计，在RESTful应用下，普遍使用uri和查询参数作为缓存的key，因此一种更高效的缓存策略是Nginx直接访问memcache，并用$uri和$args等Nginx内置变量设定缓存key规则，这样，当缓存命中时，Nginx可以跳过通过fastcgi和PHP通信的过程，直接从memcache中获取数据并返回。memc-nginx和srcache-nginx正是利用这种策略提高了缓存的效率。下图是这种高效缓存策略的示意图（当memcache命中时）。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110021513283949.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110021513303817.png" alt="image" width="590" height="265" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p style="font-size: 16px; border: 1px solid #FF6600; padding: 6px; color: #ff0000; background-color: ffeedd;"&gt;个人博客已迁移至&lt;a href="http://www.codinglabs.org"&gt;www.codinglabs.org&lt;/a&gt;，本文全文最新地址为&lt;a href="http://www.codinglabs.org/html/nginx-memc-and-srcache.html"&gt;http://www.codinglabs.org/html/nginx-memc-and-srcache.html&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;div style="display: none;"&gt;&#xD;
&lt;p&gt;&lt;strong&gt;模块介绍&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://wiki.nginx.org/HttpMemcModule" target="_blank"&gt;memc-nginx&lt;/a&gt;和&lt;a href="http://wiki.nginx.org/HttpSRCacheModule" target="_blank"&gt;srcache-nginx&lt;/a&gt;模块均为前淘宝工程师agentzh（章亦春）开发。其中memc模块扩展了Nginx标准的memcache模块，增加了set、add、delete等memcache命令，而srcache则是为location增加了透明的基于subrequest的缓存层。两者配合使用，可以实现上一节提到的高效缓存机制。关于两个模块的详细信息可以参考它们Nginx官网的wiki（&lt;a href="http://wiki.nginx.org/HttpMemcModule" target="_blank"&gt;memc wiki&lt;/a&gt;，&lt;a href="http://wiki.nginx.org/HttpSRCacheModule" target="_blank"&gt;srcache wiki&lt;/a&gt;）页。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;安装及配置&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;下面以LNMP环境介绍如何使用这两个模块构建缓存层。&lt;/p&gt;&#xD;
&lt;p&gt;因为Nginx并不支持模块动态加载，所以要安装新的模块，必须重新编译Nginx。首先下载两个模块（&lt;a href="https://github.com/agentzh/memc-nginx-module/downloads" target="_blank"&gt;memc下载地址&lt;/a&gt;，&lt;a href="https://github.com/agentzh/srcache-nginx-module/downloads" target="_blank"&gt;srcache下载地址&lt;/a&gt;），另外，为了发挥出缓存的最大性能，建议将memcache的upstream配置为keep-alive，为了支持upstream的keep-alive需要同时安装&lt;a href="http://wiki.nginx.org/HttpUpstreamKeepaliveModule" target="_blank"&gt;http-upstream-keepalive-module&lt;/a&gt;。&lt;/p&gt;&#xD;
&lt;p&gt;将模块下载并解压到合适的目录，这里我Nginx使用的版本是1.0.4，与相关模块一起解压到了/home/zhangyang/downloads，如下图所示。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110021619214800.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110021619236063.png" alt="image" width="764" height="382" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;其中红框框起来的是我们需要用到的模块。进入nginx目录，执行下列命令：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;./configure --prefix=/usr/local/nginx --add-module=../memc-nginx-module --add-module=../srcache-nginx-module --add-module=../ngx_http_upstream_keepalive&#xD;
make&#xD;
make install&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这里我将nginx安装到/usr/local/nginx下，你可以根据自己的需要更改安装路径，另外，我只列出了本文必要的configure命令，你也可以增加需要的configure选项。&lt;/p&gt;&#xD;
&lt;p&gt;然后需要对nginx进行配置，nginx默认主配置文件放在安装目录的conf下，例如我的主配置文件为/usr/local/nginx/conf/nginx.conf。&lt;/p&gt;&#xD;
&lt;p&gt;这里我只贴出相关的配置：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;#Memcache服务upstream&#xD;
upstream memcache {&#xD;
    server localhost:11211;&#xD;
    keepalive 512 single;&#xD;
}&#xD;
&#xD;
server {&#xD;
    listen       80;&#xD;
    server_name  localhost;&#xD;
&#xD;
    #memc-nginx-module&#xD;
    location /memc {&#xD;
        internal;&#xD;
&#xD;
        memc_connect_timeout 100ms;&#xD;
        memc_send_timeout 100ms;&#xD;
        memc_read_timeout 100ms;&#xD;
&#xD;
        set $memc_key $query_string;&#xD;
        set $memc_exptime 300;&#xD;
                                           &#xD;
        memc_pass memcache;&#xD;
    }&#xD;
&#xD;
    location / {&#xD;
        root   /var/www;&#xD;
        index  index.html index.htm index.php;&#xD;
    }&#xD;
&#xD;
    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000&#xD;
    #&#xD;
    location ~ \.php$ {&#xD;
        charset        utf-8;&#xD;
        default_type   text/html;&#xD;
&#xD;
        #srcache-nginx-module&#xD;
        set $key $uri$args;&#xD;
        srcache_fetch GET /memc $key;&#xD;
        srcache_store PUT /memc $key;&#xD;
&#xD;
        root           /var/www;&#xD;
        fastcgi_pass   127.0.0.1:9000;&#xD;
        fastcgi_index  index.php;&#xD;
        include        fastcgi_params;&#xD;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;&#xD;
    }&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;下面解释一下其中几个点。&lt;/p&gt;&#xD;
&lt;p&gt;上文说过，memc-nginx是一个标准的upstream模块，因此首先需要定义memcache的upstream。这里我在本机上启动了一个memcache服务，端口为默认的11211，keepalive指令是http-upsteram-keepalive-module提供的功能，这里我们最大保持512个不立即关闭的连接用于提升性能。&lt;/p&gt;&#xD;
&lt;p&gt;下面是为memc-nginx-module配置location，我们配置为/memc，所有请求都通过请求这个location来操作memcache，memc-nginx-module存取memcache是基于http method语义的，使用http的GET方法表示get、PUT方法表示set、DELETE方法表示delete。这里我们将/memc设为&lt;a href="http://wiki.nginx.org/NginxHttpCoreModule#internal" target="_blank"&gt;internal&lt;/a&gt;表示只接受内部访问，不接收外部http请求，这是为了安全考虑，当然如果需要通过http协议开放外部访问，可以去掉internal然后使用&lt;a href="http://wiki.nginx.org/NginxHttpAccessModule#deny" target="_blank"&gt;deny&lt;/a&gt;和&lt;a href="http://wiki.nginx.org/NginxHttpAccessModule#allow" target="_blank"&gt;allow&lt;/a&gt;指令控制权限。比较重要的是$memc_key这个变量，它表示以什么作为key，这里我们直接使用Nginx内置的$query_string来作为key，$memc_exptime表示缓存失效时间，以秒记。这里统一设为300（5分钟），在实际应用中可以根据具体情况为不同的内容设置不同的过期时间。&lt;/p&gt;&#xD;
&lt;p&gt;最后我们为&amp;ldquo;~ \.php$&amp;rdquo;这个location配置了缓存，这表示所有以&amp;ldquo;.php&amp;rdquo;结尾的请求都会结果被缓存，当然这里只是示例需要，实际中一般不会这么配，而是为特定需要缓存的location配置缓存。&lt;/p&gt;&#xD;
&lt;p&gt;srcache_fetch表示注册一个输入拦截处理器到location，这个配置将在location进入时被执行；而srcache_store表示注册一个输出拦截器到location，当location执行完成并输出时会被执行。注意srcache模块实际可以与任何缓存模块进行配合使用，而不必一定是memc。这里我们以$uri$args作为缓存的key。&lt;/p&gt;&#xD;
&lt;p&gt;经过上述配置后，相当于对Nginx增加了如下逻辑：当所请求的uri以&amp;ldquo;.php&amp;rdquo;结尾时，首先到memcache中查询有没有以$uri$args为key的数据，如果有则直接返回；否则，执行location的逻辑，如果返回的http状态码为200，则在输出前以$uri$args为key，将输入结果存入memcache。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;更多配置&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;上一节给出了使用memc和srcache构建缓存层的最基本方法，实际应用中可能需要更多灵活的配置，例如为不同的location配置不同的缓存参数，根据返回内容而不是返回的http状态码确定是否缓存等等。可以有很多的方法实现这些需求，例如，srcache还支持两个指令：srcache_fetch_skip和srcache_fetch_skip，这两个指令接受一个参数，当参数已定义且非0时，则进行相应操作，否则不进行。例如，如果配置了srcache_fetch_skip $skip，这条指令，那么只有当$skip的值为非0时，才将结果缓存，如果配合&lt;a href="http://wiki.nginx.org/HttpLuaModule" target="_blank"&gt;ngx_lua&lt;/a&gt;模块的&lt;a href="http://wiki.nginx.org/HttpLuaModule#set_by_lua" target="_blank"&gt;set_by_lua&lt;/a&gt;指令，则可以实现复杂的缓存控制。如：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;location /xxxx {&#xD;
    set $key ...;&#xD;
    set_by_lua $skip '&#xD;
        if ngx.var.cookie_foo == "bar" then&#xD;
            return 1&#xD;
        end&#xD;
        return 0&#xD;
    ';&#xD;
 &#xD;
    srcache_fetch_skip $skip;&#xD;
    srcache_store_skip $skip;&#xD;
 &#xD;
    srcache_fetch GET /memc $key;&#xD;
    srcache_store GET /memc $key;&#xD;
&#xD;
    # proxy_pass/fastcgi_pass/...&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这表示对/xxxx这个location的访问，只有存在cookie &amp;ldquo;foo&amp;rdquo;且值为&amp;ldquo;bar&amp;rdquo;时缓存机制才起作用。关于ngx_lua的更多内容请参考其&lt;a href="http://wiki.nginx.org/HttpLuaModule" target="_blank"&gt;主页&lt;/a&gt;。&lt;/p&gt;&#xD;
&lt;p&gt;另外，我最近在春哥（章亦春在淘宝的昵称）的微博上看到他目前正在完善srcache的功能，为其实现更多&lt;a href="http://tools.ietf.org/pdf/rfc2616.pdf" target="_blank"&gt;RFC2616&lt;/a&gt;的缓存行为标准。关于这个模块的最新动态可以关注其&lt;a href="https://github.com/agentzh/srcache-nginx-module" target="_blank"&gt;github&lt;/a&gt;主页。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;Benchmark&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;下面对使用memc和srcache构建的缓存机制进行一个简单的benchmark，并与使用PHP操作memcache的策略进行一个对比。为了简单起见，我们的测试PHP脚本不去访问I/O，而仅仅是调用phpinfo函数输出PHP相关信息。&lt;/p&gt;&#xD;
&lt;p&gt;测试一共分三组进行：第一组在Nginx和PHP中均不开启缓存，第二组仅使用PHP memcache缓存，第三组仅使用Nginx memcache缓存。三组都用&lt;a href="http://httpd.apache.org/docs/2.0/programs/ab.html" target="_blank"&gt;ab&lt;/a&gt;程序去压，并发数为20，请求次数为10000。&lt;/p&gt;&#xD;
&lt;p&gt;这里的测试环境是我的一个虚拟机，操作系统为Ubuntu10，内存512M。Nginx采用epoll，单worker进程，memcache最大并发数为1024，最大使用内存64m。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;不开启缓存&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;这一组我们不开启缓存，PHP程序非常简单：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;?php&#xD;
&#xD;
phpinfo();&#xD;
&#xD;
?&amp;gt;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;测试结果如下：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110022307189103.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110022307197019.png" alt="image" width="661" height="558" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;PHP memcache缓存策略&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;第二组我们用PHP操作缓存，测试脚本为：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;?php&#xD;
&#xD;
$memc = new Memcached;&#xD;
$memc-&amp;gt;addServer('localhost', 11211) or die('Connect to memcache server failed!');&#xD;
&#xD;
$output = $memc-&amp;gt;get('my_key');&#xD;
if(empty($output))&#xD;
{&#xD;
    ob_start();&#xD;
    phpinfo();&#xD;
    $output = ob_get_contents();&#xD;
    ob_end_clean();&#xD;
&#xD;
    $memc-&amp;gt;set('my_key', $output, 300);&#xD;
}&#xD;
&#xD;
echo $output;*/&#xD;
&#xD;
?&amp;gt;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;测试结果如下：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110022307208839.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110022307213167.png" alt="image" width="659" height="558" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;Nginx memcache缓存策略&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;最后，我们将PHP脚本回归到不使用缓存的版本，并配置好memc和srcache缓存机制。测试结果如下：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110022307233034.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110022307244539.png" alt="image" width="664" height="564" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;结果对比分析&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;为了直观，我取&amp;ldquo;每秒处理请求数&amp;rdquo;、&amp;ldquo;平均每个请求处理时间&amp;rdquo;和&amp;ldquo;吞吐率&amp;rdquo;作为评价指标，制作了一张图表。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110022307258900.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201110/201110022307269673.png" alt="image" width="607" height="400" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;我想看到图表，结论已毋需我多言。在各项指标上使用memc和srcache构建的缓存机制都大大优于使用PHP操作memcache。其中每秒处理请求数（并发度）和吞吐率都是其9倍左右，而平均个请求所用时间仅有传统策略的1/8。&lt;/p&gt;&#xD;
&lt;p&gt;这里要特别说明一下，这里之所以PHP memcache策略比不使用缓存优势不明显，是因为我们的PHP脚本不涉及I/O操作，如果其中存在如数据库存取，PHP memcache的优势还是有的，但不论如何，Nginx memcache策略在性能上的优势是其无法比拟的。&lt;/p&gt;&#xD;
&lt;p&gt;另外，除了性能优势外，使用这种策略还可以简化PHP逻辑，因为缓存这一层都放在Nginx中了，PHP就从缓存操作中解放了出来，因此是一举多得。&lt;/p&gt;&#xD;
&lt;p&gt;如果你的系统也构建在LNMP上（或LAMP）上，不妨使用本文提到的方法替代传统的缓存策略，尽情享受性能上的提升。&lt;/p&gt;&#xD;
&lt;/div&gt;&lt;img src="http://www.cnblogs.com/leoo2sk/aggbug/2198012.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/leoo2sk/archive/2011/10/02/nginx-memc-and-srcache.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/leoo2sk/archive/2011/08/11/consistent-hashing-intro.html</id><title type="text">一致性哈希算法及其在分布式系统中的应用</title><summary type="text">本文将会从实际应用场景出发，介绍一致性哈希算法（Consistent Hashing）及其在分布式系统中的应用。首先本文会描述一个在日常开发中经常会遇到的问题场景，借此介绍一致性哈希算法以及这个算法如何解决此问题；接下来会对这个算法进行相对详细的描述，并讨论一些如虚拟节点等与此算法应用相关的话题。</summary><published>2011-08-11T12:12:00Z</published><updated>2011-08-11T12:12:00Z</updated><author><name>T2噬菌体</name><uri>http://www.cnblogs.com/leoo2sk/</uri></author><link rel="alternate" href="http://www.cnblogs.com/leoo2sk/archive/2011/08/11/consistent-hashing-intro.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/leoo2sk/archive/2011/08/11/consistent-hashing-intro.html"/><content type="html">&lt;p&gt;&lt;strong&gt;摘要&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;本文将会从实际应用场景出发，介绍一致性哈希算法（Consistent Hashing）及其在分布式系统中的应用。首先本文会描述一个在日常开发中经常会遇到的问题场景，借此介绍一致性哈希算法以及这个算法如何解决此问题；接下来会对这个算法进行相对详细的描述，并讨论一些如虚拟节点等与此算法应用相关的话题。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;分布式缓存问题&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;假设我们有一个网站，最近发现随着流量增加，服务器压力越来越大，之前直接读写数据库的方式不太给力了，于是我们想引入Memcached作为缓存机制。现在我们一共有三台机器可以作为Memcached服务器，如下图所示。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/201108112009576839.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/201108112009577362.png" alt="image" width="538" height="346" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;很显然，最简单的策略是将每一次Memcached请求随机发送到一台Memcached服务器，但是这种策略可能会带来两个问题：一是同一份数据可能被存在不同的机器上而造成数据冗余，二是有可能某数据已经被缓存但是访问却没有命中，因为无法保证对相同key的所有访问都被发送到相同的服务器。因此，随机策略无论是时间效率还是空间效率都非常不好。&lt;/p&gt;&#xD;
&lt;p&gt;要解决上述问题只需做到如下一点：保证对相同key的访问会被发送到相同的服务器。很多方法可以实现这一点，最常用的方法是计算哈希。例如对于每次访问，可以按如下算法计算其哈希值：&lt;/p&gt;&#xD;
&lt;p&gt;h = Hash(key) % 3&lt;/p&gt;&#xD;
&lt;p&gt;其中Hash是一个从字符串到正整数的哈希映射函数。这样，如果我们将Memcached Server分别编号为0、1、2，那么就可以根据上式和key计算出服务器编号h，然后去访问。&lt;/p&gt;&#xD;
&lt;p&gt;这个方法虽然解决了上面提到的两个问题，但是存在一些其它的问题。如果将上述方法抽象，可以认为通过：&lt;/p&gt;&#xD;
&lt;p&gt;h = Hash(key) % N&lt;/p&gt;&#xD;
&lt;p&gt;这个算式计算每个key的请求应该被发送到哪台服务器，其中N为服务器的台数，并且服务器按照0 &amp;ndash; (N-1)编号。&lt;/p&gt;&#xD;
&lt;p&gt;这个算法的问题在于容错性和扩展性不好。所谓容错性是指当系统中某一个或几个服务器变得不可用时，整个系统是否可以正确高效运行；而扩展性是指当加入新的服务器后，整个系统是否可以正确高效运行。&lt;/p&gt;&#xD;
&lt;p&gt;现假设有一台服务器宕机了，那么为了填补空缺，要将宕机的服务器从编号列表中移除，后面的服务器按顺序前移一位并将其编号值减一，此时每个key就要按h = Hash(key) % (N-1)重新计算；同样，如果新增了一台服务器，虽然原有服务器编号不用改变，但是要按h = Hash(key) % (N+1)重新计算哈希值。因此系统中一旦有服务器变更，大量的key会被重定位到不同的服务器从而造成大量的缓存不命中。而这种情况在分布式系统中是非常糟糕的。&lt;/p&gt;&#xD;
&lt;p&gt;一个设计良好的分布式哈希方案应该具有良好的单调性，即服务节点的增减不会造成大量哈希重定位。一致性哈希算法就是这样一种哈希方案。&lt;/p&gt;&#xD;
&lt;p style="font-size: 16px; border: 1px solid #FF6600; padding: 6px; color: #ff0000; background-color: ffeedd;"&gt;个人博客已迁移至&lt;a href="http://www.codinglabs.org"&gt;www.codinglabs.org&lt;/a&gt;，本文全文最新地址为&lt;a href="http://www.codinglabs.org/html/consistent-hashing.html"&gt;http://www.codinglabs.org/html/consistent-hashing.html&lt;/a&gt;，欢迎访问！！！&lt;/p&gt;&#xD;
&lt;div style="display: none;"&gt;&#xD;
&lt;p&gt;&lt;strong&gt;一致性哈希算法&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;算法简述&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;一致性哈希算法（Consistent Hashing）最早在论文《&lt;a href="http://www.akamai.com/dl/technical_publications/ConsistenHashingandRandomTreesDistributedCachingprotocolsforrelievingHotSpotsontheworldwideweb.pdf" target="_blank"&gt;Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web&lt;/a&gt;》中被提出。简单来说，一致性哈希将整个哈希值空间组织成一个虚拟的圆环，如假设某哈希函数H的值空间为0 - 2&lt;sup&gt;32&lt;/sup&gt;-1（即哈希值是一个32位无符号整形），整个哈希空间环如下：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/201108112009573218.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/201108112009577886.png" alt="image" width="229" height="246" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;整个空间按顺时针方向组织。0和2&lt;sup&gt;32&lt;/sup&gt;-1在零点中方向重合。&lt;/p&gt;&#xD;
&lt;p&gt;下一步将各个服务器使用H进行一个哈希，具体可以选择服务器的ip或主机名作为关键字进行哈希，这样每台机器就能确定其在哈希环上的位置，这里假设将上文中三台服务器使用ip地址哈希后在环空间的位置如下：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/201108112009579281.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/20110811200957677.png" alt="image" width="298" height="248" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;接下来使用如下算法定位数据访问到相应服务器：将数据key使用相同的函数H计算出哈希值h，通根据h确定此数据在环上的位置，从此位置沿环顺时针&amp;ldquo;行走&amp;rdquo;，第一台遇到的服务器就是其应该定位到的服务器。&lt;/p&gt;&#xD;
&lt;p&gt;例如我们有A、B、C、D四个数据对象，经过哈希计算后，在环空间上的位置如下：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/201108112009585660.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/20110811200958644.png" alt="image" width="299" height="259" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;根据一致性哈希算法，数据A会被定为到Server 1上，D被定为到Server 3上，而B、C分别被定为到Server 2上。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;容错性与可扩展性分析&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;下面分析一致性哈希算法的容错性和可扩展性。现假设Server 3宕机了：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/201108112009581723.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/20110811200958295.png" alt="image" width="299" height="259" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;可以看到此时A、C、B不会受到影响，只有D节点被重定位到Server 2。一般的，在一致性哈希算法中，如果一台服务器不可用，则受影响的数据仅仅是此服务器到其环空间中前一台服务器（即顺着逆时针方向行走遇到的第一台服务器）之间数据，其它不会受到影响。&lt;/p&gt;&#xD;
&lt;p&gt;下面考虑另外一种情况，如果我们在系统中增加一台服务器Memcached Server 4：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/201108112009583642.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/201108112009588626.png" alt="image" width="299" height="259" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;此时A、D、C不受影响，只有B需要重定位到新的Server 4。一般的，在一致性哈希算法中，如果增加一台服务器，则受影响的数据仅仅是新服务器到其环空间中前一台服务器（即顺着逆时针方向行走遇到的第一台服务器）之间数据，其它不会受到影响。&lt;/p&gt;&#xD;
&lt;p&gt;综上所述，一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据，具有较好的容错性和可扩展性。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;虚拟节点&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;一致性哈希算法在服务节点太少时，容易因为节点分部不均匀而造成数据倾斜问题。例如我们的系统中有两台服务器，其环分布如下：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/201108112009583609.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/20110811200959229.png" alt="image" width="285" height="246" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;此时必然造成大量数据集中到Server 1上，而只有极少量会定位到Server 2上。为了解决这种数据倾斜问题，一致性哈希算法引入了虚拟节点机制，即对每一个服务节点计算多个哈希，每个计算结果位置都放置一个此服务节点，称为虚拟节点。具体做法可以在服务器ip或主机名的后面增加编号来实现。例如上面的情况，我们决定为每台服务器计算三个虚拟节点，于是可以分别计算&amp;ldquo;Memcached Server 1#1&amp;rdquo;、&amp;ldquo;Memcached Server 1#2&amp;rdquo;、&amp;ldquo;Memcached Server 1#3&amp;rdquo;、&amp;ldquo;Memcached Server 2#1&amp;rdquo;、&amp;ldquo;Memcached Server 2#2&amp;rdquo;、&amp;ldquo;Memcached Server 2#3&amp;rdquo;的哈希值，于是形成六个虚拟节点：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/201108112009595212.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201108/20110811200959196.png" alt="image" width="341" height="265" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;同时数据定位算法不变，只是多了一步虚拟节点到实际节点的映射，例如定位到&amp;ldquo;Memcached Server 1#1&amp;rdquo;、&amp;ldquo;Memcached Server 1#2&amp;rdquo;、&amp;ldquo;Memcached Server 1#3&amp;rdquo;三个虚拟节点的数据均定位到Server 1上。这样就解决了服务节点少时数据倾斜的问题。在实际应用中，通常将虚拟节点数设置为32甚至更大，因此即使很少的服务节点也能做到相对均匀的数据分布。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;目前一致性哈希基本成为了分布式系统组件的标准配置，例如Memcached的各种客户端都提供内置的一致性哈希支持。本文只是简要介绍了这个算法，更深入的内容可以参看论文《&lt;a href="http://www.akamai.com/dl/technical_publications/ConsistenHashingandRandomTreesDistributedCachingprotocolsforrelievingHotSpotsontheworldwideweb.pdf" target="_blank"&gt;Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web&lt;/a&gt;》，同时提供一个&lt;a href="http://www.codeproject.com/KB/recipes/lib-conhash.aspx" target="_blank"&gt;C语言版本的实现&lt;/a&gt;供参考。&lt;/p&gt;&#xD;
&lt;/div&gt;&lt;img src="http://www.cnblogs.com/leoo2sk/aggbug/2135101.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/leoo2sk/archive/2011/08/11/consistent-hashing-intro.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/leoo2sk/archive/2011/07/10/mysql-index.html</id><title type="text">MySQL索引背后的数据结构及算法原理</title><summary type="text">本文以MySQL数据库为研究对象，讨论与数据库索引相关的一些话题。特别需要说明的是，MySQL支持诸多存储引擎，而各种存储引擎对索引的支持也各不相同，因此MySQL数据库支持多种索引类型，如BTree索引，哈希索引，全文索引等等。为了避免混乱，本文将只关注于BTree索引，因为这是平常使用MySQL时主要打交道的索引，至于哈希索引和全文索引本文暂不讨论。文章主要内容分为四个部分。第一部分主要从数据结构及算法理论层面讨论MySQL数据库索引的数理基础。第二部分结合MySQL数据库中MyISAM和InnoDB数据存储引擎中索引的架构实现讨论聚集索引、非聚集索引及覆盖索引等话题。第三部分根据上面的理论基础，讨论MySQL中高性能使用索引的策略。</summary><published>2011-07-10T15:40:00Z</published><updated>2011-07-10T15:40:00Z</updated><author><name>T2噬菌体</name><uri>http://www.cnblogs.com/leoo2sk/</uri></author><link rel="alternate" href="http://www.cnblogs.com/leoo2sk/archive/2011/07/10/mysql-index.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/leoo2sk/archive/2011/07/10/mysql-index.html"/><content type="html">&lt;p&gt;&lt;strong&gt;写在前面的话&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;在编程领域有一句人尽皆知的法则&amp;ldquo;程序 = 数据结构 + 算法&amp;rdquo;，我个人是不太赞同这句话（因为我觉得程序不仅仅是数据结构加算法），但是在日常的学习和工作中我确认深深感受到数据结构和算法的重要性，很多东西，如果你愿意稍稍往深处挖一点，那么扑面而来的一定是各种数据结构和算法知识。例如几乎每个程序员都要打交道的数据库，如果仅仅是用来存个数据、建建表、建建索引、做做增删改查，那么也许觉得数据结构和这东西没什么关系。不过要是哪天心血来潮，想知道的多一点，想研究一下如何优化数据库，那么一定避免不了研究索引的原理，如果想要真正明白索引是怎么工作的，如何合理的使用索引以优化数据库，那么就免不了纠结于一堆数据结构与算法之间了。所以，如果说&amp;ldquo;程序的核心基础 = 数据结构 + 算法&amp;rdquo;我是十分赞同的，而一个想成为高手的程序员，一定会去学习程序的核心基础。&lt;/p&gt;&#xD;
&lt;p&gt;好吧，说了这么多，其实我的意思是如果想把数据库索引学个明明白白，就必须将数据结构和算法作为切入点去学习，遗憾的是我目前还没有在网上找到从原理层面去介绍数据库索引的资料（这里仅指在通俗资料领域没找到，不包括学术论文），倒不是说没有高水平的程序员，就只在我们公司范围内能把这一点讲透彻讲明白的数据库大牛也海了去了，只是由于工作的忙碌和个人兴趣原因，这些大牛们没有时间或没有兴趣去写这方面的文章。由于工作的需要，我这个半桶水的程序员这段时间也草草研究一些关于MySQL数据库索引的东西，虽然对这方面的理解相比那些大牛差的太远了，不过这里我还是将这些浅薄的知识总结成文吧。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-1"&gt;摘要&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-2"&gt;数据结构及算法基础&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-2-1"&gt;索引的本质&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-2-2"&gt;B-Tree和B+Tree&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-2-3"&gt;为什么实用B-Tree（B+Tree）&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-3"&gt;MySQL索引实现&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-3-1"&gt;MyISAM索引实现&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-3-2"&gt;InnoDB索引实现&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-4"&gt;索引使用策略及优化&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-4-1"&gt;示例数据库&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-4-2"&gt;最左前缀原理与相关优化&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-4-3"&gt;索引选择性与前缀索引&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-4-4"&gt;InnoDB的主键选择与插入优化&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-5"&gt;后记&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#nav-6"&gt;参考文献&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;摘要&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;本文以MySQL数据库为研究对象，讨论与数据库索引相关的一些话题。特别需要说明的是，MySQL支持诸多存储引擎，而各种存储引擎对索引的支持也各不相同，因此MySQL数据库支持多种索引类型，如BTree索引，哈希索引，全文索引等等。为了避免混乱，本文将只关注于BTree索引，因为这是平常使用MySQL时主要打交道的索引，至于哈希索引和全文索引本文暂不讨论。&lt;/p&gt;&#xD;
&lt;p&gt;文章主要内容分为三个部分。&lt;/p&gt;&#xD;
&lt;p&gt;第一部分主要从数据结构及算法理论层面讨论MySQL数据库索引的数理基础。&lt;/p&gt;&#xD;
&lt;p&gt;第二部分结合MySQL数据库中MyISAM和InnoDB数据存储引擎中索引的架构实现讨论聚集索引、非聚集索引及覆盖索引等话题。&lt;/p&gt;&#xD;
&lt;p&gt;第三部分根据上面的理论基础，讨论MySQL中高性能使用索引的策略。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p style="font-size: 16px; border: 1px solid #FF6600; padding: 6px; color: #ff0000; background-color: ffeedd;"&gt;个人博客已迁移至&lt;a href="http://www.codinglabs.org"&gt;www.codinglabs.org&lt;/a&gt;，本文全文最新地址为&lt;a href="http://www.codinglabs.org/html/theory-of-mysql-index.html"&gt;http://www.codinglabs.org/html/theory-of-mysql-index.html&lt;/a&gt;，欢迎访问！！！&lt;/p&gt;&#xD;
&lt;div style="display: none;"&gt;&#xD;
&lt;p&gt;&lt;strong&gt;数据结构及算法基础&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;索引的本质&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;MySQL官方对索引的定义为：&lt;strong&gt;索引（Index）是帮助MySQL高效获取数据的数据结构。&lt;/strong&gt;提取句子主干，就可以得到索引的本质：索引是数据结构。&lt;/p&gt;&#xD;
&lt;p&gt;我们知道，数据库查询是数据库的最主要功能之一，例如下面的SQL语句：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;SELECT * FROM my_table WHERE col2 = '77'&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;可以从表&amp;ldquo;my_table&amp;rdquo;中获得&amp;ldquo;col2&amp;rdquo;为&amp;ldquo;77&amp;rdquo;的数据记录。&lt;/p&gt;&#xD;
&lt;p&gt;我们都希望查询数据的速度能尽可能的快，因此数据库系统的设计者会从查询算法的角度进行优化。最基本的查询算法当然是&lt;a href="http://en.wikipedia.org/wiki/Linear_search" target="_blank"&gt;顺序查找&lt;/a&gt;（linear search），遍历&amp;ldquo;my_table&amp;rdquo;然后逐行匹配&amp;ldquo;col2&amp;rdquo;的值是否是&amp;ldquo;77&amp;rdquo;，这种复杂度为O(n)的算法在数据量很大时显然是糟糕的，好在计算机科学的发展提供了很多更优秀的查找算法，例如&lt;a href="http://en.wikipedia.org/wiki/Binary_search_algorithm" target="_blank"&gt;二分查找&lt;/a&gt;（binary search）、&lt;a href="http://en.wikipedia.org/wiki/Binary_search_tree" target="_blank"&gt;二叉树查找&lt;/a&gt;（binary tree search）等。如果稍微分析一下会发现，每种查找算法都只能应用于特定的数据结构之上，例如二分查找要求被检索数据有序，而二叉树查找只能应用于&lt;a href="http://en.wikipedia.org/wiki/Binary_search_tree" target="_blank"&gt;二叉查找树&lt;/a&gt;上，但是数据本身的组织结构不可能完全满足各种数据结构（例如，理论上不可能同时将两列都按顺序进行组织），所以，&lt;strong&gt;在数据之外，数据库系统还维护着满足特定查找算法的数据结构，这些数据结构以某种方式引用（指向）数据，这样就可以在这些数据结构上实现高级查找算法。这种数据结构，就是索引&lt;/strong&gt;。&lt;/p&gt;&#xD;
&lt;p&gt;看一个例子：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/20110706005808162.png" alt="image" width="588" height="297" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图1&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;图1展示了一种可能的索引方式。左边是数据表，一共有两列七条记录，最左边的是数据记录的物理地址（注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的）。为了加快Col2的查找，可以维护一个右边所示的二叉查找树，每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针，这样就可以运用二叉查找在O(log&lt;sub&gt;2&lt;/sub&gt;n)的复杂度内获取到相应数据。&lt;/p&gt;&#xD;
&lt;p&gt;虽然这是一个货真价实的索引，但是实际的数据库系统几乎没有使用二叉查找树或其进化品种&lt;a href="http://en.wikipedia.org/wiki/Red-black_tree" target="_blank"&gt;红黑树&lt;/a&gt;（red-black tree）实现的，原因会在下文介绍。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;B-Tree和B+Tree&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;目前大部分数据库系统及文件系统都采用B-Tree或其变种B+Tree作为索引结构，在本文的下一节会结合存储器原理及计算机存取原理讨论为什么B-Tree和B+Tree在被如此广泛用于索引，这一节先单纯从数据结构角度描述它们。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;B-Tree&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;为了描述B-Tree，首先定义一条数据记录为一个二元组[key, data]，key为记录的键值，对于不同数据记录，key是互不相同的；data为数据记录除key外的数据。那么B-Tree是满足下列条件的数据结构：&lt;/p&gt;&#xD;
&lt;ol&gt;&#xD;
&lt;li&gt;d为大于1的一个正整数，称为B-Tree的度。&lt;/li&gt;&#xD;
&lt;li&gt;h为一个正整数，称为B-Tree的高度。&lt;/li&gt;&#xD;
&lt;li&gt;每个非叶子节点由n-1个key和n个指针组成，其中d&amp;lt;=n&amp;lt;=2d。&lt;/li&gt;&#xD;
&lt;li&gt;每个叶子节点最少包含一个key和两个指针，最多包含2d-1个key和2d个指针，叶节点的指针均为null 。&lt;/li&gt;&#xD;
&lt;li&gt;所有叶节点具有相同的深度，等于树高h。&lt;/li&gt;&#xD;
&lt;li&gt;key和指针互相间隔，节点两端是指针。&lt;/li&gt;&#xD;
&lt;li&gt;一个节点中的key从左到右非递减排列。&lt;/li&gt;&#xD;
&lt;li&gt;所有节点组成树结构。&lt;/li&gt;&#xD;
&lt;li&gt;每个指针要么为null，要么指向另外一个节点。&lt;/li&gt;&#xD;
&lt;li&gt;如果某个指针在节点node最左边且不为null，则其指向节点的所有key小于v(key&lt;sub&gt;1&lt;/sub&gt;)，其中v(key&lt;sub&gt;1&lt;/sub&gt;)为node的第一个key的值。&lt;/li&gt;&#xD;
&lt;li&gt;如果某个指针在节点node最右边且不为null，则其指向节点的所有key大于v(key&lt;sub&gt;m&lt;/sub&gt;)，其中v(key&lt;sub&gt;m&lt;/sub&gt;)为node的最后一个key的值。&lt;/li&gt;&#xD;
&lt;li&gt;如果某个指针在节点node的左右相邻key分别是key&lt;sub&gt;i&lt;/sub&gt;和key&lt;sub&gt;i+1&lt;/sub&gt;且不为null，则其指向节点的所有key小于v(key&lt;sub&gt;i+1&lt;/sub&gt;)且大于v(key&lt;sub&gt;i&lt;/sub&gt;)。&lt;/li&gt;&#xD;
&lt;/ol&gt;&#xD;
&lt;p&gt;图2是一个d=2的B-Tree示意图。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107060058097555.png" alt="image" width="458" height="115" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图2&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;由于B-Tree的特性，在B-Tree中按key检索数据的算法非常直观：首先从根节点进行二分查找，如果找到则返回对应节点的data，否则对相应区间的指针指向的节点递归进行查找，直到找到节点或找到null指针，前者查找成功，后者查找失败。B-Tree上查找算法的伪代码如下：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;BTree_Search(node, key)&#xD;
{&#xD;
    if(node == null) return null;&#xD;
&#xD;
    foreach(node.key)&#xD;
    {&#xD;
        if(node.key[i] == key) return node.data[i];&#xD;
        if(node.key[i] &amp;gt; key) return BTree_Search(point[i]-&amp;gt;node);&#xD;
    }&#xD;
&#xD;
    return BTree_Search(point[i+1]-&amp;gt;node);&#xD;
}&#xD;
&#xD;
data = BTree_Search(root, my_key);&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;关于B-Tree有一系列有趣的性质，例如&lt;strong&gt;一个度为d的B-Tree，设其索引N个key，则其树高h的上限为log&lt;sub&gt;d&lt;/sub&gt;((N+1)/2)，检索一个key，其查找节点个数的渐进复杂度为O(log&lt;sub&gt;d&lt;/sub&gt;N)。&lt;/strong&gt;从这点可以看出，B-Tree是一个非常有效率的索引数据结构。&lt;/p&gt;&#xD;
&lt;p&gt;另外，由于插入删除新的数据记录会破坏B-Tree的性质，因此在插入删除时，需要对树进行一个分裂、合并、转移等操作以保持B-Tree性质，本文不打算完整讨论B-Tree这些内容，因为已经有许多资料详细说明了B-Tree的数学性质及插入删除算法，有兴趣的朋友可以在本文末的参考文献一栏找到相应的资料进行阅读。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;B+Tree&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;B-Tree有许多变种，其中最常见的是B+Tree，例如MySQL就普遍使用B+Tree实现其索引结构。&lt;/p&gt;&#xD;
&lt;p&gt;与B-Tree相比，B+Tree有以下不同点：&lt;/p&gt;&#xD;
&lt;ol&gt;&#xD;
&lt;li&gt;每个节点的指针上限为2d而不是2d+1。&lt;/li&gt;&#xD;
&lt;li&gt;内节点不存储data，只存储key；叶子节点不存储指针。&lt;/li&gt;&#xD;
&lt;/ol&gt;&#xD;
&lt;p&gt;图3是一个简单的B+Tree示意。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/20110706005810487.png" alt="image" width="543" height="191" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图3&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;由于并不是所有节点都具有相同的域，因此B+Tree中叶节点和内节点一般大小不同。这点与B-Tree不同，虽然B-Tree中不同节点存放的key和指针可能数量不一致，但是每个节点的域和上限是一致的，所以在实现中B-Tree往往对每个节点申请同等大小的空间。&lt;/p&gt;&#xD;
&lt;p&gt;一般来说，B+Tree比B-Tree更适合实现外存储索引结构，具体原因与外存储器原理及计算机存取原理有关，将在下面讨论。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;带有顺序访问指针的B+Tree&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;一般在数据库系统或文件系统中使用的B+Tree结构都在经典B+Tree的基础上进行了优化，增加了顺序访问指针。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107061549262400.png" alt="image" width="543" height="191" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图4&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;如图4所示，在B+Tree的每个叶子节点增加一个指向相邻叶子节点的指针，就形成了带有顺序访问指针的B+Tree。做这个优化的目的是为了提高区间访问的性能，例如图4中如果要查询key为从18到49的所有数据记录，当找到18后，只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点，极大提到了区间查询效率。&lt;/p&gt;&#xD;
&lt;p&gt;这一节对B-Tree和B+Tree进行了一个简单的介绍，下一节结合存储器存取原理介绍为什么目前B+Tree是数据库系统实现索引的首选数据结构。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;为什么使用B-Tree（B+Tree）&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;上文说过，红黑树等数据结构也可以用来实现索引，但是文件系统及数据库系统普遍采用B-/+Tree作为索引结构，这一节将结合计算机组成原理相关知识讨论B-/+Tree作为索引的理论基础。&lt;/p&gt;&#xD;
&lt;p&gt;一般来说，索引本身也很大，不可能全部存储在内存中，因此索引往往以索引文件的形式存储的磁盘上。这样的话，索引查找过程中就要产生磁盘I/O消耗，相对于内存存取，I/O存取的消耗要高几个数量级，所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说，索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。下面先介绍内存和磁盘存取原理，然后再结合这些原理分析B-/+Tree作为索引的效率。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;主存存取原理&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;目前计算机使用的主存基本都是随机读写存储器（RAM），现代RAM的结构和存取原理比较复杂，这里本文抛却具体差别，抽象出一个十分简单的存取模型来说明RAM的工作原理。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107111112524101.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107111113062648.png" alt="image" width="382" height="229" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图5&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;从抽象角度看，主存是一系列的存储单元组成的矩阵，每个存储单元存储固定大小的数据。每个存储单元有唯一的地址，现代主存的编址规则比较复杂，这里将其简化成一个二维地址：通过一个行地址和一个列地址可以唯一定位到一个存储单元。图5展示了一个4 x 4的主存模型。&lt;/p&gt;&#xD;
&lt;p&gt;主存的存取过程如下：&lt;/p&gt;&#xD;
&lt;p&gt;当系统需要读取主存时，则将地址信号放到地址总线上传给主存，主存读到地址信号后，解析信号并定位到指定存储单元，然后将此存储单元数据放到数据总线上，供其它部件读取。&lt;/p&gt;&#xD;
&lt;p&gt;写主存的过程类似，系统将要写入单元地址和数据分别放在地址总线和数据总线上，主存读取两个总线的内容，做相应的写操作。&lt;/p&gt;&#xD;
&lt;p&gt;这里可以看出，主存存取的时间仅与存取次数呈线性关系，因为不存在机械操作，两次存取的数据的&amp;ldquo;距离&amp;rdquo;不会对时间有任何影响，例如，先取A0再取A1和先取A0再取D3的时间消耗是一样的。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;磁盘存取原理&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;上文说过，索引一般以文件形式存储在磁盘上，索引检索需要磁盘I/O操作。与主存不同，磁盘I/O存在机械运动耗费，因此磁盘I/O的时间消耗是巨大的。&lt;/p&gt;&#xD;
&lt;p&gt;图6是磁盘的整体结构示意图。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107111113521316.png" alt="image" width="267" height="172" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图6&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;一个磁盘由大小相同且同轴的圆形盘片组成，磁盘可以转动（各个磁盘必须同步转动）。在磁盘的一侧有磁头支架，磁头支架固定了一组磁头，每个磁头负责存取一个磁盘的内容。磁头不能转动，但是可以沿磁盘半径方向运动（实际是斜切向运动），每个磁头同一时刻也必须是同轴的，即从正上方向下看，所有磁头任何时候都是重叠的（不过目前已经有多磁头独立技术，可不受此限制）。&lt;/p&gt;&#xD;
&lt;p&gt;图7是磁盘结构的示意图。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107061549283564.png" alt="image" width="269" height="246" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图7&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;盘片被划分成一系列同心环，圆心是盘片中心，每个同心环叫做一个磁道，所有半径相同的磁道组成一个柱面。磁道被沿半径线划分成一个个小的段，每个段叫做一个扇区，每个扇区是磁盘的最小存储单元。为了简单起见，我们下面假设磁盘只有一个盘片和一个磁头。&lt;/p&gt;&#xD;
&lt;p&gt;当需要从磁盘读取数据时，系统会将数据逻辑地址传给磁盘，磁盘的控制电路按照寻址逻辑将逻辑地址翻译成物理地址，即确定要读的数据在哪个磁道，哪个扇区。为了读取这个扇区的数据，需要将磁头放到这个扇区上方，为了实现这一点，磁头需要移动对准相应磁道，这个过程叫做寻道，所耗费时间叫做寻道时间，然后磁盘旋转将目标扇区旋转到磁头下，这个过程耗费的时间叫做旋转时间。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;局部性原理与磁盘预读&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;由于存储介质的特性，磁盘本身存取就比主存慢很多，再加上机械运动耗费，磁盘的存取速度往往是主存的几百分分之一，因此为了提高效率，要尽量减少磁盘I/O。为了达到这个目的，磁盘往往不是严格按需读取，而是每次都会预读，即使只需要一个字节，磁盘也会从这个位置开始，顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理：&lt;/p&gt;&#xD;
&lt;p&gt;当一个数据被用到时，其附近的数据也通常会马上被使用。&lt;/p&gt;&#xD;
&lt;p&gt;程序运行期间所需要的数据通常比较集中。&lt;/p&gt;&#xD;
&lt;p&gt;由于磁盘顺序读取的效率很高（不需要寻道时间，只需很少的旋转时间），因此对于具有局部性的程序来说，预读可以提高I/O效率。&lt;/p&gt;&#xD;
&lt;p&gt;预读的长度一般为页（page）的整倍数。页是计算机管理存储器的逻辑块，硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块，每个存储块称为一页（在许多操作系统中，页得大小通常为4k），主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时，会触发一个缺页异常，此时系统会向磁盘发出读盘信号，磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中，然后异常返回，程序继续运行。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;B-/+Tree索引的性能分析&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;到这里终于可以分析B-/+Tree索引的性能了。&lt;/p&gt;&#xD;
&lt;p&gt;上文说过一般使用磁盘I/O次数评价索引结构的优劣。先从B-Tree分析，根据B-Tree的定义，可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理，将一个节点的大小设为等于一个页，这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的，在实际实现B-Tree还需要使用如下技巧：&lt;/p&gt;&#xD;
&lt;p&gt;每次新建节点时，直接申请一个页的空间，这样就保证一个节点物理上也存储在一个页里，加之计算机存储分配都是按页对齐的，就实现了一个node只需一次I/O。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;B-Tree中一次检索最多需要h-1次I/O（根节点常驻内存），渐进复杂度为O(h)=O(log&lt;sub&gt;d&lt;/sub&gt;N)。&lt;/strong&gt;一般实际应用中，出度d是非常大的数字，通常超过100，因此h非常小（通常不超过3）。&lt;/p&gt;&#xD;
&lt;p&gt;综上所述，用B-Tree作为索引结构效率是非常高的。&lt;/p&gt;&#xD;
&lt;p&gt;而红黑树这种结构，h明显要深的多。由于逻辑上很近的节点（父子）物理上可能很远，无法利用局部性，所以红黑树的I/O渐进复杂度也为O(h)，效率明显比B-Tree差很多。&lt;/p&gt;&#xD;
&lt;p&gt;上文还说过，B+Tree更适合外存索引，原因和内节点出度d有关。从上面分析可以看到，d越大索引的性能越好，而出度的上限取决于节点内key和data的大小：&lt;/p&gt;&#xD;
&lt;p&gt;d&lt;sub&gt;max&lt;/sub&gt; = &lt;em&gt;&lt;strong&gt;floor&lt;/strong&gt;&lt;/em&gt;(pagesize / (keysize + datasize + pointsize))&amp;nbsp;&amp;nbsp; &lt;em&gt;&lt;span style="color: #a5a5a5;"&gt;(pagesize &amp;ndash; dmax &amp;gt;= pointsize)&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;&#xD;
&lt;p&gt;或&lt;/p&gt;&#xD;
&lt;p&gt;d&lt;sub&gt;max&lt;/sub&gt; = &lt;em&gt;&lt;strong&gt;floor&lt;/strong&gt;&lt;/em&gt;(pagesize / (keysize + datasize + pointsize)) - 1&amp;nbsp;&amp;nbsp; &lt;em&gt;&lt;span style="color: #a5a5a5;"&gt;(pagesize &amp;ndash; dmax &amp;lt; pointsize)&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;&#xD;
&lt;p&gt;floor表示向下取整。由于B+Tree内节点去掉了data域，因此可以拥有更大的出度，拥有更好的性能。&lt;/p&gt;&#xD;
&lt;p&gt;这一章从理论角度讨论了与索引相关的数据结构与算法问题，下一章将讨论B+Tree是如何具体实现为MySQL中索引，同时将结合MyISAM和InnDB存储引擎介绍非聚集索引和聚集索引两种不同的索引实现形式。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;MySQL索引实现&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;在MySQL中，索引属于存储引擎级别的概念，不同存储引擎对索引的实现方式是不同的，本文主要讨论MyISAM和InnoDB两个存储引擎的索引实现方式。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;MyISAM索引实现&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;MyISAM引擎使用B+Tree作为索引结构，叶节点的data域存放的是数据记录的地址。下图是MyISAM索引的原理图：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107062333213114.png" alt="image" width="664" height="534" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图8&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;这里设表一共有三列，假设我们以Col1为主键，则图8是一个MyISAM表的主索引（Primary key）示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址。&lt;strong&gt;在MyISAM中，主索引和辅助索引（Secondary key）在结构上没有任何区别，只是主索引要求key是唯一的，而辅助索引的key可以重复。&lt;/strong&gt;如果我们在Col2上建立一个辅助索引，则此索引的结构如下图所示：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107062333248673.png" alt="image" width="664" height="534" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图9&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;同样也是一颗B+Tree，data域保存数据记录的地址。因此，&lt;strong&gt;MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引，如果指定的Key存在，则取出其data域的值，然后以data域的值为地址，读取相应数据记录。&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;MyISAM的索引方式也叫做&amp;ldquo;非聚集&amp;rdquo;的，之所以这么称呼是为了与InnoDB的聚集索引区分。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;InnoDB索引实现&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;虽然InnoDB也使用B+Tree作为索引结构，但具体实现方式却与MyISAM截然不同。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;第一个重大区别是InnoDB的数据文件本身就是索引文件。&lt;/strong&gt;从上文知道，MyISAM索引文件和数据文件是分离的，索引文件仅保存数据记录的地址。而在InnoDB中，表数据文件本身就是按B+Tree组织的一个索引结构，这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键，因此InnoDB表数据文件本身就是主索引。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107062333257669.png" alt="image" width="543" height="241" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图10&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;图10是InnoDB主索引（同时也是数据文件）的示意图，可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。因为InnoDB的数据文件本身要按主键聚集，所以InnoDB要求表必须有主键（MyISAM可以没有），如果没有显式指定，则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键，如果不存在这种列，则MySQL自动为InnoDB表生成一个隐含字段作为主键，这个字段长度为6个字节，类型为长整形。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;第二个与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。&lt;/strong&gt;换句话说，InnoDB的所有辅助索引都引用主键作为data域。例如，图11为定义在Col3上的一个辅助索引：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107102337044781.png" alt="image" width="543" height="222" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图11&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;这里以英文字符的ASCII码作为比较准则。&lt;strong&gt;聚集索引这种实现方式使得按主键的搜索十分高效，但是辅助索引搜索需要检索两遍索引：首先检索辅助索引获得主键，然后用主键到主索引中检索获得记录。&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;了解不同存储引擎的索引实现方式对于正确使用和优化索引都非常有帮助，例如知道了InnoDB的索引实现后，就很容易明白为什么不建议使用过长的字段作为主键，因为所有辅助索引都引用主索引，过长的主索引会令辅助索引变得过大。再例如，用非单调的字段作为主键在InnoDB中不是个好主意，因为InnoDB数据文件本身是一颗B+Tree，非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整，十分低效，而使用自增字段作为主键则是一个很好的选择。&lt;/p&gt;&#xD;
&lt;p&gt;下一章将具体讨论这些与索引有关的优化策略。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;索引使用策略及优化&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;MySQL的优化主要分为结构优化（Scheme optimization）和查询优化（Query optimization）。本章讨论的高性能索引策略主要属于结构优化范畴。本章的内容完全基于上文的理论基础，实际上一旦理解了索引背后的机制，那么选择高性能的策略就变成了纯粹的推理，并且可以理解这些策略背后的逻辑。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;示例数据库&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;为了讨论索引策略，需要一个数据量不算小的数据库作为示例。本文选用MySQL官方文档中提供的示例数据库之一：employees。这个数据库关系复杂度适中，且数据量较大。下图是这个数据库的E-R关系图（引用自MySQL官方手册）：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107071935378489.png" alt="image" width="833" height="655" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图12&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;MySQL官方文档中关于此数据库的页面为&lt;a title="http://dev.mysql.com/doc/employee/en/employee.html" href="http://dev.mysql.com/doc/employee/en/employee.html"&gt;http://dev.mysql.com/doc/employee/en/employee.html&lt;/a&gt;。里面详细介绍了此数据库，并提供了下载地址和导入方法，如果有兴趣导入此数据库到自己的MySQL可以参考文中内容。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;最左前缀原理与相关优化&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;高效使用索引的首要条件是知道什么样的查询会使用到索引，这个问题和B+Tree中的&amp;ldquo;最左前缀原理&amp;rdquo;有关，下面通过例子说明最左前缀原理。&lt;/p&gt;&#xD;
&lt;p&gt;这里先说一下联合索引的概念。在上文中，我们都是假设索引只引用了单个的列，实际上，MySQL中的索引可以以一定顺序引用多个列，这种索引叫做联合索引，一般的，一个联合索引是一个有序元组&amp;lt;a1, a2, &amp;hellip;, an&amp;gt;，其中各个元素均为数据表的一列，实际上要严格定义索引需要用到关系代数，但是这里我不想讨论太多关系代数的话题，因为那样会显得很枯燥，所以这里就不再做严格定义。另外，单列索引可以看成联合索引元素数为1的特例。&lt;/p&gt;&#xD;
&lt;p&gt;以employees.titles表为例，下面先查看其上都有哪些索引：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;SHOW INDEX FROM employees.titles;&#xD;
+--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+&#xD;
| Table  | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Null | Index_type |&#xD;
+--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+&#xD;
| titles |          0 | PRIMARY  |            1 | emp_no      | A         |        NULL |      | BTREE      |&#xD;
| titles |          0 | PRIMARY  |            2 | title       | A         |        NULL |      | BTREE      |&#xD;
| titles |          0 | PRIMARY  |            3 | from_date   | A         |      443308 |      | BTREE      |&#xD;
| titles |          1 | emp_no   |            1 | emp_no      | A         |      443308 |      | BTREE      |&#xD;
+--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;从结果中可以到titles表的主索引为&amp;lt;emp_no, title, from_date&amp;gt;，还有一个辅助索引&amp;lt;emp_no&amp;gt;。为了避免多个索引使事情变复杂（MySQL的SQL优化器在多索引时行为比较复杂），这里我们将辅助索引drop掉：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;ALTER TABLE employees.titles DROP INDEX emp_no;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这样就可以专心分析索引PRIMARY的行为了。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;情况一：全列匹配。&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title='Senior Engineer' AND from_date='1986-06-26';&#xD;
+----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+&#xD;
| id | select_type | table  | type  | possible_keys | key     | key_len | ref               | rows | Extra |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+&#xD;
|  1 | SIMPLE      | titles | const | PRIMARY       | PRIMARY | 59      | const,const,const |    1 |       |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;很明显，当按照索引中所有列进行精确匹配（这里精确匹配指&amp;ldquo;=&amp;rdquo;或&amp;ldquo;IN&amp;rdquo;匹配）时，索引可以被用到。这里有一点需要注意，理论上索引对顺序是敏感的，但是由于MySQL的查询优化器会自动调整where子句的条件顺序以使用适合的索引，例如我们将where中的条件顺序颠倒：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26' AND emp_no='10001' AND title='Senior Engineer';&#xD;
+----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+&#xD;
| id | select_type | table  | type  | possible_keys | key     | key_len | ref               | rows | Extra |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+&#xD;
|  1 | SIMPLE      | titles | const | PRIMARY       | PRIMARY | 59      | const,const,const |    1 |       |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;效果是一样的。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;情况二：最左前缀匹配。&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001';&#xD;
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------+&#xD;
| id | select_type | table  | type | possible_keys | key     | key_len | ref   | rows | Extra |&#xD;
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------+&#xD;
|  1 | SIMPLE      | titles | ref  | PRIMARY       | PRIMARY | 4       | const |    1 |       |&#xD;
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;当查询条件精确匹配索引的左边连续一个或几个列时，如&amp;lt;emp_no&amp;gt;或&amp;lt;emp_no, title&amp;gt;，所以可以被用到，但是只能用到一部分，即条件所组成的最左前缀。上面的查询从分析结果看用到了PRIMARY索引，但是key_len为4，说明只用到了索引的第一列前缀。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;情况三：查询条件用到了索引中列的精确匹配，但是中间某个条件未提供。&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26';&#xD;
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+&#xD;
| id | select_type | table  | type | possible_keys | key     | key_len | ref   | rows | Extra       |&#xD;
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+&#xD;
|  1 | SIMPLE      | titles | ref  | PRIMARY       | PRIMARY | 4       | const |    1 | Using where |&#xD;
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;此时索引使用情况和情况二相同，因为title未提供，所以查询只用到了索引的第一列，而后面的from_date虽然也在索引中，但是由于title不存在而无法和左前缀连接，因此需要对结果进行扫描过滤from_date（这里由于emp_no唯一，所以不存在扫描）。如果想让from_date也使用索引而不是where过滤，可以增加一个辅助索引&amp;lt;emp_no, from_date&amp;gt;，此时上面的查询会使用这个索引。除此之外，还可以使用一种称之为&amp;ldquo;隔离列&amp;rdquo;的优化方法，将emp_no与from_date之间的&amp;ldquo;坑&amp;rdquo;填上。&lt;/p&gt;&#xD;
&lt;p&gt;首先我们看下title一共有几种不同的值：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;SELECT DISTINCT(title) FROM employees.titles;&#xD;
+--------------------+&#xD;
| title              |&#xD;
+--------------------+&#xD;
| Senior Engineer    |&#xD;
| Staff              |&#xD;
| Engineer           |&#xD;
| Senior Staff       |&#xD;
| Assistant Engineer |&#xD;
| Technique Leader   |&#xD;
| Manager            |&#xD;
+--------------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;只有7种。在这种成为&amp;ldquo;坑&amp;rdquo;的列值比较少的情况下，可以考虑用&amp;ldquo;IN&amp;rdquo;来填补这个&amp;ldquo;坑&amp;rdquo;从而形成最左前缀：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles &#xD;
WHERE emp_no='10001' &#xD;
AND title IN ('Senior Engineer', 'Staff', 'Engineer', 'Senior Staff', 'Assistant Engineer', 'Technique Leader', 'Manager') &#xD;
AND from_date='1986-06-26'; &#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&#xD;
| id | select_type | table  | type  | possible_keys | key     | key_len | ref  | rows | Extra       |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&#xD;
|  1 | SIMPLE      | titles | range | PRIMARY       | PRIMARY | 59      | NULL |    7 | Using where |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这次key_len为59，说明索引被用全了，但是从type和rows看出IN实际上执行了一个range查询，这里检查了7个key。看下两种查询的性能比较：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;SHOW PROFILES;&#xD;
+----------+------------+-------------------------------------------------------------------------------+&#xD;
| Query_ID | Duration   | Query                                                                         |&#xD;
+----------+------------+-------------------------------------------------------------------------------+&#xD;
|       10 | 0.00058000 | SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26'|&#xD;
|       11 | 0.00052500 | SELECT * FROM employees.titles WHERE emp_no='10001' AND title IN ...          |&#xD;
+----------+------------+-------------------------------------------------------------------------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;&amp;ldquo;填坑&amp;rdquo;后性能提升了一点。如果经过emp_no筛选后余下很多数据，则后者性能优势会更加明显。当然，如果title的值很多，用填坑就不合适了，必须建立辅助索引。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;情况四：查询条件没有指定索引第一列。&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26';                   &#xD;
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+&#xD;
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows   | Extra       |&#xD;
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+&#xD;
|  1 | SIMPLE      | titles | ALL  | NULL          | NULL | NULL    | NULL | 443308 | Using where |&#xD;
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;由于不是最左前缀，索引这样的查询显然用不到索引。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;情况五：匹配某列的前缀字符串。&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title LIKE 'Senior%';                                 &lt;/pre&gt;&#xD;
&lt;pre &gt;+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&#xD;
| id | select_type | table  | type  | possible_keys | key     | key_len | ref  | rows | Extra       |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&#xD;
|  1 | SIMPLE      | titles | range | PRIMARY       | PRIMARY | 56      | NULL |    1 | Using where |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;此时可以用到索引，但是如果通配符不是只出现在末尾，则无法使用索引。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;情况六：范围查询。&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles WHERE emp_no&amp;lt;'10010' and title='Senior Engineer';&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&#xD;
| id | select_type | table  | type  | possible_keys | key     | key_len | ref  | rows | Extra       |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&#xD;
|  1 | SIMPLE      | titles | range | PRIMARY       | PRIMARY | 4       | NULL |   16 | Using where |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;范围列可以用到索引（必须是最左前缀），但是范围列后面的列无法用到索引。同时，索引最多用于一个范围列，因此如果查询条件中有两个范围列则无法全用到索引。&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles &#xD;
WHERE emp_no&amp;lt;'10010' &#xD;
AND title='Senior Engineer' &#xD;
AND from_date BETWEEN '1986-01-01' AND '1986-12-31';&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&#xD;
| id | select_type | table  | type  | possible_keys | key     | key_len | ref  | rows | Extra       |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&#xD;
|  1 | SIMPLE      | titles | range | PRIMARY       | PRIMARY | 4       | NULL |   16 | Using where |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;可以看到索引对第二个范围索引无能为力。这里特别要说明MySQL一个有意思的地方，那就是仅用explain可能无法区分范围索引和多值匹配，因为在type中这两者都显示为range。同时，用了&amp;ldquo;between&amp;rdquo;并不意味着就是范围查询，例如下面的查询：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles &#xD;
WHERE emp_no BETWEEN '10001' AND '10010' &#xD;
AND title='Senior Engineer' &#xD;
AND from_date BETWEEN '1986-01-01' AND '1986-12-31';&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&#xD;
| id | select_type | table  | type  | possible_keys | key     | key_len | ref  | rows | Extra       |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&#xD;
|  1 | SIMPLE      | titles | range | PRIMARY       | PRIMARY | 59      | NULL |   16 | Using where |&#xD;
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;看起来是用了两个范围查询，但作用于emp_no上的&amp;ldquo;BETWEEN&amp;rdquo;实际上相当于&amp;ldquo;IN&amp;rdquo;，也就是说emp_no实际是多值精确匹配。可以看到这个查询用到了索引全部三个列。因此在MySQL中要谨慎地区分多值匹配和范围匹配，否则会对MySQL的行为产生困惑。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;情况七：查询条件中含有函数或表达式。&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;很不幸，如果查询条件中含有函数或表达式，则MySQL不会为这列使用索引（虽然某些在数学意义上可以使用）。例如：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND left(title, 6)='Senior';&#xD;
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+&#xD;
| id | select_type | table  | type | possible_keys | key     | key_len | ref   | rows | Extra       |&#xD;
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+&#xD;
|  1 | SIMPLE      | titles | ref  | PRIMARY       | PRIMARY | 4       | const |    1 | Using where |&#xD;
+----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;虽然这个查询和情况五中功能相同，但是由于使用了函数left，则无法为title列应用索引，而情况五中用LIKE则可以。再如：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.titles WHERE emp_no - 1='10000';                        &#xD;
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+&#xD;
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows   | Extra       |&#xD;
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+&#xD;
|  1 | SIMPLE      | titles | ALL  | NULL          | NULL | NULL    | NULL | 443308 | Using where |&#xD;
+----+-------------+--------+------+---------------+------+---------+------+--------+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;显然这个查询等价于查询emp_no为10001的函数，但是由于查询条件是一个表达式，MySQL无法为其使用索引。看来MySQL还没有智能到自动优化常量表达式的程度，因此在写查询语句时尽量避免表达式出现在查询中，而是先手工私下代数运算，转换为无表达式的查询语句。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;索引选择性与前缀索引&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;既然索引可以加快查询速度，那么是不是只要是查询语句需要，就建上索引？答案是否定的。因为索引虽然加快了查询速度，但索引也是有代价的：索引文件本身要消耗存储空间，同时索引会加重插入、删除和修改记录时的负担，另外，MySQL在运行时也要消耗资源维护索引，因此索引并不是越多越好。一般两种情况下不建议建索引。&lt;/p&gt;&#xD;
&lt;p&gt;第一种情况是表记录比较少，例如一两千条甚至只有几百条记录的表，没必要建索引，让查询做全表扫描就好了。至于多少条记录才算多，这个个人有个人的看法，我个人的经验是以2000作为分界线，记录数不超过 2000可以考虑不建索引，超过2000条可以酌情考虑索引。&lt;/p&gt;&#xD;
&lt;p&gt;另一种不建议建索引的情况是索引的选择性较低。所谓索引的选择性（Selectivity），是指不重复的索引值（也叫基数，Cardinality）与表记录数（#T）的比值：&lt;/p&gt;&#xD;
&lt;p&gt;Index Selectivity = Cardinality / #T&lt;/p&gt;&#xD;
&lt;p&gt;显然选择性的取值范围为(0, 1]，选择性越高的索引价值越大，这是由B+Tree的性质决定的。例如，上文用到的employees.titles表，如果title字段经常被单独查询，是否需要建索引，我们看一下它的选择性：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM employees.titles;&#xD;
+-------------+&#xD;
| Selectivity |&#xD;
+-------------+&#xD;
|      0.0000 |&#xD;
+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;title的选择性不足0.0001（精确值为0.00001579），所以实在没有什么必要为其单独建索引。&lt;/p&gt;&#xD;
&lt;p&gt;有一种与索引选择性有关的索引优化策略叫做前缀索引，就是用列的前缀代替整个列作为索引key，当前缀长度合适时，可以做到既使得前缀索引的选择性接近全列索引，同时因为索引key变短而减少了索引文件的大小和维护开销。下面以employees.employees表为例介绍前缀索引的选择和使用。&lt;/p&gt;&#xD;
&lt;p&gt;从图12可以看到employees表只有一个索引&amp;lt;emp_no&amp;gt;，那么如果我们想按名字搜索一个人，就只能全表扫描了：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';                 &#xD;
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+&#xD;
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows   | Extra       |&#xD;
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+&#xD;
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 300024 | Using where |&#xD;
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;如果频繁按名字搜索员工，这样显然效率很低，因此我们可以考虑建索引。有两种选择，建&amp;lt;first_name&amp;gt;或&amp;lt;first_name, last_name&amp;gt;，看下两个索引的选择性：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees;&#xD;
+-------------+&#xD;
| Selectivity |&#xD;
+-------------+&#xD;
|      0.0042 |&#xD;
+-------------+&#xD;
&#xD;
SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM employees.employees;&#xD;
+-------------+&#xD;
| Selectivity |&#xD;
+-------------+&#xD;
|      0.9313 |&#xD;
+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;&amp;lt;first_name&amp;gt;显然选择性太低，&amp;lt;first_name, last_name&amp;gt;选择性很好，但是first_name和last_name加起来长度为30，有没有兼顾长度和选择性的办法？可以考虑用first_name和last_name的前几个字符建立索引，例如&amp;lt;first_name, left(last_name, 3)&amp;gt;，看看其选择性：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees;&#xD;
+-------------+&#xD;
| Selectivity |&#xD;
+-------------+&#xD;
|      0.7879 |&#xD;
+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;选择性还不错，但离0.9313还是有点距离，那么把last_name前缀加到4：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees; &#xD;
+-------------+&#xD;
| Selectivity |&#xD;
+-------------+&#xD;
|      0.9007 |&#xD;
+-------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这时选择性已经很理想了，而这个索引的长度只有18，比&amp;lt;first_name, last_name&amp;gt;短了接近一半，我们把这个前缀索引 建上：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;ALTER TABLE employees.employees &#xD;
ADD INDEX `first_name_last_name4` (first_name, last_name(4));&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;此时再执行一遍按名字查询，比较分析一下与建索引前的结果：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;SHOW PROFILES;&#xD;
+----------+------------+---------------------------------------------------------------------------------+&#xD;
| Query_ID | Duration   | Query                                                                           |&#xD;
+----------+------------+---------------------------------------------------------------------------------+&#xD;
|       87 | 0.11941700 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |&#xD;
|       90 | 0.00092400 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' |&#xD;
+----------+------------+---------------------------------------------------------------------------------+&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;性能的提升是显著的，查询速度提高了120多倍。&lt;/p&gt;&#xD;
&lt;p&gt;前缀索引兼顾索引大小和查询速度，但是其缺点是不能用于ORDER BY和GROUP BY操作，也不能用于Covering index（即当索引本身包含查询所需全部数据时，不再访问数据文件本身）。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;InnoDB的主键选择与插入优化&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;在使用InnoDB存储引擎时，如果没有特别的需要，请永远使用一个与业务无关的自增字段作为主键。&lt;/p&gt;&#xD;
&lt;p&gt;经常看到有帖子或博客讨论主键选择问题，有人建议使用业务无关的自增主键，有人觉得没有必要，完全可以使用如学号或身份证号这种唯一字段作为主键。不论支持哪种论点，大多数论据都是业务层面的。如果从数据库索引优化角度看，使用InnoDB引擎而不使用自增主键绝对是一个糟糕的主意。&lt;/p&gt;&#xD;
&lt;p&gt;上文讨论过InnoDB的索引实现，InnoDB使用聚集索引，数据记录本身被存于主索引（一颗B+Tree）的叶子节点上。这就要求同一个叶子节点内（大小为一个内存页或磁盘页）的各条数据记录按主键顺序存放，因此每当有一条新的记录插入时，MySQL会根据其主键将其插入适当的节点和位置，如果页面达到装载因子（InnoDB默认为15/16），则开辟一个新的页（节点）。&lt;/p&gt;&#xD;
&lt;p&gt;如果表使用自增主键，那么每次插入新的记录，记录就会顺序添加到当前索引节点的后续位置，当一页写满，就会自动开辟一个新的页。如下图所示：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107102337059665.png" alt="image" width="437" height="175" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图13&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;这样就会形成一个紧凑的索引结构，近似顺序填满。由于每次插入时也不需要移动已有数据，因此效率很高，也不会增加很多开销在维护索引上。&lt;/p&gt;&#xD;
&lt;p&gt;如果使用非自增主键（如果身份证号或学号等），由于每次插入主键的值近似于随机，因此每次新纪录都要被插到现有索引页得中间某个位置：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107102337098712.png" alt="image" width="229" height="175" border="0" /&gt;&lt;/p&gt;&#xD;
&lt;p align="center"&gt;&lt;strong&gt;图14&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;此时MySQL不得不为了将新记录插到合适位置而移动数据，甚至目标页面可能已经被回写到磁盘上而从缓存中清掉，此时又要从磁盘上读回来，这增加了很多开销，同时频繁的移动、分页操作造成了大量的碎片，得到了不够紧凑的索引结构，后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。&lt;/p&gt;&#xD;
&lt;p&gt;因此，只要可以，请尽量在InnoDB上采用自增字段做主键。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;后记&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;这篇文章断断续续写了半个月，主要内容就是上面这些了。不可否认，这篇文章在一定程度上有纸上谈兵之嫌，因为我本人对MySQL的使用属于菜鸟级别，更没有太多数据库调优的经验，在这里大谈数据库索引调优有点大言不惭。就当是我个人的一篇学习笔记了。&lt;/p&gt;&#xD;
&lt;p&gt;其实数据库索引调优是一项技术活，不能仅仅靠理论，因为实际情况千变万化，而且MySQL本身存在很复杂的机制，如查询优化策略和各种引擎的实现差异等都会使情况变得更加复杂。但同时这些理论是索引调优的基础，只有在明白理论的基础上，才能对调优策略进行合理推断并了解其背后的机制，然后结合实践中不断的实验和摸索，从而真正达到高效使用MySQL索引的目的。&lt;/p&gt;&#xD;
&lt;p&gt;另外，MySQL索引及其优化涵盖范围非常广，本文只是涉及到其中一部分。如与排序（ORDER BY）相关的索引优化及覆盖索引（Covering index）的话题本文并未涉及，同时除B-Tree索引外MySQL还根据不同引擎支持的哈希索引、全文索引等等本文也并未涉及。如果有机会，希望再对本文未涉及的部分进行补充吧。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;参考文献&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;[1] Baron Scbwartz等 著，王小东等 译；高性能MySQL（High Performance MySQL）；电子工业出版社，2010&lt;/p&gt;&#xD;
&lt;p&gt;[2] Michael Kofler 著，杨晓云等 译；MySQL5权威指南（The Definitive Guide to MySQL5）；人民邮电出版社，2006&lt;/p&gt;&#xD;
&lt;p&gt;[3] 姜承尧 著；MySQL技术内幕-InnoDB存储引擎；机械工业出版社，2011&lt;/p&gt;&#xD;
&lt;p&gt;[4] D Comer, Ubiquitous B-tree; ACM Computing Surveys (CSUR), 1979&lt;/p&gt;&#xD;
&lt;p&gt;[5] Codd, E. F. (1970). "A relational model of data for large shared data banks". Communications of the ACM, , Vol. 13, No. 6, pp. 377-387&lt;/p&gt;&#xD;
&lt;p&gt;[6] MySQL5.1参考手册 - &lt;a title="http://dev.mysql.com/doc/refman/5.1/zh/index.html" href="http://dev.mysql.com/doc/refman/5.1/zh/index.html"&gt;http://dev.mysql.com/doc/refman/5.1/zh/index.html&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;/div&gt;&lt;img src="http://www.cnblogs.com/leoo2sk/aggbug/2096816.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/leoo2sk/archive/2011/07/10/mysql-index.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/leoo2sk/archive/2011/07/01/temod-intro.html</id><title type="text">在SeaJS中实现html模板文件的加载（Temod介绍）</title><summary type="text">在几天前的一篇文章中，我介绍了JavaScript的模块化加载框架SeaJS。目前SeaJS支持js和css的模块化加载。在实际应用中，可能会遇到需要加载html页面模板文件的场景，例如我接触的某些应用使用Template Toolkit（以下简称TT）写页面模板，然后由js载入TT模板后渲染输出，在这种纯JavaScript渲染的Web架构中，需要将tt文件（或其它格式html模板文件）作为模块载入。由于JavaScript不支持类似于php heredoc那样的长字符串写法，所以手工做这种转换会比较纠结，而且还要处理特殊字符转义、html压缩等繁琐事情，当模板文件比较多时全手工实现费时费力还容易出错。所以我写了一个小工具temod，temod可以将html或tt等页面模板文件编译成符合CommonJS规范的module，这样任何实现了CommonJS规范的模块加载框架（如SeaJS）就可以将编译好的文件作为普通模板加载进来。</summary><published>2011-07-01T07:58:00Z</published><updated>2011-07-01T07:58:00Z</updated><author><name>T2噬菌体</name><uri>http://www.cnblogs.com/leoo2sk/</uri></author><link rel="alternate" href="http://www.cnblogs.com/leoo2sk/archive/2011/07/01/temod-intro.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/leoo2sk/archive/2011/07/01/temod-intro.html"/><content type="html">&lt;p&gt;在&lt;a href="http://www.cnblogs.com/leoo2sk/archive/2011/06/27/write-javascript-with-seajs.html" target="_blank"&gt;几天前的一篇文章&lt;/a&gt;中，我介绍了JavaScript的模块化加载框架SeaJS。目前SeaJS支持js和css的模块化加载。在实际应用中，可能会遇到需要加载html页面模板文件的场景，例如我接触的某些应用使用&lt;a href="http://template-toolkit.org/" target="_blank"&gt;Template Toolkit&lt;/a&gt;（以下简称TT）写页面模板，然后由js载入TT模板后渲染输出，在这种纯JavaScript渲染的Web架构中，需要将tt文件（或其它格式html模板文件）作为模块载入。&lt;/p&gt;&#xD;
&lt;p&gt;实现这一点的基本的方法是将tt文件内容看做一个长字符串，然后封装为纯Json格式的模块：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;div &gt;&#xD;
&lt;div &gt;&#xD;
&lt;div &gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;define({&#xD;
    html: '&amp;lt;div&amp;gt;html&amp;lt;/div&amp;gt;' //原html模板文件代码&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;由于JavaScript不支持类似于&lt;a href="http://www.php.net/manual/en/language.types.string.php#language.types.string.syntax.heredoc" target="_blank"&gt;php heredoc&lt;/a&gt;那样的长字符串写法，所以手工做这种转换会比较纠结，而且还要处理特殊字符转义、html压缩等繁琐事情，当模板文件比较多时全手工实现费时费力还容易出错。所以我写了一个小工具&lt;a href="https://github.com/ericzy/temod" target="_blank"&gt;temod&lt;/a&gt;，temod可以将html或tt等页面模板文件编译成符合CommonJS规范的module，这样任何实现了CommonJS规范的模块加载框架（如SeaJS）就可以将编译好的文件作为普通模板加载进来。&lt;/p&gt;&#xD;
&lt;p&gt;Temod目前托管在GitHub上，访问地址是：&lt;a href="https://github.com/ericzy/temod"&gt;https://github.com/ericzy/temod&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;Temod使用简介&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;获取temod&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;获取temod有两种方式，一是通过&lt;a href="http://git-scm.com/" target="_blank"&gt;Git&lt;/a&gt;获取整个temod git repository：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;git clone https://github.com/ericzy/temod.git&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;或者也可以通过这个地址直接下载temod.js脚本：&lt;a href="https://raw.github.com/ericzy/temod/master/bin/temod.js"&gt;https://raw.github.com/ericzy/temod/master/bin/temod.js&lt;/a&gt;。例如在linux下可以通过wget获取：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;wget https://raw.github.com/ericzy/temod/master/bin/temod.js&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;&lt;strong&gt;运行temod&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;Temod程序只有一个temod.js，这个脚本使用&lt;a href="http://nodejs.org/" target="_blank"&gt;NodeJS&lt;/a&gt;写成，不需要任何安装，直接使用NodeJS执行即可：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;node temod.js path/to/template/directory [option]&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;temod.js的第一个参数为必须参数，是模板文件所在目录，可选参数option有如下几个：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;span style="color: #4f81bd;" color="#4f81bd"&gt;&lt;strong&gt;--compress=on|off&lt;/strong&gt;&lt;/span&gt; &amp;mdash;&amp;mdash; 是否开启html代码压缩。如果设为on，则temod会将整个模板文件压缩成一个不换行字符串；如果设为off，则会将模板文件中每一行转为一个字符串，这些字符串组成数组，通过join输出页面，保持所有html代码格式。一般来说，前者节省带宽，而后者传给客户端的html源代码可读性更好。默认开启压缩。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;span style="color: #4f81bd;" color="#4f81bd"&gt;--encoding=utf-8&lt;/span&gt;&lt;/strong&gt; &amp;mdash;&amp;mdash; 模板文件和输出文件的编码，默认为utf-8。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;span style="color: #4f81bd;" color="#4f81bd"&gt;&lt;strong&gt;--ext=htm,html,tt,tpl,&amp;hellip;&lt;/strong&gt;&lt;/span&gt; &amp;mdash;&amp;mdash; 模板文件扩展名。这个选项指定了所有模板文件的扩展名，如果文件在模板文件目录里但不包含指定的扩展名，则不会被编译。多个扩展名之间用英文逗号隔开。默认为htm,html,tt,tpl。&lt;/p&gt;&#xD;
&lt;p&gt;temod会自动遍历指定的模板目录，生成一个与其结构一致的目录，名称为原目录名加&amp;ldquo;_build&amp;rdquo;，其中包含了所有编译好的文件，文件名为源文件名加&amp;ldquo;.js&amp;rdquo;。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;Temod使用示例&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;下面给一个temod的使用示例。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107011541568592.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border-width: 0px;" title="image" alt="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107011541584853.png" border="0" height="162" width="169" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;如上图所示，其中bin下放着temod.js，例子放在example下。example目录内部如下：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107011541591397.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border-width: 0px;" title="image" alt="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107011542012086.png" border="0" height="192" width="260" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;刚开始没有template_build，这是一个编译生成的目录。其中template下放有一个模板文件content.tt：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;div&amp;gt;&#xD;
    &amp;lt;p&amp;gt;This is a Temod Sample.&amp;lt;/p&amp;gt;&#xD;
    &amp;lt;p&amp;gt;&amp;lt;a href="https://github.com/ericzy/temod"&amp;gt;Click here to visit home page&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&#xD;
&amp;lt;/div&amp;gt;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;现在到bin目录下运行如下命令：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;node temod.js ../example/template --compress=on --ext=tt --encoding=utf-8&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;或者省略默认参数也可以：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;node temod.js ../example/template&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这会在example下生成template_build目录，里面有个content.tt.js文件，内容如下：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;//This file was generated by temod (https://github.com/ericy/temod)&#xD;
&#xD;
define({html: '&amp;lt;div&amp;gt;&amp;lt;p&amp;gt;This is a Temod Sample.&amp;lt;/p&amp;gt;&amp;lt;p&amp;gt;&amp;lt;a href="https://github.com/ericzy/temod"&amp;gt;Click here to visit home page&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;'});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;然后我们在js下写一个init.js模块，在这个模块中加载这个模板并做一些渲染操作：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;define(function(require, exports, module) {&#xD;
    var tpl = require('../template_build/content.tt');&#xD;
&#xD;
    document.body.innerHTML = tpl.html;&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;最后在index.html中将init作为入口模块：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;!DOCTYPE HTML&amp;gt;&#xD;
&amp;lt;html lang="en"&amp;gt;&#xD;
&amp;lt;head&amp;gt;&#xD;
&amp;lt;meta charset="UTF-8"&amp;gt;&#xD;
&amp;lt;title&amp;gt;Temod Sample&amp;lt;/title&amp;gt;&#xD;
&amp;lt;/head&amp;gt;&#xD;
&amp;lt;body&amp;gt;&#xD;
&#xD;
&amp;lt;script src="./js/sea.js" data-main="./js/init"&amp;gt;&amp;lt;/script&amp;gt;&#xD;
&amp;lt;/body&amp;gt;&#xD;
&amp;lt;/html&amp;gt;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;访问index.html，则可看到如下效果：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107011542028314.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border-width: 0px;" title="image" alt="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201107/201107011542036494.png" border="0" height="119" width="254" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;如果不了解SeaJS，请看这篇文章：&lt;a title="http://www.cnblogs.com/leoo2sk/archive/2011/06/27/write-javascript-with-seajs.html" href="http://www.cnblogs.com/leoo2sk/archive/2011/06/27/write-javascript-with-seajs.html"&gt;http://www.cnblogs.com/leoo2sk/archive/2011/06/27/write-javascript-with-seajs.html&lt;/a&gt;。&lt;/p&gt;&#xD;
&lt;p&gt;需要这个例子的完整代码可以访问&lt;a title="https://github.com/ericzy/temod/tree/master/example" href="https://github.com/ericzy/temod/tree/master/example"&gt;https://github.com/ericzy/temod/tree/master/example&lt;/a&gt;。&lt;/p&gt;&lt;img src="http://www.cnblogs.com/leoo2sk/aggbug/2095565.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/leoo2sk/archive/2011/07/01/temod-intro.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/leoo2sk/archive/2011/06/27/write-javascript-with-seajs.html</id><title type="text">使用SeaJS实现模块化JavaScript开发</title><summary type="text">SeaJS是一个遵循CommonJS规范的JavaScript模块加载框架，可以实现JavaScript的模块化开发及加载机制。与jQuery等JavaScript框架不同，SeaJS不会扩展封装语言特性，而只是实现JavaScript的模块化及按模块加载。SeaJS的主要目的是令JavaScript开发模块化并可以轻松愉悦进行加载，将前端工程师从繁重的JavaScript文件及对象依赖处理中解放出来，可以专注于代码本身的逻辑。SeaJS可以与jQuery这类框架完美集成。使用SeaJS可以提高JavaScript代码的可读性和清晰度，解决目前JavaScript编程中普遍存在的依赖关系混乱和代码纠缠等问题，方便代码的编写和维护。SeaJS的作者是淘宝前端工程师玉伯。SeaJS本身遵循KISS（Keep It Simple, Stupid）理念进行开发，其本身仅有个位数的API，因此学习起来毫无压力。在学习SeaJS的过程中，处处能感受到KISS原则的精髓——仅做一件事，做好一件事。本文首先通过一个例子直观对比传统JavaScript编程和使用SeaJS的模块化JavaScript编程</summary><published>2011-06-27T00:20:00Z</published><updated>2011-06-27T00:20:00Z</updated><author><name>T2噬菌体</name><uri>http://www.cnblogs.com/leoo2sk/</uri></author><link rel="alternate" href="http://www.cnblogs.com/leoo2sk/archive/2011/06/27/write-javascript-with-seajs.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/leoo2sk/archive/2011/06/27/write-javascript-with-seajs.html"/><content type="html">&lt;a id="navi-1"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;前言&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://seajs.com/" target="_blank"&gt;SeaJS&lt;/a&gt;是一个遵循&lt;a href="http://wiki.commonjs.org/" target="_blank"&gt;CommonJS&lt;/a&gt;规范的JavaScript模块加载框架，可以实现JavaScript的模块化开发及加载机制。与&lt;a href="http://jquery.com" target="_blank"&gt;jQuery&lt;/a&gt;等JavaScript框架不同，SeaJS不会扩展封装语言特性，而只是实现JavaScript的模块化及按模块加载。SeaJS的主要目的是令JavaScript开发模块化并可以轻松愉悦进行加载，将前端工程师从繁重的JavaScript文件及对象依赖处理中解放出来，可以专注于代码本身的逻辑。SeaJS可以与jQuery这类框架完美集成。使用SeaJS可以提高JavaScript代码的可读性和清晰度，解决目前JavaScript编程中普遍存在的依赖关系混乱和代码纠缠等问题，方便代码的编写和维护。&lt;/p&gt;&#xD;
&lt;p&gt;SeaJS的作者是淘宝前端工程师&lt;a href="http://lifesinger.wordpress.com/" target="_blank"&gt;玉伯&lt;/a&gt;。&lt;/p&gt;&#xD;
&lt;p&gt;SeaJS本身遵循KISS（Keep It Simple, Stupid）理念进行开发，其本身仅有个位数的API，因此学习起来毫无压力。在学习SeaJS的过程中，处处能感受到KISS原则的精髓&amp;mdash;&amp;mdash;仅做一件事，做好一件事。&lt;/p&gt;&#xD;
&lt;p&gt;本文首先通过一个例子直观对比传统JavaScript编程和使用SeaJS的模块化JavaScript编程，然后详细讨论SeaJS的使用方法，最后给出一些与SeaJS相关的资料。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-1"&gt;前言&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-2"&gt;传统模式 vs SeaJS模块化&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-2-1"&gt;传统开发&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-2-2"&gt;SeaJS模块化开发&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-3"&gt;使用SeaJS&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-3-1"&gt;下载及安装&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-3-2"&gt;SeaJS基本开发原则&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-3-3"&gt;模块的定义及编写&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-3-4"&gt;模块的载入及引用&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-3-5"&gt;SeaJS的全局配置&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-3-6"&gt;SeaJS如何与现有JS库配合使用&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-3-7"&gt;SeaJS项目打包部署&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-4"&gt;一个完整的例子&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#navi-5"&gt;主要参考文献&amp;amp;SeaJS学习资源&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;a id="navi-2"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;传统模式 vs SeaJS模块化&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;假设我们现在正在开发一个Web应用TinyApp，我们决定在TinyApp中使用jQuery框架。TinyApp的首页会用到module1.js，module1.js依赖module2.js和module3.js，同时module3.js依赖module4.js。&lt;/p&gt;&#xD;
&lt;a id="navi-2-1"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;传统开发&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;使用传统的开发方法，各个js文件代码如下：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;//module1.js&#xD;
var module1 = {&#xD;
    run: function() {&#xD;
        return $.merge(['module1'], $.merge(module2.run(), module3.run()));&#xD;
    }&#xD;
}&#xD;
&#xD;
//module2.js&#xD;
var module1 = {&#xD;
    run: function() {&#xD;
        return ['module2'];&#xD;
    }&#xD;
}&#xD;
&#xD;
//module3.js&#xD;
var module3 = {&#xD;
    run: function() {&#xD;
        return $.merge(['module3'], module4.run());&#xD;
    }&#xD;
}&#xD;
&#xD;
//module4.js&#xD;
var module4 = {&#xD;
    run: function() {&#xD;
        return ['module4'];&#xD;
    }&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;此时index.html需要引用module1.js及其所有下层依赖（注意顺序）：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;!DOCTYPE HTML&amp;gt;&#xD;
&amp;lt;html lang="zh-CN"&amp;gt;&#xD;
&amp;lt;head&amp;gt;&#xD;
    &amp;lt;meta charset="UTF-8"&amp;gt;&#xD;
    &amp;lt;title&amp;gt;TinyApp&amp;lt;/title&amp;gt;&#xD;
    &amp;lt;script src="./jquery-min.js"&amp;gt;&amp;lt;/script&amp;gt;&#xD;
    &amp;lt;script src="./module4.js"&amp;gt;&amp;lt;/script&amp;gt;&#xD;
    &amp;lt;script src="./module2.js"&amp;gt;&amp;lt;/script&amp;gt;&#xD;
    &amp;lt;script src="./module3.js"&amp;gt;&amp;lt;/script&amp;gt;&#xD;
    &amp;lt;script src="./module1.js"&amp;gt;&amp;lt;/script&amp;gt;&#xD;
&amp;lt;/head&amp;gt;&#xD;
&amp;lt;body&amp;gt;&#xD;
    &amp;lt;p &amp;gt;&amp;lt;/p&amp;gt;&#xD;
    &amp;lt;script&amp;gt;&#xD;
        $('.content').html(module1.run());&#xD;
    &amp;lt;/script&amp;gt;&#xD;
&amp;lt;/body&amp;gt;&#xD;
&amp;lt;/html&amp;gt;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;随着项目的进行，js文件会越来越多，依赖关系也会越来越复杂，使得js代码和html里的script列表往往变得难以维护。&lt;/p&gt;&#xD;
&lt;a id="navi-2-2"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;SeaJS模块化开发&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;下面看看如何使用SeaJS实现相同的功能。&lt;/p&gt;&#xD;
&lt;p&gt;首先是index.html：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;!DOCTYPE HTML&amp;gt;&#xD;
&amp;lt;html lang="zh-CN"&amp;gt;&#xD;
&amp;lt;head&amp;gt;&#xD;
    &amp;lt;meta charset="UTF-8"&amp;gt;&#xD;
    &amp;lt;title&amp;gt;TinyApp&amp;lt;/title&amp;gt;&#xD;
&amp;lt;/head&amp;gt;&#xD;
&amp;lt;body&amp;gt;&#xD;
    &amp;lt;p &amp;gt;&amp;lt;/p&amp;gt;&#xD;
    &amp;lt;script src="./sea.js"&amp;gt;&amp;lt;/script&amp;gt;&#xD;
    &amp;lt;script&amp;gt;&#xD;
        seajs.use('./init', function(init) {&#xD;
            init.initPage();&#xD;
        });&#xD;
    &amp;lt;/script&amp;gt;&#xD;
&amp;lt;/body&amp;gt;&#xD;
&amp;lt;/html&amp;gt;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;可以看到html页面不再需要引入所有依赖的js文件，而只是引入一个sea.js，sea.js会处理所有依赖，加载相应的js文件，加载策略可以选择在渲染页面时一次性加载所有js文件，也可以按需加载（用到时才加载响应js），具体加载策略使用方法下文讨论。&lt;/p&gt;&#xD;
&lt;p&gt;index.html加载了init模块，并使用此模块的initPage方法初始化页面数据，这里先不讨论代码细节。&lt;/p&gt;&#xD;
&lt;p&gt;下面看一下模块化后JavaScript的写法：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;//jquery.js&#xD;
define(function(require, exports, module) = {&#xD;
&#xD;
    //原jquery.js代码...&#xD;
&#xD;
    module.exports = $.noConflict(true);&#xD;
});&#xD;
&#xD;
//init.js&#xD;
define(function(require, exports, module) = {&#xD;
    var $ = require('jquery');&#xD;
    var m1 = require('module1');&#xD;
    &#xD;
    exports.initPage = function() {&#xD;
        $('.content').html(m1.run());    &#xD;
    }&#xD;
});&#xD;
&#xD;
//module1.js&#xD;
define(function(require, exports, module) = {&#xD;
    var $ = require('jquery');&#xD;
    var m2 = require('module2');&#xD;
    var m3 = require('module3');&#xD;
    &#xD;
    exports.run = function() {&#xD;
        return $.merge(['module1'], $.merge(m2.run(), m3.run()));    &#xD;
    }&#xD;
});&#xD;
&#xD;
//module2.js&#xD;
define(function(require, exports, module) = {&#xD;
    exports.run = function() {&#xD;
        return ['module2'];&#xD;
    }&#xD;
});&#xD;
&#xD;
//module3.js&#xD;
define(function(require, exports, module) = {&#xD;
    var $ = require('jquery');&#xD;
    var m4 = require('module4');&#xD;
    &#xD;
    exports.run = function() {&#xD;
        return $.merge(['module3'], m4.run());    &#xD;
    }&#xD;
});&#xD;
&#xD;
//module4.js&#xD;
define(function(require, exports, module) = {&#xD;
    exports.run = function() {&#xD;
        return ['module4'];&#xD;
    }&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;乍看之下代码似乎变多变复杂了，这是因为这个例子太简单，如果是大型项目，SeaJS代码的优势就会显现出来。不过从这里我们还是能窥探到一些SeaJS的特性：&lt;/p&gt;&#xD;
&lt;p&gt;一是html页面不用再维护冗长的script标签列表，只要引入一个sea.js即可。&lt;/p&gt;&#xD;
&lt;p&gt;二是js代码以模块进行组织，各个模块通过require引入自己依赖的模块，代码清晰明了。&lt;/p&gt;&#xD;
&lt;p&gt;通过这个例子朋友们应该对SeaJS有了一个直观的印象，下面本文具体讨论SeaJS的使用。&lt;/p&gt;&#xD;
&lt;a id="navi-3"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;使用SeaJS&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;a id="navi-3-1"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;下载及安装&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;要在项目中使用SeaJS，你所有需要做的准备工作就是下载sea.js然后放到你项目的某个位置。&lt;/p&gt;&#xD;
&lt;p&gt;SeaJS项目目前托管在&lt;a href="https://github.com/" target="_blank"&gt;GitHub&lt;/a&gt;上，主页为&lt;a title="https://github.com/seajs/seajs/" href="https://github.com/seajs/seajs/" target="_blank"&gt;https://github.com/seajs/seajs/&lt;/a&gt;。可以到其git库的build目录下（&lt;a title="https://github.com/seajs/seajs/tree/master/build" href="https://github.com/seajs/seajs/tree/master/build" target="_blank"&gt;https://github.com/seajs/seajs/tree/master/build&lt;/a&gt;）下载sea.js（已压缩）或sea-debug.js（未压缩）。&lt;/p&gt;&#xD;
&lt;p&gt;下载完成后放到项目的相应位置，然后在页面中通过&amp;lt;script&amp;gt;标签引入，你就可以使用SeaJS了。&lt;/p&gt;&#xD;
&lt;a id="navi-3-2"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;SeaJS基本开发原则&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;在讨论SeaJS的具体使用前，先介绍一下SeaJS的模块化理念和开发原则。&lt;/p&gt;&#xD;
&lt;p&gt;使用SeaJS开发JavaScript的基本原则就是：一切皆为模块。引入SeaJS后，编写JavaScript代码就变成了编写一个又一个模块，SeaJS中模块的概念有点类似于面向对象中的类&amp;mdash;&amp;mdash;模块可以拥有数据和方法，数据和方法可以定义为公共或私有，公共数据和方法可以供别的模块调用。&lt;/p&gt;&#xD;
&lt;p&gt;另外，每个模块应该都定义在一个单独js文件中，即一个对应一个模块。&lt;/p&gt;&#xD;
&lt;p&gt;下面介绍模块的编写和调用。&lt;/p&gt;&#xD;
&lt;a id="navi-3-3"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;模块的定义及编写&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;模块定义函数define&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;SeaJS中使用&amp;ldquo;define&amp;rdquo;函数定义一个模块。因为SeaJS的文档并没有关于define的完整参考，所以我阅读了SeaJS源代码，发现define可以接收三个参数：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;/**&#xD;
* Defines a module.&#xD;
* @param {string=} id The module id.&#xD;
* @param {Array.|string=} deps The module dependencies.&#xD;
* @param {function()|Object} factory The module factory function.&#xD;
*/&#xD;
fn.define = function(id, deps, factory) {&#xD;
    //code of function&amp;hellip;&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;上面是我从SeaJS源码中摘录出来的，define可以接收的参数分别是模块ID，依赖模块数组及工厂函数。我阅读源代码后发现define对于不同参数个数的解析规则如下：&lt;/p&gt;&#xD;
&lt;p&gt;如果只有一个参数，则赋值给factory。&lt;/p&gt;&#xD;
&lt;p&gt;如果有两个参数，第二个赋值给factory；第一个如果是array则赋值给deps，否则赋值给id。&lt;/p&gt;&#xD;
&lt;p&gt;如果有三个参数，则分别赋值给id，deps和factory。&lt;/p&gt;&#xD;
&lt;p&gt;但是，包括SeaJS的官方示例在内几乎所有用到define的地方都只传递一个工厂函数进去，类似与如下代码：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;define(function(require, exports, module) {&#xD;
    //code of the module...&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;个人建议遵循SeaJS官方示例的标准，用一个参数的define定义模块。那么id和deps会怎么处理呢？&lt;/p&gt;&#xD;
&lt;p&gt;id是一个模块的标识字符串，define只有一个参数时，id会被默认赋值为此js文件的绝对路径。如example.com下的a.js文件中使用define定义模块，则这个模块的ID会赋值为&amp;ldquo;http://example.com/a.js&amp;rdquo;，没有特别的必要建议不要传入id。deps一般也不需要传入，需要用到的模块用require加载即可。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;工厂函数factory解析&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;工厂函数是模块的主体和重点。在只传递一个参数给define时（推荐写法），这个参数就是工厂函数，此时工厂函数的三个参数分别是：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;require&lt;/em&gt;&amp;mdash;&amp;mdash;模块加载函数，用于记载依赖模块。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;exports&lt;/em&gt;&amp;mdash;&amp;mdash;接口点，将数据或方法定义在其上则将其暴露给外部调用。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;module&lt;/em&gt;&amp;mdash;&amp;mdash;模块的元数据。&lt;/p&gt;&#xD;
&lt;p&gt;这三个参数可以根据需要选择是否需要显示指定。&lt;/p&gt;&#xD;
&lt;p&gt;下面说一下module。module是一个对象，存储了模块的元信息，具体如下：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;module.id&lt;/em&gt;&amp;mdash;&amp;mdash;模块的ID。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;module.dependencies&lt;/em&gt;&amp;mdash;&amp;mdash;一个数组，存储了此模块依赖的所有模块的ID列表。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;module.exports&lt;/em&gt;&amp;mdash;&amp;mdash;与exports指向同一个对象。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;三种编写模块的模式&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;第一种定义模块的模式是基于exports的模式：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;define(function(require, exports, module) {&#xD;
    var a = require('a'); //引入a模块&#xD;
    var b = require('b'); //引入b模块&#xD;
&#xD;
    var data1 = 1; //私有数据&#xD;
&#xD;
    var func1 = function() { //私有方法&#xD;
        return a.run(data1);&#xD;
    }&#xD;
&#xD;
    exports.data2 = 2; //公共数据&#xD;
&#xD;
    exports.func2 = function() { //公共方法&#xD;
        return 'hello';&#xD;
    }&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;上面是一种比较&amp;ldquo;正宗&amp;rdquo;的模块定义模式。除了将公共数据和方法附加在exports上，也可以直接返回一个对象表示模块，如下面的代码与上面的代码功能相同：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;define(function(require) {&#xD;
    var a = require('a'); //引入a模块&#xD;
    var b = require('b'); //引入b模块&#xD;
&#xD;
    var data1 = 1; //私有数据&#xD;
&#xD;
    var func1 = function() { //私有方法&#xD;
        return a.run(data1);&#xD;
    }&#xD;
&#xD;
    return {&#xD;
        data2: 2,&#xD;
        func2: function() {&#xD;
            return 'hello';&#xD;
        }&#xD;
    };&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;如果模块定义没有其它代码，只返回一个对象，还可以有如下简化写法：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;define({&#xD;
    data: 1,&#xD;
    func: function() {&#xD;
        return 'hello';&#xD;
    }&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;第三种方法对于定义纯JSON数据的模块非常合适。&lt;/p&gt;&#xD;
&lt;a id="navi-3-4"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;模块的载入和引用&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;模块的寻址算法&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;上文说过一个模块对应一个js文件，而载入模块时一般都是提供一个字符串参数告诉载入函数需要的模块，所以就需要有一套从字符串标识到实际模块所在文件路径的解析算法。SeaJS支持如下标识：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;绝对地址&lt;/em&gt;&amp;mdash;&amp;mdash;给出js文件的绝对路径。&lt;/p&gt;&#xD;
&lt;p&gt;如&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;require("http://example/js/a");&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;就代表载入&amp;ldquo;http://example/js/a.js&amp;rdquo;。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;相对地址&lt;/em&gt;&amp;mdash;&amp;mdash;用相对调用载入函数所在js文件的相对地址寻找模块。&lt;/p&gt;&#xD;
&lt;p&gt;例如在&amp;ldquo;http://example/js/b.js&amp;rdquo;中载入&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;require("./c");&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;则载入&amp;ldquo;http://example/js/c.js&amp;rdquo;。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;基址地址&lt;/em&gt;&amp;mdash;&amp;mdash;如果载入字符串标识既不是绝对路径也不是以&amp;rdquo;./&amp;rdquo;开头，则相对SeaJS全局配置中的&amp;ldquo;base&amp;rdquo;来寻址，这种方法稍后讨论。&lt;/p&gt;&#xD;
&lt;p&gt;注意上面在载入模块时都不用传递后缀名&amp;ldquo;.js&amp;rdquo;，SeaJS会自动添加&amp;ldquo;.js&amp;rdquo;。但是下面三种情况下不会添加：&lt;/p&gt;&#xD;
&lt;p&gt;载入css时，如&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;require("./module1-style.css");&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;路径中含有&amp;rdquo;?&amp;rdquo;时，如&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;require(&lt;a href="http://example/js/a.json?cb=func"&gt;http://example/js/a.json?cb=func&lt;/a&gt;);&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;路径以&amp;rdquo;#&amp;rdquo;结尾时，如&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;require("http://example/js/a.json#");&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;根据应用场景的不同，SeaJS提供了三个载入模块的API，分别是seajs.use，require和require.async，下面分别介绍。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;seajs.use&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;seajs.use主要用于载入入口模块。入口模块相当于C程序的main函数，同时也是整个模块依赖树的根。上面在TinyApp小例子中，init就是入口模块。seajs.use用法如下：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;//单一模式&#xD;
seajs.use('./a');&#xD;
&#xD;
//回调模式&#xD;
seajs.use('./a', function(a) {&#xD;
  a.run();&#xD;
});&#xD;
&#xD;
//多模块模式&#xD;
seajs.use(['./a', './b'], function(a, b) {&#xD;
  a.run();&#xD;
  b.run();&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;一般seajs.use只用在页面载入入口模块，SeaJS会顺着入口模块解析所有依赖模块并将它们加载。如果入口模块只有一个，也可以通过给引入sea.js的script标签加入&amp;rdquo;data-main&amp;rdquo;属性来省略seajs.use，例如，上面TinyApp的index.html也可以改为如下写法：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;!DOCTYPE HTML&amp;gt;&#xD;
&amp;lt;html lang="zh-CN"&amp;gt;&#xD;
&amp;lt;head&amp;gt;&#xD;
    &amp;lt;meta charset="UTF-8"&amp;gt;&#xD;
    &amp;lt;title&amp;gt;TinyApp&amp;lt;/title&amp;gt;&#xD;
&amp;lt;/head&amp;gt;&#xD;
&amp;lt;body&amp;gt;&#xD;
    &amp;lt;p &amp;gt;&amp;lt;/p&amp;gt;&#xD;
    &amp;lt;script src="./sea.js" data-main="./init"&amp;gt;&amp;lt;/script&amp;gt;&#xD;
&amp;lt;/body&amp;gt;&#xD;
&amp;lt;/html&amp;gt;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这种写法会令html更加简洁。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;require&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;require是SeaJS主要的模块加载方法，当在一个模块中需要用到其它模块时一般用require加载：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;var m = require('/path/to/module/file');&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这里简要介绍一下SeaJS的自动加载机制。上文说过，使用SeaJS后html只要包含sea.js即可，那么其它js文件是如何加载进来的呢？SeaJS会首先下载入口模块，然后顺着入口模块使用正则表达式匹配代码中所有的require，再根据require中的文件路径标识下载相应的js文件，对下载来的js文件再迭代进行类似操作。整个过程类似图的遍历操作（因为可能存在交叉循环依赖所以整个依赖数据结构是一个图而不是树）。&lt;/p&gt;&#xD;
&lt;p&gt;明白了上面这一点，下面的规则就很好理解了：&lt;/p&gt;&#xD;
&lt;p&gt;传给require的路径标识必须是字符串字面量，不能是表达式，如下面使用require的方法是错误的：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;require('module' + '1');&#xD;
&#xD;
require('Module'.toLowerCase());&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这都会造成SeaJS无法进行正确的正则匹配以下载相应的js文件。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;require.async&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;上文说过SeaJS会在html页面打开时通过静态分析一次性记载所有需要的js文件，如果想要某个js文件在用到时才下载，可以使用require.async：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;require.async('/path/to/module/file', function(m) {&#xD;
    //code of callback...&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这样只有在用到这个模块时，对应的js文件才会被下载，也就实现了JavaScript代码的按需加载。&lt;/p&gt;&#xD;
&lt;a id="navi-3-5"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;SeaJS的全局配置&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;SeaJS提供了一个seajs.config方法可以设置全局配置，接收一个表示全局配置的配置对象。具体使用方法如下：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;seajs.config({&#xD;
    base: 'path/to/jslib/',&#xD;
    alias: {&#xD;
      'app': 'path/to/app/'&#xD;
    },&#xD;
    charset: 'utf-8',&#xD;
    timeout: 20000,&#xD;
    debug: false&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;其中&lt;em&gt;base&lt;/em&gt;表示基址寻址时的基址路径。例如base设置为&amp;rdquo;http://example.com/js/3-party/&amp;rdquo;，则&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;var $ = require('jquery');&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;会载入&amp;rdquo;http://example.com/js/3-party/jquery.js&amp;rdquo;。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;alias&lt;/em&gt;可以对较长的常用路径设置缩写。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;charset&lt;/em&gt;表示下载js时script标签的charset属性。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;timeout&lt;/em&gt;表示下载文件的最大时长，以毫秒为单位。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;debug&lt;/em&gt;表示是否工作在调试模式下。&lt;/p&gt;&#xD;
&lt;a id="navi-3-6"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;SeaJS如何与现有JS库配合使用&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;要将现有JS库如jQuery与SeaJS一起使用，只需根据SeaJS的的模块定义规则对现有库进行一个封装。例如，下面是对jQuery的封装方法：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;define(function() {&#xD;
&#xD;
//{{{jQuery原有代码开始&#xD;
/*!  &#xD;
 * jQuery JavaScript Library v1.6.1&#xD;
 * http://jquery.com/&#xD;
 *&#xD;
 * Copyright 2011, John Resig&#xD;
 * Dual licensed under the MIT or GPL Version 2 licenses.&#xD;
 * http://jquery.org/license&#xD;
 *&#xD;
 * Includes Sizzle.js&#xD;
 * http://sizzlejs.com/&#xD;
 * Copyright 2011, The Dojo Foundation&#xD;
 * Released under the MIT, BSD, and GPL Licenses.&#xD;
 *&#xD;
 * Date: Thu May 12 15:04:36 2011 -0400&#xD;
 */&#xD;
//...&#xD;
//}}}jQuery原有代码结束&#xD;
&#xD;
return $.noConflict();&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;a id="navi-3-7"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;SeaJS项目的打包部署&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;SeaJS本来集成了一个打包部署工具spm，后来作者为了更KISS一点，将spm拆出了SeaJS而成为了一个&lt;a href="https://github.com/seajs/spm/" target="_blank"&gt;单独的项目&lt;/a&gt;。spm的核心思想是将所有模块的代码都合并压缩后并入入口模块，由于SeaJS本身的特性，html不需要做任何改动就可以很方便的在开发环境和生产环境间切换。但是由于spm目前并没有发布正式版本，所以本文不打算详细介绍，有兴趣的朋友可以参看其github项目主页&lt;a title="https://github.com/seajs/spm/" href="https://github.com/seajs/spm/"&gt;https://github.com/seajs/spm/&lt;/a&gt;。&lt;/p&gt;&#xD;
&lt;p&gt;其实，由于每个项目所用的JS合并和压缩工具不尽相同，所以spm可能并不是完全适合每个项目。在了解了SeaJS原理后，完全可以自己写一个符合自己项目特征的合并打包脚本。&lt;/p&gt;&#xD;
&lt;a id="navi-4"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;一个完整的例子&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;上文说了那么多，知识点比较分散，所以最后我打算用一个完整的SeaJS例子把这些知识点串起来，方便朋友们归纳回顾。这个例子包含如下文件：&lt;/p&gt;&#xD;
&lt;p&gt;index.html&amp;mdash;&amp;mdash;主页面。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;sea.js&lt;/em&gt;&amp;mdash;&amp;mdash;SeaJS脚本。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;init.js&lt;/em&gt;&amp;mdash;&amp;mdash;init模块，入口模块，依赖data、jquery、style三个模块。由主页面载入。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;data.js&lt;/em&gt;&amp;mdash;&amp;mdash;data模块，纯json数据模块，由init载入。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;jquery.js&lt;/em&gt;&amp;mdash;&amp;mdash;jquery模块，对 jQuery库的模块化封装，由init载入。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;em&gt;style.css&lt;/em&gt;&amp;mdash;&amp;mdash;CSS样式表，作为style模块由init载入。&lt;/p&gt;&#xD;
&lt;p&gt;sea.js和jquery.js的代码属于库代码，就不赘述，这里只给出自己编写的文件的代码。&lt;/p&gt;&#xD;
&lt;p&gt;html：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;!DOCTYPE HTML&amp;gt;&#xD;
&amp;lt;html lang="zh-CN"&amp;gt;&#xD;
&amp;lt;head&amp;gt;&#xD;
    &amp;lt;meta charset="UTF-8"&amp;gt;&#xD;
    &amp;lt;title&amp;gt;&amp;lt;/title&amp;gt;&#xD;
&amp;lt;/head&amp;gt;&#xD;
&amp;lt;body&amp;gt;&#xD;
&amp;lt;div id="content"&amp;gt;&#xD;
    &amp;lt;p &amp;gt;&amp;lt;/p&amp;gt;&#xD;
    &amp;lt;p &amp;gt;&amp;lt;a href="#"&amp;gt;Blog&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&#xD;
&amp;lt;/div&amp;gt;&amp;lt;&#xD;
&#xD;
&amp;lt;script src="./sea.js" data-main="./init"&amp;gt;&amp;lt;/script&amp;gt;&#xD;
&amp;lt;/body&amp;gt;&#xD;
&amp;lt;/html&amp;gt;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;javascript：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;//init.js&#xD;
define(function(require, exports, module) {&#xD;
    var $ = require('./jquery');&#xD;
    var data = require('./data');&#xD;
    var css = require('./style.css');&#xD;
&#xD;
    $('.author').html(data.author);&#xD;
    $('.blog').attr('href', data.blog);&#xD;
});&#xD;
&#xD;
//data.js&#xD;
define({&#xD;
    author: 'ZhangYang',&#xD;
    blog: 'http://leoo2sk.cnblogs.com'&#xD;
});&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;css：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;.author{color:red;font-size:10pt;}&#xD;
.blog{font-size:10pt;}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;运行效果如下：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201106/201106262318552022.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: inline; padding-top: 0px; border-width: 0px;" title="image" alt="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201106/201106262318565186.png" border="0" height="167" width="217" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;a id="navi-5"&gt;&lt;/a&gt;&#xD;
&lt;p&gt;&lt;strong&gt;主要参考文献&amp;amp;SeaJS学习资源&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;[1] SeaJS主页 &amp;ndash; &lt;a href="http://seajs.com" target="_blank"&gt;http://seajs.com&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;[2] SeaJS的GitHub库（可获取源码） &amp;ndash; &lt;a title="https://github.com/seajs/seajs" href="https://github.com/seajs/seajs" target="_blank"&gt;https://github.com/seajs/seajs&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;[3] SeaJS作者玉伯的博客 - &lt;a title="http://lifesinger.wordpress.com/" href="http://lifesinger.wordpress.com/" target="_blank"&gt;http://lifesinger.wordpress.com/&lt;/a&gt;&lt;/p&gt;&lt;img src="http://www.cnblogs.com/leoo2sk/aggbug/2089164.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/leoo2sk/archive/2011/06/27/write-javascript-with-seajs.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/leoo2sk/archive/2011/04/19/nginx-module-develop-guide.html</id><title type="text">Nginx模块开发入门</title><summary type="text">本文将会重点关注Nginx模块开发入门及基础。目前Nginx的学习资料非常少，而扩展模块开发相关的资料几乎只有《Emiller's Guide To Nginx Module Development》一文，此文十分经典，但是由于Nginx版本的演进，其中少许内容可能有点过时。本文是笔者在研读这篇文章和Nginx源代码的基础上，对自己学习Nginx模块开发的一个总结。本文将通过一个完整的模块开发实例讲解Nginx模块开发的入门内容。</summary><published>2011-04-19T05:03:00Z</published><updated>2011-04-19T05:03:00Z</updated><author><name>T2噬菌体</name><uri>http://www.cnblogs.com/leoo2sk/</uri></author><link rel="alternate" href="http://www.cnblogs.com/leoo2sk/archive/2011/04/19/nginx-module-develop-guide.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/leoo2sk/archive/2011/04/19/nginx-module-develop-guide.html"/><content type="html">&lt;p&gt;&lt;strong&gt;&lt;a name="section0"&gt;&lt;/a&gt;前言&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;Nginx是当前最流行的HTTP Server之一，根据W3Techs的统计，目前世界排名（根据Alexa）前100万的网站中，&lt;a href="http://w3techs.com/technologies/overview/web_server/all" target="_blank"&gt;Nginx的占有率为6.8%&lt;/a&gt;。与Apache相比，&lt;a href="http://www.joeandmotorboat.com/2008/02/28/apache-vs-nginx-web-server-performance-deathmatch/" target="_blank"&gt;Nginx在高并发情况下具有巨大的性能优势&lt;/a&gt;。&lt;/p&gt;&#xD;
&lt;p&gt;Nginx属于典型的微内核设计，其内核非常简洁和优雅，同时具有非常高的可扩展性。Nginx最初仅仅主要被用于做反向代理，后来随着HTTP核心的成熟和各种HTTP扩展模块的丰富，Nginx越来越多被用来取代Apache而单独承担HTTP Server的责任，例如目前淘宝内各个部门正越来越多使用Nginx取代Apache，据笔者了解，在腾讯和新浪等公司也存在类似情况。&lt;/p&gt;&#xD;
&lt;p&gt;同时，大量的第三方扩展模块也令Nginx越来越强大。例如，由淘宝的工程师清无（王晓哲）和春来（章亦春）所开发的&lt;a href="https://github.com/chaoslawful/lua-nginx-module" target="_blank"&gt;nginx_lua_module&lt;/a&gt;可以将Lua语言嵌入到Nginx配置中，从而利用Lua极大增强了Nginx本身的编程能力，甚至可以不用配合其它脚本语言（如PHP或Python等），只靠Nginx本身就可以实现复杂业务的处理。而春来所开发的&lt;a href="https://github.com/agentzh/ngx_openresty" target="_blank"&gt;ngx_openresty&lt;/a&gt;更是通过集成&lt;a href="http://luajit.org/" target="_blank"&gt;LuaJIT&lt;/a&gt;等组件，将Nginx本身变成了一个完全的应用开发平台。目前淘宝数据平台与产品部量子统计的产品都是基于ngx_openresty所开发。对ngxin_lua_module或ngx_openresty感兴趣的朋友可以参考我在关键词上给出的链接，后续我也可能会写一些与其有关的文章。&lt;/p&gt;&#xD;
&lt;p&gt;本文将会重点关注Nginx模块开发入门及基础。目前Nginx的学习资料非常少，而扩展模块开发相关的资料几乎只有《&lt;a href="http://www.evanmiller.org/nginx-modules-guide.html" target="_blank"&gt;Emiller's Guide To Nginx Module Development&lt;/a&gt;》一文，此文十分经典，但是由于Nginx版本的演进，其中少许内容可能有点过时。本文是笔者在研读这篇文章和Nginx源代码的基础上，对自己学习Nginx模块开发的一个总结。本文将通过一个完整的模块开发实例讲解Nginx模块开发的入门内容。&lt;/p&gt;&#xD;
&lt;p&gt;本文将基于Nginx最新的&lt;a href="http://nginx.org/download/nginx-1.0.0.tar.gz" target="_blank"&gt;1.0.0&lt;/a&gt;版本，操作系统环境为Linux（Ubuntu10.10）。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section0"&gt;前言&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section1"&gt;Nginx提要&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section1-1"&gt;Nginx在Linux下的安装与运行&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section1-2"&gt;Nginx配置文件基本结构&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section1-3"&gt;Nginx模块工作原理概述&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section2"&gt;Nginx模块开发实战&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section2-1"&gt;定义模块配置结构&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section2-2"&gt;定义指令&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section2-3"&gt;创建合并配置信息&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section2-4"&gt;编写Handler&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section2-5"&gt;组合Nginx Module&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section3"&gt;Nginx模块的安装&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section4"&gt;Nginx更深入的学习&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="#section5"&gt;Nginx参考文献&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section1"&gt;&lt;/a&gt;Nginx提要&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;开发Nginx扩展当然首要前提是对Nginx有一定的了解，然而本文并不打算详细阐述Nginx的方方面面，诸如Nginx的安装和各种详细配置等内容都可以在Nginx官网的Document中找到，本文在这里只会概括性描述一些后面可能会用到的原理和概念。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section1-1"&gt;&lt;/a&gt;Nginx在Linux下的安装与运行&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;使用Nginx的第一步是下载Nginx源码包，例如1.0.0的下载地址为&lt;a title="http://nginx.org/download/nginx-1.0.0.tar.gz" href="http://nginx.org/download/nginx-1.0.0.tar.gz"&gt;http://nginx.org/download/nginx-1.0.0.tar.gz&lt;/a&gt;。下载完后用tar命令解压缩，进入目录后安装过程与Linux下通常步骤无异，例如我想讲Nginx安装到/usr/local/nginx下，则执行如下命令：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;./configure --prefix=/usr/local/nginx&#xD;
make&#xD;
make install&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;安装完成后可以直接使用下面命令启动Nginx：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;/usr/local/nginx/sbin/nginx&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;Nginx默认以Deamon进程启动，输入下列命令：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;curl -i http://localhost/&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;就可以检测Nginx是否已经成功运行。或者也可以在浏览器中输入&lt;a href="http://localhost/"&gt;http://localhost/&lt;/a&gt;，应该可以看到Nginx的欢迎页面了。启动后如果想停止Nginx可以使用：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;/usr/local/nginx/sbin/nginx -s stop&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p style="font-size: 16px; border: 1px solid #FF6600; padding: 6px; color: #ff0000; background-color: ffeedd;"&gt;个人博客已迁移至&lt;a href="http://www.codinglabs.org"&gt;www.codinglabs.org&lt;/a&gt;，本文全文最新地址为&lt;a href="http://www.codinglabs.org/html/intro-of-nginx-module-development.html"&gt;http://www.codinglabs.org/html/intro-of-nginx-module-development.html&lt;/a&gt;，欢迎访问！！！&lt;/p&gt;&#xD;
&lt;div style="display: none;"&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section1-2"&gt;&lt;/a&gt;Nginx配置文件基本结构&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;配置文件可以看做是Nginx的灵魂，Nginx服务在启动时会读入配置文件，而后续几乎一切动作行为都是按照配置文件中的指令进行的，因此如果将Nginx本身看做一个计算机，那么Nginx的配置文件可以看成是全部的程序指令。&lt;/p&gt;&#xD;
&lt;p&gt;下面是一个Nginx配置文件的实例：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;#user  nobody;&#xD;
worker_processes  8;&#xD;
error_log  logs/error.log;&#xD;
pid        logs/nginx.pid;&#xD;
events {&#xD;
    worker_connections  1024;&#xD;
}&#xD;
http {&#xD;
    include       mime.types;&#xD;
    default_type  application/octet-stream;&#xD;
    sendfile        on;&#xD;
    #tcp_nopush     on;&#xD;
    keepalive_timeout  65;&#xD;
    #gzip  on;&#xD;
    server {&#xD;
        listen       80;&#xD;
        server_name  localhost;&#xD;
        location / {&#xD;
            root   /home/yefeng/www;&#xD;
            index  index.html index.htm;&#xD;
        }&#xD;
        #error_page  404              /404.html;&#xD;
        # redirect server error pages to the static page /50x.html&#xD;
        #&#xD;
        error_page   500 502 503 504  /50x.html;&#xD;
        location = /50x.html {&#xD;
            root   html;&#xD;
        }&#xD;
    }&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;Nginx配置文件是纯文本文件，你可以用任何文本编辑器如vim或emacs打开它，通常它会在nginx安装目录的conf下，如我的nginx安装在/usr/local/nginx，主配置文件默认放在/usr/local/nginx/conf/nginx.conf。&lt;/p&gt;&#xD;
&lt;p&gt;其中&amp;ldquo;#&amp;rdquo;表示此行是注释，由于笔者为了学习扩展开发安装了一个纯净的Nginx，因此配置文件没有经过太多改动。&lt;/p&gt;&#xD;
&lt;p&gt;Nginx的配置文件是以block的形式组织的，一个block通常使用大括号&amp;ldquo;{}&amp;rdquo;表示。block分为几个层级，整个配置文件为main层级，这是最大的层级；在main层级下可以有event、http等层级，而http中又会有server block，server block中可以包含location block。&lt;/p&gt;&#xD;
&lt;p&gt;每个层级可以有自己的指令（Directive），例如worker_processes是一个main层级指令，它指定Nginx服务的Worker进程数量。有的指令只能在一个层级中配置，如worker_processes只能存在于main中，而有的指令可以存在于多个层级，在这种情况下，子block会继承父block的配置，同时如果子block配置了与父block不同的指令，则会覆盖掉父block的配置。指令的格式是&amp;ldquo;指令名 参数1&amp;nbsp; 参数2 &amp;hellip; 参数N;&amp;rdquo;，注意参数间可用任意数量空格分隔，最后要加分号。&lt;/p&gt;&#xD;
&lt;p&gt;在开发Nginx HTTP扩展模块过程中，需要特别注意的是main、server和location三个层级，因为扩展模块通常允许指定新的配置指令在这三个层级中。&lt;/p&gt;&#xD;
&lt;p&gt;最后要提到的是配置文件是可以包含的，如上面配置文件中&amp;ldquo;include mime.types&amp;rdquo;就包含了mine.types这个配置文件，此文件指定了各种HTTP Content-type。&lt;/p&gt;&#xD;
&lt;p&gt;一般来说，一个server block表示一个Host，而里面的一个location则代表一个路由映射规则，这两个block可以说是HTTP配置的核心。&lt;/p&gt;&#xD;
&lt;p&gt;下图是Nginx配置文件通常结构图示。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201104/201104172212311407.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201104/201104172212324656.png" alt="image" width="290" height="346" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;关于Nginx配置的更多内容请参看Nginx官方文档。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section1-3"&gt;&lt;/a&gt;Nginx模块工作原理概述&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;（Nginx本身支持多种模块，如HTTP模块、EVENT模块和MAIL模块，本文只讨论HTTP模块）&lt;/p&gt;&#xD;
&lt;p&gt;Nginx本身做的工作实际很少，当它接到一个HTTP请求时，它仅仅是通过查找配置文件将此次请求映射到一个location block，而此location中所配置的各个指令则会启动不同的模块去完成工作，因此模块可以看做Nginx真正的劳动工作者。通常一个location中的指令会涉及一个handler模块和多个filter模块（当然，多个location可以复用同一个模块）。handler模块负责处理请求，完成响应内容的生成，而filter模块对响应内容进行处理。因此Nginx模块开发分为handler开发和filter开发（本文不考虑load-balancer模块）。下图展示了一次常规请求和响应的过程。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201104/201104171720493059.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201104/201104171720507387.png" alt="image" width="533" height="365" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section2"&gt;&lt;/a&gt;Nginx模块开发实战&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;下面本文展示一个简单的Nginx模块开发全过程，我们开发一个叫echo的handler模块，这个模块功能非常简单，它接收&amp;ldquo;echo&amp;rdquo;指令，指令可指定一个字符串参数，模块会输出这个字符串作为HTTP响应。例如，做如下配置：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;location /echo {&#xD;
    echo "hello nginx";&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;则访问&lt;a href="http://hostname/echo"&gt;http://hostname/echo&lt;/a&gt;时会输出hello nginx。&lt;/p&gt;&#xD;
&lt;p&gt;直观来看，要实现这个功能需要三步：1、读入配置文件中echo指令及其参数；2、进行HTTP包装（添加HTTP头等工作）；3、将结果返回给客户端。下面本文将分部介绍整个模块的开发过程。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section2-1"&gt;&lt;/a&gt;定义模块配置结构&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;首先我们需要一个结构用于存储从配置文件中读进来的相关指令参数，即模块配置信息结构。根据Nginx模块开发规则，这个结构的命名规则为ngx_http_[module-name]_[main|srv|loc]_conf_t。其中main、srv和loc分别用于表示同一模块在三层block中的配置信息。这里我们的echo模块只需要运行在loc层级下，需要存储一个字符串参数，因此我们可以定义如下的模块配置：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;typedef struct {&#xD;
    ngx_str_t  ed;&#xD;
} ngx_http_echo_loc_conf_t;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;其中字段ed用于存储echo指令指定的需要输出的字符串。注意这里ed的类型，在Nginx模块开发中使用ngx_str_t类型表示字符串，这个类型定义在core/ngx_string中：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;typedef struct {&#xD;
    size_t      len;&#xD;
    u_char     *data;&#xD;
} ngx_str_t; &lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;其中两个字段分别表示字符串的长度和数据起始地址。注意在Nginx源代码中对数据类型进行了别称定义，如ngx_int_t为intptr_t的别称，为了保持一致，在开发Nginx模块时也应该使用这些Nginx源码定义的类型而不要使用C原生类型。除了ngx_str_t外，其它三个常用的nginx type分别为：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;typedef intptr_t        ngx_int_t;&#xD;
typedef uintptr_t       ngx_uint_t;&#xD;
typedef intptr_t        ngx_flag_t;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;具体定义请参看core/ngx_config.h。关于intptr_t和uintptr_t请参考C99中的&lt;a href="http://linux.die.net/include/stdint.h" target="_blank"&gt;stdint.h&lt;/a&gt;或&lt;a title="http://linux.die.net/man/3/intptr_t" href="http://linux.die.net/man/3/intptr_t"&gt;http://linux.die.net/man/3/intptr_t&lt;/a&gt;。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section2-2"&gt;&lt;/a&gt;定义指令&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;一个Nginx模块往往接收一至多个指令，echo模块接收一个指令&amp;ldquo;echo&amp;rdquo;。Nginx模块使用一个ngx_command_t数组表示模块所能接收的所有模块，其中每一个元素表示一个条指令。ngx_command_t是ngx_command_s的一个别称（Nginx习惯于使用&amp;ldquo;_s&amp;rdquo;后缀命名结构体，然后typedef一个同名&amp;ldquo;_t&amp;rdquo;后缀名称作为此结构体的类型名），ngx_command_s定义在core/ngx_config_file.h中：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;struct ngx_command_s {&#xD;
    ngx_str_t             name;&#xD;
    ngx_uint_t            type;&#xD;
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);&#xD;
    ngx_uint_t            conf;&#xD;
    ngx_uint_t            offset;&#xD;
    void                 *post;&#xD;
};&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;其中name是词条指令的名称，type使用掩码标志位方式配置指令参数，相关可用type定义在core/ngx_config_file.h中：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;#define NGX_CONF_NOARGS      0x00000001&#xD;
#define NGX_CONF_TAKE1       0x00000002&#xD;
#define NGX_CONF_TAKE2       0x00000004&#xD;
#define NGX_CONF_TAKE3       0x00000008&#xD;
#define NGX_CONF_TAKE4       0x00000010&#xD;
#define NGX_CONF_TAKE5       0x00000020&#xD;
#define NGX_CONF_TAKE6       0x00000040&#xD;
#define NGX_CONF_TAKE7       0x00000080&#xD;
&#xD;
#define NGX_CONF_MAX_ARGS    8&#xD;
&#xD;
#define NGX_CONF_TAKE12      (NGX_CONF_TAKE1|NGX_CONF_TAKE2)&#xD;
#define NGX_CONF_TAKE13      (NGX_CONF_TAKE1|NGX_CONF_TAKE3)&#xD;
&#xD;
#define NGX_CONF_TAKE23      (NGX_CONF_TAKE2|NGX_CONF_TAKE3)&#xD;
&#xD;
#define NGX_CONF_TAKE123     (NGX_CONF_TAKE1|NGX_CONF_TAKE2|NGX_CONF_TAKE3)&#xD;
#define NGX_CONF_TAKE1234    (NGX_CONF_TAKE1|NGX_CONF_TAKE2|NGX_CONF_TAKE3   \&#xD;
                              |NGX_CONF_TAKE4)&#xD;
&#xD;
#define NGX_CONF_ARGS_NUMBER 0x000000ff&#xD;
#define NGX_CONF_BLOCK       0x00000100&#xD;
#define NGX_CONF_FLAG        0x00000200&#xD;
#define NGX_CONF_ANY         0x00000400&#xD;
#define NGX_CONF_1MORE       0x00000800&#xD;
#define NGX_CONF_2MORE       0x00001000&#xD;
#define NGX_CONF_MULTI       0x00002000&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;其中NGX_CONF_NOARGS表示此指令不接受参数，NGX_CON F_TAKE1-7表示精确接收1-7个，NGX_CONF_TAKE12表示接受1或2个参数，NGX_CONF_1MORE表示至少一个参数，NGX_CONF_FLAG表示接受&amp;ldquo;on|off&amp;rdquo;&amp;hellip;&amp;hellip;&lt;/p&gt;&#xD;
&lt;p&gt;set是一个函数指针，用于指定一个参数转化函数，这个函数一般是将配置文件中相关指令的参数转化成需要的格式并存入配置结构体。Nginx预定义了一些转换函数，可以方便我们调用，这些函数定义在core/ngx_conf_file.h中，一般以&amp;ldquo;_slot&amp;rdquo;结尾，例如ngx_conf_set_flag_slot将&amp;ldquo;on或off&amp;rdquo;转换为&amp;ldquo;1或0&amp;rdquo;，再如ngx_conf_set_str_slot将裸字符串转化为ngx_str_t。&lt;/p&gt;&#xD;
&lt;p&gt;conf用于指定Nginx相应配置文件内存其实地址，一般可以通过内置常量指定，如NGX_HTTP_LOC_CONF_OFFSET，offset指定此条指令的参数的偏移量。&lt;/p&gt;&#xD;
&lt;p&gt;下面是echo模块的指令定义：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;static ngx_command_t  ngx_http_echo_commands[] = {&#xD;
    { ngx_string("echo"),&#xD;
      NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,&#xD;
      ngx_http_echo,&#xD;
      NGX_HTTP_LOC_CONF_OFFSET,&#xD;
      offsetof(ngx_http_echo_loc_conf_t, ed),&#xD;
      NULL },&#xD;
&#xD;
      ngx_null_command&#xD;
};&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;指令数组的命名规则为ngx_http_[module-name]_commands，注意数组最后一个元素要是ngx_null_command结束。&lt;/p&gt;&#xD;
&lt;p&gt;参数转化函数的代码为：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;static char *&#xD;
ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)&#xD;
{&#xD;
    ngx_http_core_loc_conf_t  *clcf;&#xD;
    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);&#xD;
    clcf-&amp;gt;handler = ngx_http_echo_handler;&#xD;
    ngx_conf_set_str_slot(cf,cmd,conf);&#xD;
    &#xD;
    return NGX_CONF_OK;&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;div &gt;&lt;span style="font-family: courier new;"&gt;这个函数除了调用ngx_conf_set_str_slot转化echo指令的参数外，还将修改了核心模块配置（也就是这个location的配置），将其handler替换为我们编写的handler：ngx_http_echo_handler。这样就屏蔽了此location的默认handler，使用ngx_http_echo_handler产生HTTP响应。&lt;/span&gt;&lt;/div&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section2-3"&gt;&lt;/a&gt;创建合并配置信息&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;下一步是定义模块Context。&lt;/p&gt;&#xD;
&lt;p&gt;这里首先需要定义一个ngx_http_module_t类型的结构体变量，命名规则为ngx_http_[module-name]_module_ctx，这个结构主要用于定义各个Hook函数。下面是echo模块的context结构：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;static ngx_http_module_t  ngx_http_echo_module_ctx = {&#xD;
    NULL,                                  /* preconfiguration */&#xD;
    NULL,                                  /* postconfiguration */&#xD;
&#xD;
    NULL,                                  /* create main configuration */&#xD;
    NULL,                                  /* init main configuration */&#xD;
&#xD;
    NULL,                                  /* create server configuration */&#xD;
    NULL,                                  /* merge server configuration */&#xD;
&#xD;
    ngx_http_echo_create_loc_conf,         /* create location configration */&#xD;
    ngx_http_echo_merge_loc_conf           /* merge location configration */&#xD;
};&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;可以看到一共有8个Hook注入点，分别会在不同时刻被Nginx调用，由于我们的模块仅仅用于location域，这里将不需要的注入点设为NULL即可。其中create_loc_conf用于初始化一个配置结构体，如为配置结构体分配内存等工作；merge_loc_conf用于将其父block的配置信息合并到此结构体中，也就是实现配置的继承。这两个函数会被Nginx自动调用。注意这里的命名规则：ngx_http_[module-name]_[create|merge]_[main|srv|loc]_conf。&lt;/p&gt;&#xD;
&lt;p&gt;下面是echo模块这个两个函数的代码：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;static void *&#xD;
ngx_http_echo_create_loc_conf(ngx_conf_t *cf)&#xD;
{&#xD;
    ngx_http_echo_loc_conf_t  *conf;&#xD;
&#xD;
    conf = ngx_pcalloc(cf-&amp;gt;pool, sizeof(ngx_http_echo_loc_conf_t));&#xD;
    if (conf == NULL) {&#xD;
        return NGX_CONF_ERROR;&#xD;
    }&#xD;
    conf-&amp;gt;ed.len = 0;&#xD;
    conf-&amp;gt;ed.data = NULL;&#xD;
&#xD;
    return conf;&#xD;
}&#xD;
&#xD;
static char *&#xD;
ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)&#xD;
{&#xD;
    ngx_http_echo_loc_conf_t *prev = parent;&#xD;
    ngx_http_echo_loc_conf_t *conf = child;&#xD;
&#xD;
    ngx_conf_merge_str_value(conf-&amp;gt;ed, prev-&amp;gt;ed, "");&#xD;
&#xD;
    return NGX_CONF_OK;&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;其中ngx_pcalloc用于在Nginx内存池中分配一块空间，是pcalloc的一个包装。使用ngx_pcalloc分配的内存空间不必手工free，Nginx会自行管理，在适当是否释放。&lt;/p&gt;&#xD;
&lt;p&gt;create_loc_conf新建一个ngx_http_echo_loc_conf_t，分配内存，并初始化其中的数据，然后返回这个结构的指针；merge_loc_conf将父block域的配置信息合并到create_loc_conf新建的配置结构体中。&lt;/p&gt;&#xD;
&lt;p&gt;其中ngx_conf_merge_str_value不是一个函数，而是一个宏，其定义在core/ngx_conf_file.h中：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;#define ngx_conf_merge_str_value(conf, prev, default)                        \&#xD;
    if (conf.data == NULL) {                                                 \&#xD;
        if (prev.data) {                                                     \&#xD;
            conf.len = prev.len;                                             \&#xD;
            conf.data = prev.data;                                           \&#xD;
        } else {                                                             \&#xD;
            conf.len = sizeof(default) - 1;                                  \&#xD;
            conf.data = (u_char *) default;                                  \&#xD;
        }                                                                    \&#xD;
    }&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;同时可以看到，core/ngx_conf_file.h还定义了很多merge value的宏用于merge各种数据。它们的行为比较相似：使用prev填充conf，如果prev的数据为空则使用default填充。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section2-4"&gt;&lt;/a&gt;编写Handler&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;下面的工作是编写handler。handler可以说是模块中真正干活的代码，它主要有以下四项职责：&lt;/p&gt;&#xD;
&lt;p&gt;读入模块配置。&lt;/p&gt;&#xD;
&lt;p&gt;处理功能业务。&lt;/p&gt;&#xD;
&lt;p&gt;产生HTTP header。&lt;/p&gt;&#xD;
&lt;p&gt;产生HTTP body。&lt;/p&gt;&#xD;
&lt;p&gt;下面先贴出echo模块的代码，然后通过分析代码的方式介绍如何实现这四步。这一块的代码比较复杂：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;static ngx_int_t&#xD;
ngx_http_echo_handler(ngx_http_request_t *r)&#xD;
{&#xD;
    ngx_int_t rc;&#xD;
    ngx_buf_t *b;&#xD;
    ngx_chain_t out;&#xD;
&#xD;
    ngx_http_echo_loc_conf_t *elcf;&#xD;
    elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module);&#xD;
&#xD;
    if(!(r-&amp;gt;method &amp;amp; (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST)))&#xD;
    {&#xD;
        return NGX_HTTP_NOT_ALLOWED;&#xD;
    }&#xD;
    &#xD;
    r-&amp;gt;headers_out.content_type.len = sizeof("text/html") - 1;&#xD;
    r-&amp;gt;headers_out.content_type.data = (u_char *) "text/html";&#xD;
&#xD;
    r-&amp;gt;headers_out.status = NGX_HTTP_OK;&#xD;
    r-&amp;gt;headers_out.content_length_n = elcf-&amp;gt;ed.len;&#xD;
&#xD;
    if(r-&amp;gt;method == NGX_HTTP_HEAD)&#xD;
    {&#xD;
        rc = ngx_http_send_header(r);&#xD;
        if(rc != NGX_OK)&#xD;
        {&#xD;
            return rc;&#xD;
        }&#xD;
    }&#xD;
&#xD;
    b = ngx_pcalloc(r-&amp;gt;pool, sizeof(ngx_buf_t));&#xD;
    if(b == NULL)&#xD;
    {&#xD;
        ngx_log_error(NGX_LOG_ERR, r-&amp;gt;connection-&amp;gt;log, 0, "Failed to allocate response buffer.");&#xD;
        return NGX_HTTP_INTERNAL_SERVER_ERROR;&#xD;
    }&#xD;
    out.buf = b;&#xD;
    out.next = NULL;&#xD;
&#xD;
    b-&amp;gt;pos = elcf-&amp;gt;ed.data;&#xD;
    b-&amp;gt;last = elcf-&amp;gt;ed.data + (elcf-&amp;gt;ed.len);&#xD;
    b-&amp;gt;memory = 1;&#xD;
    b-&amp;gt;last_buf = 1;&#xD;
    rc = ngx_http_send_header(r);&#xD;
    if(rc != NGX_OK)&#xD;
    {&#xD;
        return rc;&#xD;
    }&#xD;
    return ngx_http_output_filter(r, &amp;amp;out);&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;handler会接收一个ngx_http_request_t指针类型的参数，这个参数指向一个ngx_http_request_t结构体，此结构体存储了这次HTTP请求的一些信息，这个结构定义在http/ngx_http_request.h中：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;struct ngx_http_request_s {&#xD;
    uint32_t                          signature;         /* "HTTP" */&#xD;
&#xD;
    ngx_connection_t                 *connection;&#xD;
&#xD;
    void                            **ctx;&#xD;
    void                            **main_conf;&#xD;
    void                            **srv_conf;&#xD;
    void                            **loc_conf;&#xD;
&#xD;
    ngx_http_event_handler_pt         read_event_handler;&#xD;
    ngx_http_event_handler_pt         write_event_handler;&#xD;
&#xD;
#if (NGX_HTTP_CACHE)&#xD;
    ngx_http_cache_t                 *cache;&#xD;
#endif&#xD;
&#xD;
    ngx_http_upstream_t              *upstream;&#xD;
    ngx_array_t                      *upstream_states;&#xD;
                                         /* of ngx_http_upstream_state_t */&#xD;
&#xD;
    ngx_pool_t                       *pool;&#xD;
    ngx_buf_t                        *header_in;&#xD;
&#xD;
    ngx_http_headers_in_t             headers_in;&#xD;
    ngx_http_headers_out_t            headers_out;&#xD;
&#xD;
    ngx_http_request_body_t          *request_body;&#xD;
&#xD;
    time_t                            lingering_time;&#xD;
    time_t                            start_sec;&#xD;
    ngx_msec_t                        start_msec;&#xD;
&#xD;
    ngx_uint_t                        method;&#xD;
    ngx_uint_t                        http_version;&#xD;
&#xD;
    ngx_str_t                         request_line;&#xD;
    ngx_str_t                         uri;&#xD;
    ngx_str_t                         args;&#xD;
    ngx_str_t                         exten;&#xD;
    ngx_str_t                         unparsed_uri;&#xD;
&#xD;
    ngx_str_t                         method_name;&#xD;
    ngx_str_t                         http_protocol;&#xD;
&#xD;
    ngx_chain_t                      *out;&#xD;
    ngx_http_request_t               *main;&#xD;
    ngx_http_request_t               *parent;&#xD;
    ngx_http_postponed_request_t     *postponed;&#xD;
    ngx_http_post_subrequest_t       *post_subrequest;&#xD;
    ngx_http_posted_request_t        *posted_requests;&#xD;
&#xD;
    ngx_http_virtual_names_t         *virtual_names;&#xD;
&#xD;
    ngx_int_t                         phase_handler;&#xD;
    ngx_http_handler_pt               content_handler;&#xD;
    ngx_uint_t                        access_code;&#xD;
&#xD;
    ngx_http_variable_value_t        *variables;&#xD;
    &#xD;
    /* ... */&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;由于ngx_http_request_s定义比较长，这里我只截取了一部分。可以看到里面有诸如uri，args和request_body等HTTP常用信息。这里需要特别注意的几个字段是headers_in、headers_out和chain，它们分别表示request header、response header和输出数据缓冲区链表（缓冲区链表是Nginx I/O中的重要内容，后面会单独介绍）。&lt;/p&gt;&#xD;
&lt;p&gt;第一步是获取模块配置信息，这一块只要简单使用ngx_http_get_module_loc_conf就可以了。&lt;/p&gt;&#xD;
&lt;p&gt;第二步是功能逻辑，因为echo模块非常简单，只是简单输出一个字符串，所以这里没有功能逻辑代码。&lt;/p&gt;&#xD;
&lt;p&gt;第三步是设置response header。Header内容可以通过填充headers_out实现，我们这里只设置了Content-type和Content-length等基本内容，ngx_http_headers_out_t定义了所有可以设置的HTTP Response Header信息：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;typedef struct {&#xD;
    ngx_list_t                        headers;&#xD;
&#xD;
    ngx_uint_t                        status;&#xD;
    ngx_str_t                         status_line;&#xD;
&#xD;
    ngx_table_elt_t                  *server;&#xD;
    ngx_table_elt_t                  *date;&#xD;
    ngx_table_elt_t                  *content_length;&#xD;
    ngx_table_elt_t                  *content_encoding;&#xD;
    ngx_table_elt_t                  *location;&#xD;
    ngx_table_elt_t                  *refresh;&#xD;
    ngx_table_elt_t                  *last_modified;&#xD;
    ngx_table_elt_t                  *content_range;&#xD;
    ngx_table_elt_t                  *accept_ranges;&#xD;
    ngx_table_elt_t                  *www_authenticate;&#xD;
    ngx_table_elt_t                  *expires;&#xD;
    ngx_table_elt_t                  *etag;&#xD;
&#xD;
    ngx_str_t                        *override_charset;&#xD;
&#xD;
    size_t                            content_type_len;&#xD;
    ngx_str_t                         content_type;&#xD;
    ngx_str_t                         charset;&#xD;
    u_char                           *content_type_lowcase;&#xD;
    ngx_uint_t                        content_type_hash;&#xD;
&#xD;
    ngx_array_t                       cache_control;&#xD;
&#xD;
    off_t                             content_length_n;&#xD;
    time_t                            date_time;&#xD;
    time_t                            last_modified_time;&#xD;
} ngx_http_headers_out_t;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这里并不包含所有HTTP头信息，如果需要可以使用agentzh（春来）开发的Nginx模块&lt;a href="http://wiki.nginx.org/HttpHeadersMoreModule" target="_blank"&gt;HttpHeadersMore&lt;/a&gt;在指令中指定更多的Header头信息。&lt;/p&gt;&#xD;
&lt;p&gt;设置好头信息后使用ngx_http_send_header就可以将头信息输出，ngx_http_send_header接受一个ngx_http_request_t类型的参数。&lt;/p&gt;&#xD;
&lt;p&gt;第四步也是最重要的一步是输出Response body。这里首先要了解Nginx的I/O机制，Nginx允许handler一次产生一组输出，可以产生多次，Nginx将输出组织成一个单链表结构，链表中的每个节点是一个chain_t，定义在core/ngx_buf.h：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;struct ngx_chain_s {&#xD;
    ngx_buf_t    *buf;&#xD;
    ngx_chain_t  *next;&#xD;
};&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;其中ngx_chain_t是ngx_chain_s的别名，buf为某个数据缓冲区的指针，next指向下一个链表节点，可以看到这是一个非常简单的链表。ngx_buf_t的定义比较长而且很复杂，这里就不贴出来了，请自行参考core/ngx_buf.h。ngx_but_t中比较重要的是pos和last，分别表示要缓冲区数据在内存中的起始地址和结尾地址，这里我们将配置中字符串传进去，last_buf是一个位域，设为1表示此缓冲区是链表中最后一个元素，为0表示后面还有元素。因为我们只有一组数据，所以缓冲区链表中只有一个节点，如果需要输入多组数据可将各组数据放入不同缓冲区后插入到链表。下图展示了Nginx缓冲链表的结构：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201104/201104191155017285.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201104/201104191155025791.png" alt="image" width="488" height="331" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;缓冲数据准备好后，用ngx_http_output_filter就可以输出了（会送到filter进行各种过滤处理）。ngx_http_output_filter的第一个参数为ngx_http_request_t结构，第二个为输出链表的起始地址&amp;amp;out。ngx_http_out_put_filter会遍历链表，输出所有数据。&lt;/p&gt;&#xD;
&lt;p&gt;以上就是handler的所有工作，请对照描述理解上面贴出的handler代码。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;组合Nginx Module&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;上面完成了Nginx模块各种组件的开发下面就是将这些组合起来了。一个Nginx模块被定义为一个ngx_module_t结构，这个结构的字段很多，不过开头和结尾若干字段一般可以通过Nginx内置的宏去填充，下面是我们echo模块的模块主体定义：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;ngx_module_t  ngx_http_echo_module = {&#xD;
    NGX_MODULE_V1,&#xD;
    &amp;amp;ngx_http_echo_module_ctx,             /* module context */&#xD;
    ngx_http_echo_commands,                /* module directives */&#xD;
    NGX_HTTP_MODULE,                       /* module type */&#xD;
    NULL,                                  /* init master */&#xD;
    NULL,                                  /* init module */&#xD;
    NULL,                                  /* init process */&#xD;
    NULL,                                  /* init thread */&#xD;
    NULL,                                  /* exit thread */&#xD;
    NULL,                                  /* exit process */&#xD;
    NULL,                                  /* exit master */&#xD;
    NGX_MODULE_V1_PADDING&#xD;
};&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;开头和结尾分别用NGX_MODULE_V1和NGX_MODULE_V1_PADDING 填充了若干字段，就不去深究了。这里主要需要填入的信息从上到下以依次为context、指令数组、模块类型以及若干特定事件的回调处理函数（不需要可以置为NULL），其中内容还是比较好理解的，注意我们的echo是一个HTTP模块，所以这里类型是NGX_HTTP_MODULE，其它可用类型还有NGX_EVENT_MODULE（事件处理模块）和NGX_MAIL_MODULE（邮件模块）。&lt;/p&gt;&#xD;
&lt;p&gt;这样，整个echo模块就写好了，下面给出echo模块的完整代码：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;/*&#xD;
 * Copyright (C) Eric Zhang&#xD;
 */&#xD;
&#xD;
#include &amp;lt;ngx_config.h&amp;gt;&#xD;
#include &amp;lt;ngx_core.h&amp;gt;&#xD;
#include &amp;lt;ngx_http.h&amp;gt;&#xD;
&#xD;
/* Module config */&#xD;
typedef struct {&#xD;
    ngx_str_t  ed;&#xD;
} ngx_http_echo_loc_conf_t;&#xD;
&#xD;
static char *ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);&#xD;
static void *ngx_http_echo_create_loc_conf(ngx_conf_t *cf);&#xD;
static char *ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child);&#xD;
&#xD;
/* Directives */&#xD;
static ngx_command_t  ngx_http_echo_commands[] = {&#xD;
    { ngx_string("echo"),&#xD;
      NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,&#xD;
      ngx_http_echo,&#xD;
      NGX_HTTP_LOC_CONF_OFFSET,&#xD;
      offsetof(ngx_http_echo_loc_conf_t, ed),&#xD;
      NULL },&#xD;
&#xD;
      ngx_null_command&#xD;
};&#xD;
&#xD;
/* Http context of the module */&#xD;
static ngx_http_module_t  ngx_http_echo_module_ctx = {&#xD;
    NULL,                                  /* preconfiguration */&#xD;
    NULL,                                  /* postconfiguration */&#xD;
&#xD;
    NULL,                                  /* create main configuration */&#xD;
    NULL,                                  /* init main configuration */&#xD;
&#xD;
    NULL,                                  /* create server configuration */&#xD;
    NULL,                                  /* merge server configuration */&#xD;
&#xD;
    ngx_http_echo_create_loc_conf,         /* create location configration */&#xD;
    ngx_http_echo_merge_loc_conf           /* merge location configration */&#xD;
};&#xD;
&#xD;
/* Module */&#xD;
ngx_module_t  ngx_http_echo_module = {&#xD;
    NGX_MODULE_V1,&#xD;
    &amp;amp;ngx_http_echo_module_ctx,             /* module context */&#xD;
    ngx_http_echo_commands,                /* module directives */&#xD;
    NGX_HTTP_MODULE,                       /* module type */&#xD;
    NULL,                                  /* init master */&#xD;
    NULL,                                  /* init module */&#xD;
    NULL,                                  /* init process */&#xD;
    NULL,                                  /* init thread */&#xD;
    NULL,                                  /* exit thread */&#xD;
    NULL,                                  /* exit process */&#xD;
    NULL,                                  /* exit master */&#xD;
    NGX_MODULE_V1_PADDING&#xD;
};&#xD;
&#xD;
/* Handler function */&#xD;
static ngx_int_t&#xD;
ngx_http_echo_handler(ngx_http_request_t *r)&#xD;
{&#xD;
    ngx_int_t rc;&#xD;
    ngx_buf_t *b;&#xD;
    ngx_chain_t out;&#xD;
&#xD;
    ngx_http_echo_loc_conf_t *elcf;&#xD;
    elcf = ngx_http_get_module_loc_conf(r, ngx_http_echo_module);&#xD;
&#xD;
    if(!(r-&amp;gt;method &amp;amp; (NGX_HTTP_HEAD|NGX_HTTP_GET|NGX_HTTP_POST)))&#xD;
    {&#xD;
        return NGX_HTTP_NOT_ALLOWED;&#xD;
    }&#xD;
    &#xD;
    r-&amp;gt;headers_out.content_type.len = sizeof("text/html") - 1;&#xD;
    r-&amp;gt;headers_out.content_type.data = (u_char *) "text/html";&#xD;
&#xD;
    r-&amp;gt;headers_out.status = NGX_HTTP_OK;&#xD;
    r-&amp;gt;headers_out.content_length_n = elcf-&amp;gt;ed.len;&#xD;
&#xD;
    if(r-&amp;gt;method == NGX_HTTP_HEAD)&#xD;
    {&#xD;
        rc = ngx_http_send_header(r);&#xD;
        if(rc != NGX_OK)&#xD;
        {&#xD;
            return rc;&#xD;
        }&#xD;
    }&#xD;
&#xD;
    b = ngx_pcalloc(r-&amp;gt;pool, sizeof(ngx_buf_t));&#xD;
    if(b == NULL)&#xD;
    {&#xD;
        ngx_log_error(NGX_LOG_ERR, r-&amp;gt;connection-&amp;gt;log, 0, "Failed to allocate response buffer.");&#xD;
        return NGX_HTTP_INTERNAL_SERVER_ERROR;&#xD;
    }&#xD;
    out.buf = b;&#xD;
    out.next = NULL;&#xD;
&#xD;
    b-&amp;gt;pos = elcf-&amp;gt;ed.data;&#xD;
    b-&amp;gt;last = elcf-&amp;gt;ed.data + (elcf-&amp;gt;ed.len);&#xD;
    b-&amp;gt;memory = 1;&#xD;
    b-&amp;gt;last_buf = 1;&#xD;
    rc = ngx_http_send_header(r);&#xD;
    if(rc != NGX_OK)&#xD;
    {&#xD;
        return rc;&#xD;
    }&#xD;
    return ngx_http_output_filter(r, &amp;amp;out);&#xD;
}&#xD;
&#xD;
static char *&#xD;
ngx_http_echo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)&#xD;
{&#xD;
    ngx_http_core_loc_conf_t  *clcf;&#xD;
    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);&#xD;
    clcf-&amp;gt;handler = ngx_http_echo_handler;&#xD;
    ngx_conf_set_str_slot(cf,cmd,conf);&#xD;
    &#xD;
    return NGX_CONF_OK;&#xD;
}&#xD;
&#xD;
static void *&#xD;
ngx_http_echo_create_loc_conf(ngx_conf_t *cf)&#xD;
{&#xD;
    ngx_http_echo_loc_conf_t  *conf;&#xD;
&#xD;
    conf = ngx_pcalloc(cf-&amp;gt;pool, sizeof(ngx_http_echo_loc_conf_t));&#xD;
    if (conf == NULL) {&#xD;
        return NGX_CONF_ERROR;&#xD;
    }&#xD;
    conf-&amp;gt;ed.len = 0;&#xD;
    conf-&amp;gt;ed.data = NULL;&#xD;
&#xD;
    return conf;&#xD;
}&#xD;
&#xD;
static char *&#xD;
ngx_http_echo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)&#xD;
{&#xD;
    ngx_http_echo_loc_conf_t *prev = parent;&#xD;
    ngx_http_echo_loc_conf_t *conf = child;&#xD;
&#xD;
    ngx_conf_merge_str_value(conf-&amp;gt;ed, prev-&amp;gt;ed, "");&#xD;
&#xD;
    return NGX_CONF_OK;&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section3"&gt;&lt;/a&gt;Nginx模块的安装&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;Nginx不支持动态链接模块，所以安装模块需要将模块代码与Nginx源代码进行重新编译。安装模块的步骤如下：&lt;/p&gt;&#xD;
&lt;p&gt;1、编写模块config文件，这个文件需要放在和模块源代码文件放在同一目录下。文件内容如下：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;ngx_addon_name=模块完整名称&#xD;
HTTP_MODULES="$HTTP_MODULES 模块完整名称"&#xD;
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/源代码文件名"&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;2、进入Nginx源代码，使用下面命令编译安装&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;./configure --prefix=安装目录 --add-module=模块源代码文件目录&#xD;
make&#xD;
make install&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这样就完成安装了，例如，我的源代码文件放在/home/yefeng/ngxdev/ngx_http_echo下，我的config文件为：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;ngx_addon_name=ngx_http_echo_module&#xD;
HTTP_MODULES="$HTTP_MODULES ngx_http_echo_module"&#xD;
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_echo_module.c"&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;编译安装命令为：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;./configure --prefix=/usr/local/nginx --add-module=/home/yefeng/ngxdev/ngx_http_echo&#xD;
make&#xD;
sudo make install&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这样echo模块就被安装在我的Nginx上了，下面测试一下，修改配置文件，增加以下一项配置：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;location /echo {&#xD;
    echo "This is my first nginx module!!!";&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;然后用curl测试一下：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;curl -i http://localhost/echo&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;结果如下：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201104/201104191300402587.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201104/201104191300415836.png" alt="image" width="561" height="169" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;可以看到模块已经正常工作了，也可以在浏览器中打开网址，就可以看到结果：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201104/201104191300421833.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201104/201104191300437830.png" alt="image" width="443" height="165" border="0" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section4"&gt;&lt;/a&gt;更深入的学习&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;本文只是简要介绍了Nginx模块的开发过程，由于篇幅的原因，不能面面俱到。因为目前Nginx的学习资料很少，如果读者希望更深入学习Nginx的原理及模块开发，那么阅读源代码是最好的办法。在Nginx源代码的core/下放有Nginx的核心代码，对理解Nginx的内部工作机制非常有帮助，http/目录下有Nginx HTTP相关的实现，http/module下放有大量内置http模块，可供读者学习模块的开发，另外在&lt;a title="http://wiki.nginx.org/3rdPartyModules" href="http://wiki.nginx.org/3rdPartyModules"&gt;http://wiki.nginx.org/3rdPartyModules&lt;/a&gt;上有大量优秀的第三方模块，也是非常好的学习资料。&lt;/p&gt;&#xD;
&lt;p&gt;如有意见建议或疑问欢迎发送邮件至&lt;a href="mailto:ericzhang.buaa@gmail.com"&gt;ericzhang.buaa@gmail.com&lt;/a&gt;。希望本文对您有所帮助！！！&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;&lt;a name="section5"&gt;&lt;/a&gt;参考文献&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;[1] Evan Miller, Emiller's Guide To Nginx Module Development. &lt;a title="http://www.evanmiller.org/nginx-modules-guide.html" href="http://www.evanmiller.org/nginx-modules-guide.html"&gt;http://www.evanmiller.org/nginx-modules-guide.html&lt;/a&gt;, 2009&lt;/p&gt;&#xD;
&lt;p&gt;[2] &lt;a title="http://wiki.nginx.org/Configuration" href="http://wiki.nginx.org/Configuration"&gt;http://wiki.nginx.org/Configuration&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;[3] Cl&amp;eacute;ment Nedelcu, Nginx Http Server. Packt Publishing, 2010&lt;/p&gt;&#xD;
&lt;/div&gt;&lt;img src="http://www.cnblogs.com/leoo2sk/aggbug/2018920.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/leoo2sk/archive/2011/04/19/nginx-module-develop-guide.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/leoo2sk/archive/2011/03/03/computational-reuse.html</id><title type="text">程序设计中的计算复用(Computational Reuse)</title><summary type="text">本文简要论述了计算复用（Computational Reuse）的基本概念及作用，通过计算斐波那契数列和Strassen算法两个例子展示了计算复用，然后，本文论述了计算复用思想对非数值计算程序开发的启示。</summary><published>2011-03-03T10:25:00Z</published><updated>2011-03-03T10:25:00Z</updated><author><name>T2噬菌体</name><uri>http://www.cnblogs.com/leoo2sk/</uri></author><link rel="alternate" href="http://www.cnblogs.com/leoo2sk/archive/2011/03/03/computational-reuse.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/leoo2sk/archive/2011/03/03/computational-reuse.html"/><content type="html">&lt;p&gt;&lt;strong&gt;从斐波那契数列说起&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;我想几乎每一个程序员对&lt;a href="http://en.wikipedia.org/wiki/Fibonacci_number" target="_blank"&gt;斐波那契（Fibonacci）数列&lt;/a&gt;都不会陌生，在很多教科书或文章中涉及到递归或计算复杂性的地方都会将计算斐波那契数列的程序作为经典示例。如果现在让你以最快的速度用C#写出一个计算斐波那契数列第n个数的函数（不考虑参数小于1或结果溢出等异常情况），我不知你的程序是否会和下列代码类似：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;public static ulong Fib(ulong n)&#xD;
{&#xD;
    return (n == 1 || n == 2) ? 1 : Fib(n - 1) + Fib(n - 2);&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这段代码应该算是短小精悍（执行代码只有一行），直观清晰，而且非常符合许多程序员的代码美学，许多人在面试时写出这样的代码可能心里还会暗爽。但是如果用这段代码试试计算Fib(100)我想就再也爽不起来了，估计下星期甚至下个月前结果很难算得出来。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;span style="font-size: 16px;"&gt;&lt;strong&gt;&lt;span size="3"&gt;&lt;span size="3"&gt;看来好看的代码未必中用，如果程序在效率不能接受那美观神马的就都是浮云了。&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/span&gt;如果简单分析一下程序的执行流，就会发现问题在哪，以计算Fibonacci(5)为例：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201103/201103031726514848.png"&gt;&lt;img style="background-image: none; padding-left: 0px; padding-right: 0px; display: block; float: none; margin-left: auto; margin-right: auto; padding-top: 0px; border-width: 0px;" title="image" border="0" alt="image" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201103/201103031726515371.png" width="437" height="267" /&gt;&lt;/a&gt;&lt;/p&gt;&#xD;
&lt;p&gt;从上图可以看出，在计算Fib(5)的过程中，Fib(1)计算了两次、Fib(2)计算了3次，Fib(3)计算了两次，本来只需要5次计算就可以完成的任务却计算了9次。这个问题随着规模的增加会愈发凸显，以至于Fib(100)已经无法再可接受的时间内算出。虽然可以通过&lt;a href="http://en.wikipedia.org/wiki/Tail_call" target="_blank"&gt;尾递归优化&lt;/a&gt;将双递归变为单递归，但是效果也并不理想。&lt;/p&gt;&#xD;
&lt;p&gt;这是一个非常典型的忽视&amp;ldquo;计算复用&amp;rdquo;的例子。&lt;span style="font-size: 16px;"&gt;&lt;strong&gt;&lt;span size="3"&gt;&lt;span size="3"&gt;计算复用的目标在于保证计算过程中同一计算子过程只进行一次，通过保存子过程计算结果并复用来提高计算效率。&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/span&gt;其实类似上面的代码出现在很多教科书中，如果是为了展示斐波那契数列的数学特性当然无可厚非，但是作为计算机程序就很有问题了。因为&lt;span style="font-size: 16px;"&gt;&lt;strong&gt;&lt;span size="3"&gt;&lt;span size="3"&gt;数学和计算科学是有区别的，数学要求严谨和简洁的表达，而计算科学则需要尽量快的得出结果，好的数学公式未必是好的计算公式。&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/span&gt;这也说明程序设计不是简单的将数学语言翻译为计算机语言就可以了，程序员应该能将数学语言首先翻译成计算科学语言（算法？），然后再翻译成机器语言。因此程序员的工作绝不是机械的，而是要有一定的创造性，所以必要的算法知识对程序员至关重要，因为算法教会程序员如何用最有效率的方式去编写程序。&lt;/p&gt;&#xD;
&lt;p&gt;言归正传，根据以上分析，可以写出一个更高效的斐波那契数列计算程序：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;public static ulong Fib(ulong n)&#xD;
{&#xD;
    if (n == 1 || n == 2)&#xD;
    {&#xD;
        return 1;&#xD;
    }&#xD;
    ulong m1 = 1, m2 = 1;&#xD;
    for (ulong i = 3; i &amp;lt;= n; i++)&#xD;
    {&#xD;
        m2 = m1 + m2;&#xD;
        m1 = m2 - m1;&#xD;
    }&#xD;
&#xD;
    return m2;&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这段代码可能看起来不如上一段那么优美，但是其效率却是第一段代码不可比拟的。例如计算Fib(40)，在我的机器上，第一段代码用时3.5秒，而第二段代码小于0.001秒。这个差距随着规模增大会更明显，例如Fib(100)，第一段代码可能需要几天甚至几周，而第二段代码耗时仍然小于0.001秒。&lt;span style="font-size: 16px;"&gt;&lt;strong&gt;&lt;span size="3"&gt;&lt;span size="3"&gt;天壤之别！&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&#xD;
&lt;p&gt;如果从计算复杂性的角度分析，第一段代码的复杂度为O(1.6^n)，对数学敏感的朋友应该能体会到这个函数可怕的增长速度，这甚至不是一个多项式级别的复杂度，而第二段代码仅为O(n)。看到如此简单一个例子出现如此差别，还能说程序员学习算法没有用吗。&lt;/p&gt;&#xD;
&lt;p&gt;上面代码对于&amp;ldquo;计算复用&amp;rdquo;的思想体现不是很明显，因为我们仅仅需要一个结果，中间结果都被丢弃了，如果是计算1&amp;lt;=i&amp;lt;=n的所有Fib(i)，那么计算复用的思想就会体现的比较明显。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;矩阵乘法与Strassen算法&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;下面说一个将计算复用发挥到极致的例子，说实话直到现在每次看到&lt;a href="http://en.wikipedia.org/wiki/Strassen_algorithm" target="_blank"&gt;Strassen算法&lt;/a&gt;我都觉得震撼，不知Strassen当年是长了何等天才的脑子才发现这么漂亮的一个算法。&lt;/p&gt;&#xD;
&lt;p&gt;矩阵计算在许多领域如机器学习、图形图像处理、模式识别中均占有重要地位。而计算两个n*n矩阵乘积的运算是矩阵计算中常见的计算。由矩阵理论可知，普通方法计算两个n阶方阵的乘积需要进行n^3次乘法计算，其计算复杂度自然是O(n^3)。但是德国数学家&lt;a href="http://en.wikipedia.org/wiki/Volker_Strassen" target="_blank"&gt;Volker Strassen&lt;/a&gt;通过拆分矩阵并复用计算结果，发现了一种复杂度为O(n^2.81)的算法，这个算法简单说来如下。&lt;/p&gt;&#xD;
&lt;p&gt;假设n为2的幂（不为2的幂也能计算，这里是为了方便说明），A和B是两个n阶方阵，则A和B分别可以分解成4个n/2阶方阵：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="display: block; float: none; margin-left: auto; margin-right: auto;" src="http://latex.codecogs.com/gif.latex?A=\begin{pmatrix}%20a%20&amp;amp;%20b\\%20c%20&amp;amp;%20d%20\end{pmatrix},%20B=\begin{pmatrix}%20e%20&amp;amp;%20f\\%20g%20&amp;amp;%20h%20\end{pmatrix},%20AB=\begin{pmatrix}%20r%20&amp;amp;%20s\\%20t%20&amp;amp;%20u%20\end{pmatrix}" /&gt;&lt;/p&gt;&#xD;
&lt;p&gt;则：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="display: block; float: none; margin-left: auto; margin-right: auto;" src="http://latex.codecogs.com/gif.latex?r=ae+bg,s=af+bh,t=ce+dg,u=cf+dh" /&gt;&lt;/p&gt;&#xD;
&lt;p&gt;可惜这样经过8次n/2阶方阵相乘，复杂度还是O(n^3)，没有降低复杂度。天才的Volker Strassen发现了一种通过计算7次n/2阶方阵来得出n阶方阵乘积的方法。具体来说，假设每个矩阵的积可以写成如下形式：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img src="http://latex.codecogs.com/gif.latex?P_i=A_iB_i=(\alpha_{i1}a+\alpha_{i2}b+\alpha_{i3}c+\alpha_{i4}d)(\beta_{i1}e+\beta_{i2}f+\beta_{i3}g+\beta_{i4}h)" width="512" height="19" style="display: block; margin-left: auto; margin-right: auto;" /&gt;&lt;/p&gt;&#xD;
&lt;p&gt;然后设：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img height="151" width="382" style="display: block; float: none; margin-left: auto; margin-right: auto;" src="http://latex.codecogs.com/gif.latex?\\%20P_1=A_1B_1=a(f-h)=af-ah\\%20P_2=A_2B_2=(a+b)h=ah+bh\\%20P_3=A_3B_3=(c+d)e=ce+de\\%20P_4=A_4B_4=d(g-e)=dg-de\\%20P_5=A_5B_5=(a+d)(e+h)=ae+ah+de+dh\\%20P_6=A_6B_6=(b-d)(g+h)=bg+bh-dg-dh\\%20P_7=A_7B_7=(a-c)(e+g)=ae+af-ce-cf" /&gt;&lt;/p&gt;&#xD;
&lt;p&gt;这样通过7次n/2矩阵的相乘计算出P1-P7，然后：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;img style="display: block; float: none; margin-left: auto; margin-right: auto;" src="http://latex.codecogs.com/gif.latex?\\%20s=P_1+P_2\\%20t=P_3+P_4\\%20r=P_4+P_5-P_2+P_6\\%20u=P_1+P_5-P_3-P_7" width="176" height="82" /&gt;&lt;/p&gt;&#xD;
&lt;p&gt;这样就组合出了AB，这个方法的复杂度为O(n^2.81)，这个算法实在是太漂亮了。天才！绝对的天才啊！对于这种人除了无限崇敬我真是没有其它想法了，能将计算复用发挥到如此境地，不知世间能有几人。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;计算复用对软件开发的启示&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;也许有的朋友会说，&amp;ldquo;我又不开发数值计算型程序，也不会接触如此复杂的算法，计算复用与我何干？&amp;rdquo;。实际上即使开发非数值型程序，计算复用的思想也是大有用途的。例如我曾经在一个真实的PHP开发的行业系统中见过类似这样的代码：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;foreach($items as $k =&amp;gt; $v){&#xD;
    //...&#xD;
    $money = $v-&amp;gt;money + getTax();&#xD;
    //...&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;当时我问开发这个程序的人这里getTax的返回值和每个item有关系吗，他说税费是一套复杂的算法算出来的，但是其值固定的。那这里可就太浪费了，每次循环都计算一次，如果改为如下：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;$tax = getTax();&#xD;
foreach($items as $k =&amp;gt; $v){&#xD;
    //...&#xD;
    $money = $v-&amp;gt;money + $tax;&#xD;
    //...&#xD;
}&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;则可以节省不少计算资源。在后来的沟通中发现这个问题原来是重构的遗留问题，以前系统中的税率计算是写在程序里的，后来发现这个计算越来越多，就使用&amp;ldquo;Extract Method&amp;rdquo;重构模式提取成了getTax函数，但是这样的后果就是到处都是getTax调用，有的程序段甚至调用七八次，但是如果应用计算复用的思想，则应该在脚本开始只计算一次税费并保存，后面全都使用这个变量而不是每次调用getTax。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;span style="font-size: 16px;"&gt;&lt;strong&gt;&lt;span size="3"&gt;&lt;span size="3"&gt;总之，只要某个计算结果与执行上下文无关，并且在一个执行流中超过一次被使用，则应该使用计算复用。&lt;/span&gt;&lt;/span&gt;&lt;/strong&gt;&lt;/span&gt;&lt;/p&gt;&#xD;
&lt;p&gt;这个例子还算明显的，有时可能不会这么明显，例如我们知道JavaScript中从深层函数中引用全局对象的代价是很高的，因为需要遍历作用域链（当然是隐式的），因此在JS中如果深层函数代码频繁使用全局对象，则要付出很高的代价。如果程序员不懂得对象及作用域链相关知识，则不会发现这种潜在的效率问题，而正确的做法是使用一个局部变量保存对全局对象的引用而不是每次都直接使用全局变量。&lt;/p&gt;&#xD;
&lt;p&gt;很多成熟的产品也处处体现着计算复用的思想，如在PHP中，下面代码可以得到一个数组的元素个数：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;echo count($arr);&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;如果我们来实现，最自然的想法就是遍历数组。但是PHP的开发者明显更聪明，他们在建立数组时同时建立一个与之关联的内部的数量计数变量（对PHP程序员透明），随着数组元素的增减，这个变量也相应增减，每次调用count函数直接返回这个变量即可，这就将count的复杂度从O(n)降为O(1)，这也是计算复用的一个典型应用。&lt;/p&gt;&#xD;
&lt;p&gt;另外，其实计算复用和缓存的概念是相通的，很多缓存系统就使用了计算复用的思想。&lt;/p&gt;&lt;img src="http://www.cnblogs.com/leoo2sk/aggbug/1970147.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/leoo2sk/archive/2011/03/03/computational-reuse.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry><entry><id>http://www.cnblogs.com/leoo2sk/archive/2011/02/27/php-gc.html</id><title type="text">浅谈PHP5中垃圾回收算法(Garbage Collection)的演化</title><summary type="text">PHP是一门托管型语言，在PHP编程中程序员不需要手工处理内存资源的分配与释放（使用C编写PHP或Zend扩展除外），这就意味着PHP本身实现了垃圾回收机制（Garbage Collection）。现在如果去PHP官方网站（php.net）可以看到，目前PHP5的两个分支版本PHP5.2和PHP5.3是分别更新的，这是因为许多项目仍然使用5.2版本的PHP，而5.3版本对5.2并不是完全兼容。PHP5.3在PHP5.2的基础上做了诸多改进，其中垃圾回收算法就属于一个比较大的改变。本文将分别讨论PHP5.2和PHP5.3的垃圾回收机制，并讨论这种演化和改进对于程序员编写PHP的影响以及要注意的问题。</summary><published>2011-02-26T16:10:00Z</published><updated>2011-02-26T16:10:00Z</updated><author><name>T2噬菌体</name><uri>http://www.cnblogs.com/leoo2sk/</uri></author><link rel="alternate" href="http://www.cnblogs.com/leoo2sk/archive/2011/02/27/php-gc.html"/><link rel="alternate" type="text/html" href="http://www.cnblogs.com/leoo2sk/archive/2011/02/27/php-gc.html"/><content type="html">&lt;p&gt;&lt;strong&gt;前言&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;PHP是一门托管型语言，在PHP编程中程序员不需要手工处理内存资源的分配与释放（使用C编写PHP或Zend扩展除外），这就意味着PHP本身实现了垃圾回收机制（Garbage Collection）。现在如果去PHP官方网站（&lt;a target="_blank" href="http://php.net"&gt;php.net&lt;/a&gt;）可以看到，目前PHP5的两个分支版本PHP5.2和PHP5.3是分别更新的，这是因为许多项目仍然使用5.2版本的PHP，而5.3版本对5.2并不是完全兼容。PHP5.3在PHP5.2的基础上做了诸多改进，其中垃圾回收算法就属于一个比较大的改变。本文将分别讨论PHP5.2和PHP5.3的垃圾回收机制，并讨论这种演化和改进对于程序员编写PHP的影响以及要注意的问题。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;PHP变量及关联内存对象的内部表示&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;垃圾回收说到底是对变量及其所关联内存对象的操作，所以在讨论PHP的垃圾回收机制之前，先简要介绍PHP中变量及其内存对象的内部表示（其C源代码中的表示）。&lt;/p&gt;&#xD;
&lt;p&gt;PHP官方文档中将PHP中的变量划分为两类：标量类型和复杂类型。标量类型包括布尔型、整型、浮点型和字符串；复杂类型包括数组、对象和资源；还有一个NULL比较特殊，它不划分为任何类型，而是单独成为一类。&lt;/p&gt;&#xD;
&lt;p&gt;所有这些类型，在PHP内部统一用一个叫做zval的结构表示，在PHP源代码中这个结构名称为&amp;ldquo;_zval_struct&amp;rdquo;。zval的具体定义在PHP源代码的&amp;ldquo;Zend/zend.h&amp;rdquo;文件中，下面是相关代码的摘录。&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;typedef union _zvalue_value {&#xD;
	long lval;					/* long value */&#xD;
	double dval;				/* double value */&#xD;
	struct {&#xD;
		char *val;&#xD;
		int len;&#xD;
	} str;&#xD;
	HashTable *ht;				/* hash table value */&#xD;
	zend_object_value obj;&#xD;
} zvalue_value;&#xD;
&#xD;
struct _zval_struct {&#xD;
	/* Variable information */&#xD;
	zvalue_value value;		/* value */&#xD;
	zend_uint refcount__gc;&#xD;
	zend_uchar type;	/* active type */&#xD;
	zend_uchar is_ref__gc;&#xD;
};&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;其中联合体&amp;ldquo;_zvalue_value&amp;rdquo;用于表示PHP中所有变量的值，这里之所以使用union，是因为一个zval在一个时刻只能表示一种类型的变量。可以看到_zvalue_value中只有5个字段，但是PHP中算上NULL有8种数据类型，那么PHP内部是如何用5个字段表示8种类型呢？这算是PHP设计比较巧妙的一个地方，它通过复用字段达到了减少字段的目的。例如，在PHP内部布尔型、整型及资源（只要存储资源的标识符即可）都是通过lval字段存储的；dval用于存储浮点型；str存储字符串；ht存储数组（注意PHP中的数组其实是哈希表）；而obj存储对象类型；如果所有字段全部置为0或NULL则表示PHP中的NULL，这样就达到了用5个字段存储8种类型的值。&lt;/p&gt;&#xD;
&lt;p&gt;而当前zval中的value（value的类型即是_zvalue_value）到底表示那种类型，则由&amp;ldquo;_zval_struct&amp;rdquo;中的type确定。_zval_struct即是zval在C语言中的具体实现，每个zval表示一个变量的内存对象。除了value和type，可以看到_zval_struct中还有两个字段refcount__gc和is_ref__gc，从其后缀就可以断定这两个家伙与垃圾回收有关。没错，PHP的垃圾回收全靠这俩字段了。其中refcount__gc表示当前有几个变量引用此zval，而is_ref__gc表示当前zval是否被按引用引用，这话听起来很拗口，这和PHP中zval的&amp;ldquo;Write-On-Copy&amp;rdquo;机制有关，由于这个话题不是本文重点，因此这里不再详述，读者只需记住refcount__gc这个字段的作用即可。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;PHP5.2中的垃圾回收算法&amp;mdash;&amp;mdash;Reference Counting&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;PHP5.2中使用的内存回收算法是大名鼎鼎的&lt;a target="_blank" href="http://en.wikipedia.org/wiki/Reference_counting"&gt;Reference Counting&lt;/a&gt;，这个算法中文翻译叫做&amp;ldquo;引用计数&amp;rdquo;，其思想非常直观和简洁：为每个内存对象分配一个计数器，当一个内存对象建立时计数器初始化为1（因此此时总是有一个变量引用此对象），以后每有一个新变量引用此内存对象，则计数器加1，而每当减少一个引用此内存对象的变量则计数器减1，当垃圾回收机制运作的时候，将所有计数器为0的内存对象销毁并回收其占用的内存。而PHP中内存对象就是zval，而计数器就是refcount__gc。&lt;/p&gt;&#xD;
&lt;p&gt;例如下面一段PHP代码演示了PHP5.2计数器的工作原理（计数器值通过&lt;a target="_blank" href="http://www.xdebug.org/"&gt;xdebug&lt;/a&gt;得到）：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;?php&#xD;
&#xD;
$val1 = 100; //zval(val1).refcount_gc = 1;&#xD;
$val2 = $val1; //zval(val1).refcount_gc = 2,zval(val2).refcount_gc = 2(因为是Write on copy，当前val2与val1共同引用一个zval)&#xD;
$val2 = 200; //zval(val1).refcount_gc = 1,zval(val2).refcount_gc = 1(此处val2新建了一个zval)&#xD;
unset($val1); //zval(val1).refcount_gc = 0($val1引用的zval再也不可用，会被GC回收)&#xD;
&#xD;
?&amp;gt;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;Reference Counting简单直观，实现方便，但却存在一个致命的缺陷，就是容易造成内存泄露。很多朋友可能已经意识到了，如果存在循环引用，那么Reference Counting就可能导致内存泄露。例如下面的代码：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;?php&#xD;
&#xD;
$a = array();&#xD;
$a[] = &amp;amp; $a;&#xD;
unset($a);&#xD;
&#xD;
?&amp;gt;&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;p&gt;这段代码首先建立了数组a，然后让a的第一个元素按引用指向a，这时a的zval的refcount就变为2，然后我们销毁变量a，此时a最初指向的zval的refcount为1，但是我们再也没有办法对其进行操作，因为其形成了一个循环自引用，如下图所示：&lt;/p&gt;&#xD;
&lt;p&gt;&lt;a href="http://images.cnblogs.com/cnblogs_com/leoo2sk/201102/20110226165422276.png"&gt;&lt;img height="300" width="357" src="http://images.cnblogs.com/cnblogs_com/leoo2sk/201102/201102261654241747.png" alt="image" border="0" title="image" style="display: block; float: none; margin-left: auto; margin-right: auto; border: 0px;" /&gt;&lt;/a&gt; &lt;/p&gt;&#xD;
&lt;p&gt;其中灰色部分表示已经不复存在。由于a之前指向的zval的refcount为1（被其HashTable的第一个元素引用），这个zval就不会被GC销毁，这部分内存就泄露了。&lt;/p&gt;&#xD;
&lt;p&gt;这里特别要指出的是，PHP是通过符号表（Symbol Table）存储变量符号的，全局有一个符号表，而每个复杂类型如数组或对象有自己的符号表，因此上面代码中，a和a[0]是两个符号，但是a储存在全局符号表中，而a[0]储存在数组本身的符号表中，且这里a和a[0]引用同一个zval（当然符号a后来被销毁了）。希望读者朋友注意分清符号（Symbol）的zval的关系。&lt;/p&gt;&#xD;
&lt;p&gt;在PHP只用于做动态页面脚本时，这种泄露也许不是很要紧，因为动态页面脚本的生命周期很短，PHP会保证当脚本执行完毕后，释放其所有资源。但是PHP发展到目前已经不仅仅用作动态页面脚本这么简单，如果将PHP用在生命周期较长的场景中，例如自动化测试脚本或deamon进程，那么经过多次循环后积累下来的内存泄露可能就会很严重。这并不是我在耸人听闻，我曾经实习过的一个公司就通过PHP写的deamon进程来与数据存储服务器交互。&lt;/p&gt;&#xD;
&lt;p&gt;由于Reference Counting的这个缺陷，PHP5.3改进了垃圾回收算法。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;PHP5.3中的垃圾回收算法&amp;mdash;&amp;mdash;Concurrent Cycle Collection in Reference Counted Systems&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;PHP5.3的垃圾回收算法仍然以引用计数为基础，但是不再是使用简单计数作为回收准则，而是使用了一种同步回收算法，这个算法由IBM的工程师在论文&lt;a target="_blank" href="http://www.research.ibm.com/people/d/dfb/papers/Bacon01Concurrent.pdf"&gt;Concurrent Cycle Collection in Reference Counted Systems&lt;/a&gt;中提出。&lt;/p&gt;&#xD;
&lt;p&gt;这个算法可谓相当复杂，从论文29页的数量我想大家也能看出来，所以我不打算（也没有能力）完整论述此算法，有兴趣的朋友可以阅读上面的提到的论文（强烈推荐，这篇论文非常精彩）。&lt;/p&gt;&#xD;
&lt;p&gt;我在这里，只能大体描述一下此算法的基本思想。&lt;/p&gt;&#xD;
&lt;p&gt;首先PHP会分配一个固定大小的&amp;ldquo;根缓冲区&amp;rdquo;，这个缓冲区用于存放固定数量的zval，这个数量默认是10,000，如果需要修改则需要修改源代码Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES然后重新编译。&lt;/p&gt;&#xD;
&lt;p&gt;由上文我们可以知道，一个zval如果有引用，要么被全局符号表中的符号引用，要么被其它表示复杂类型的zval中的符号引用。因此在zval中存在一些可能根（root）。这里我们暂且不讨论PHP是如何发现这些可能根的，这是个很复杂的问题，总之PHP有办法发现这些可能根zval并将它们投入根缓冲区。&lt;/p&gt;&#xD;
&lt;p&gt;当根缓冲区满额时，PHP就会执行垃圾回收，此回收算法如下：&lt;/p&gt;&#xD;
&lt;p&gt;1、对每个根缓冲区中的根zval按照深度优先遍历算法遍历所有能遍历到的zval，并将每个zval的refcount减1，同时为了避免对同一zval多次减1（因为可能不同的根能遍历到同一个zval），每次对某个zval减1后就对其标记为&amp;ldquo;已减&amp;rdquo;。&lt;/p&gt;&#xD;
&lt;p&gt;2、再次对每个缓冲区中的根zval深度优先遍历，如果某个zval的refcount不为0，则对其加1，否则保持其为0。&lt;/p&gt;&#xD;
&lt;p&gt;3、清空根缓冲区中的所有根（注意是把这些zval从缓冲区中清除而不是销毁它们），然后销毁所有refcount为0的zval，并收回其内存。&lt;/p&gt;&#xD;
&lt;p&gt;如果不能完全理解也没有关系，只需记住PHP5.3的垃圾回收算法有以下几点特性：&lt;/p&gt;&#xD;
&lt;p&gt;1、并不是每次refcount减少时都进入回收周期，只有根缓冲区满额后在开始垃圾回收。&lt;/p&gt;&#xD;
&lt;p&gt;2、可以解决循环引用问题。&lt;/p&gt;&#xD;
&lt;p&gt;3、可以总将内存泄露保持在一个阈值以下。&lt;/p&gt;&#xD;
&lt;p&gt;&lt;strong&gt;PHP5.2与PHP5.3垃圾回收算法的性能比较&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;由于我目前条件所限，我就不重新设计试验了，而是直接引用PHP Manual中的实验，关于两者的性能比较请参考PHP Manual中的相关章节：&lt;a href="http://www.php.net/manual/en/features.gc.performance-considerations.php" target="_blank"&gt;http://www.php.net/manual/en/features.gc.performance-considerations.php&lt;/a&gt;。&lt;/p&gt;&#xD;
&lt;p&gt;首先是内存泄露试验，下面直接引用&lt;a target="_blank" href="http://www.php.net/manual/en/index.php"&gt;PHP Manual&lt;/a&gt;中的实验代码和试验结果图：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;?php&#xD;
class Foo&#xD;
{&#xD;
    public $var = '3.1415962654';&#xD;
}&#xD;
&#xD;
$baseMemory = memory_get_usage();&#xD;
&#xD;
for ( $i = 0; $i &amp;lt;= 100000; $i++ )&#xD;
{&#xD;
    $a = new Foo;&#xD;
    $a-&amp;gt;self = $a;&#xD;
    if ( $i % 500 === 0 )&#xD;
    {&#xD;
        echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "\n";&#xD;
    }&#xD;
}&#xD;
?&amp;gt;&#xD;
&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
&lt;img alt="PHP内存泄露试验" height="282" width="500" src="http://www.php.net/manual/en/images/12f37b1c6963c1c5c18f30495416a197-gc-benchmark.png" /&gt;&lt;br /&gt;&#xD;
&lt;p&gt;可以看到在可能引发累积性内存泄露的场景下，PHP5.2发生持续累积性内存泄露，而PHP5.3则总能将内存泄露控制在一个阈值以下（与根缓冲区大小有关）。&lt;/p&gt;&#xD;
&lt;p&gt;另外是关于性能方面的对比：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;&amp;lt;?php&#xD;
class Foo&#xD;
{&#xD;
    public $var = '3.1415962654';&#xD;
}&#xD;
&#xD;
for ( $i = 0; $i &amp;lt;= 1000000; $i++ )&#xD;
{&#xD;
    $a = new Foo;&#xD;
    $a-&amp;gt;self = $a;&#xD;
}&#xD;
&#xD;
echo memory_get_peak_usage(), "\n";&#xD;
?&amp;gt;&#xD;
&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
这个脚本执行1000000次循环，使得延迟时间足够进行对比。&#xD;
&lt;p&gt;然后使用CLI方式分别在打开内存回收和关闭内存回收的的情况下运行此脚本：&lt;/p&gt;&#xD;
&lt;div &gt;&#xD;
&lt;pre &gt;time php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php&#xD;
# and&#xD;
time php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php&#xD;
&lt;/pre&gt;&#xD;
&lt;/div&gt;&#xD;
在我的机器环境下，运行时间分别为6.4s和7.2s，可以看到PHP5.3的垃圾回收机制会慢一些，但是影响并不大。&lt;br /&gt;&#xD;
&lt;p&gt;&lt;strong&gt;与垃圾回收算法相关的PHP配置&lt;/strong&gt;&lt;/p&gt;&#xD;
&lt;p&gt;可以通过修改php.ini中的zend.enable_gc来打开或关闭PHP的垃圾回收机制，也可以通过调用gc_enable()或gc_disable()打开或关闭PHP的垃圾回收机制。在PHP5.3中即使关闭了垃圾回收机制，PHP仍然会记录可能根到根缓冲区，只是当根缓冲区满额时，PHP不会自动运行垃圾回收，当然，任何时候您都可以通过手工调用gc_collect_cycles()函数强制执行内存回收。&lt;/p&gt;&lt;img src="http://www.cnblogs.com/leoo2sk/aggbug/1965750.html?type=1" width="1" height="1" alt=""/&gt;&lt;p&gt;&lt;a href="http://www.cnblogs.com/leoo2sk/archive/2011/02/27/php-gc.html" target="_blank"&gt;本文链接&lt;/a&gt;&lt;/p&gt;</content></entry></feed>
