<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>潘锦的空间 &#187; 优化</title>
	<atom:link href="https://www.phppan.com/tag/%e4%bc%98%e5%8c%96/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.phppan.com</link>
	<description>SaaS SaaS架构 团队管理 技术管理 技术架构 PHP 内核 扩展 项目管理</description>
	<lastBuildDate>Sat, 25 Apr 2026 00:56:17 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=3.9.40</generator>
	<item>
		<title>用户参与记录存储的演变</title>
		<link>https://www.phppan.com/2013/07/user-operation-record/</link>
		<comments>https://www.phppan.com/2013/07/user-operation-record/#comments</comments>
		<pubDate>Sun, 14 Jul 2013 08:35:39 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[程序相关]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[位存储]]></category>
		<category><![CDATA[分库]]></category>
		<category><![CDATA[分表]]></category>

		<guid isPermaLink="false">http://www.phppan.com/?p=1847</guid>
		<description><![CDATA[有这样一个应用场景：用户有两个连续的操作A和操作B，必须是操作A完成后才能执行操作B，如果操作A没有完成就触发 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>有这样一个应用场景：用户有两个连续的操作A和操作B，必须是操作A完成后才能执行操作B，如果操作A没有完成就触发了操作B，则显示用户需要先执行操作A，即在操作B执行需要查询操作A是否执行过。这里引申出来的问题是，记录用户参与记录，提供针对用户和操作的查询方法。当不同的数据量时，我们的存储方案会大不相同，随着数据的增长，方案不断演变。</p>
<p><strong>1、数据量较小，用户操作行为固定：</strong><br />
     存储：MySQL<br />
     方案：我们以UID为key，一行一个用户，每个用户包括的用户作为列存储，比如UID=100，固定存储为操作A和操作B，则表结构大致如下：<br />
     table_operation<br />
     uid   operation_a  operation_b<br />
     100   1             1      </p>
<p>如果我们要查询用户是否参与A或B时，直接使用SQL: SELECT * FROM table_operation WHERE uid=100  AND  action_a=1就可以达成目标。</p>
<p>    问题：用户操作固定，扩展较难，如果需要增加用户操作行为，则需要增加字段或增加表存储，增加字段的方法在一定的数据量级以下（比如100万）是可行的，如果行为间无关，则增加表存储方案的表现会很不错。</p>
<p><strong>2、数据量较小、用户操作行为不固定：</strong><br />
     与场景1相比，当前场景除了uid这个变量，增加了用户操作变量，即我们需要关注用户和用户操作两个变量。<br />
     存储：MySQL<br />
     方案1：增加操作表，生成操作id，用户操作行为表存储uid和oid。当用户执行一个新的操作时就在操作行为表插入一条记录。其表结构大致如下：</p>
<p>      table_operation_info<br />
      oid    name<br />
      1       operation_a<br />
      2       operation_b</p>
<p>      table_operation<br />
      uid   oid<br />
      1      1<br />
      1      2</p>
<p>     当需要查询用户1是否执行过操作A时，使用SQL：SELECT * FROM table_operation WHERE oid=1 AND  oid=1。<br />
      问题：当用户的操作行为较多时，用户操作行为增长速度很快，数据量也为逐渐增大，可能MySQL单表无法负载。解决方案在后续场景中说明。</p>
<p><strong>3、数据量较大，用户行为固定</strong><br />
       存储：MySQL<br />
       方案：与场景1相比，当前场景不同在于数据量比场景1大，数据量大到MySQL单表负载不过来。此方案解决的就是这个问题，当单表太大时，性价比较高的方法一般是采用分表。我们当前场景的变量是uid，只要依据uid按水平分表即可。</p>
<p><strong>4、数据量较大，用户行为不固定</strong><br />
      存储： MySQL<br />
      方案1：此方案应用于用户的操作行为可以分类的情况，即在场景1的基础上增加两次分表操作，按操作行为类分表和按用户分表。当前方案中我们需要应对两个变量：操作行为和用户。两次分表分别对应这两个变量，按业务规则做操作行为的分表操作，按用户id水平切分减少数据量。</p>
<p>       方案2：此方案是完全的水平分表操作，在场景2的方案基础上，按用户水平切分。</p>
<p><strong>5、数据量超大</strong><br />
     存储： MySQL<br />
     方案1：分库分表，此时一个库已经无法满足需求，规则依据前面的场景实现，根据实际的需求可以考虑把不同的库放不同的机器上。<br />
     方案2：在分库分表的基础上，按位存储，因为一个操作行为有没有执行过是一个是否的状态，即0，1状态，因此我们可以用一个位来存储，64位可以存储64个操作行为的标记。</p>
<p><strong>其它存储</strong><br />
     key-value数据库<br />
     我们的需求实际上并不需要太多的关系型数据库的功能，简单的 k-v数据库就可以实现我们的功能，并且在性能上也会有所提升，毕竟做得少，会快。<br />
     先不管是选择基于内存的，还是非内存的（可以根据实际需求来选择，也可以是热点数据在内存，沉默数据在非内存中），假设我们有足够的空间存储。<br />
     方案1：<br />
        以uid+oid为key，值可以存储状态，也可以只存储是否参与（0和1），但是会存在key太多的情况，特别是当数据量超大时，uid的个数*oid的个数，可能是你无法相像的量级。<br />
     方案2：<br />
        一般来说，用户操作行为的数据量完全小于用户的量级，并且用户操作行为的数据可控。如果要减少key的个数，我们可以使用oid+用户分区索引id作为key，这里所谓的用户分区索引是指将用户以某个数量分成一个区，所有的用户都记录在这个这个区间内，比如以10000为一个区间，则uid为1到9999的用户分到区间0，这里可以以1和0存储用户是否执行了此操作，一个key对应的value初始化存储10000个0。当uid=100的用户执行了某操作，则将第100个0置为1。<br />
      方案3：<br />
           在方案2的基础上，将10000个0转换为10000个01位，假设一个位存储50位，则总共只需要200个。<br />
      方案4：<br />
         当用户量超大时，大多数的用户对于某个操作可能都是没有参与的，则在方案3的基础上我们增加简单的稀疏矩阵压缩，给每个存储位添加索引，当存储值不为0时才会存储。<br />
     方案5：<br />
        我还没想到，期待你的分享</p>
<p><strong>小结</strong></p>
<ul>
<li>随着数据量的增大，总的思路是分冶，当一个表搞不定时分表，当一个库搞不定时分库，当一台机器搞不定时加机器。</li>
<li>对于不同的存储介质选择需要考虑成本和需求，所有的选择都是平衡后的结果。</li>
<li>节省空间，按位存储。</li>
<li>不要过早优化。</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2013/07/user-operation-record/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>一次查询优化过程</title>
		<link>https://www.phppan.com/2012/01/query-optimization/</link>
		<comments>https://www.phppan.com/2012/01/query-optimization/#comments</comments>
		<pubDate>Sun, 15 Jan 2012 01:07:19 +0000</pubDate>
		<dc:creator><![CDATA[admin]]></dc:creator>
				<category><![CDATA[程序相关]]></category>
		<category><![CDATA[优化]]></category>
		<category><![CDATA[工作事记]]></category>

		<guid isPermaLink="false">http://www.phppan.com/?p=1586</guid>
		<description><![CDATA[问题描述 在正在维护的系统中有一个数据分析的模块，用于来分析一些用户访问的数据。其中有一个操作是查找某URL地 [&#8230;]]]></description>
				<content:encoded><![CDATA[<h2>问题描述</h2>
<p>在正在维护的系统中有一个数据分析的模块，用于来分析一些用户访问的数据。其中有一个操作是查找某URL地址对应的ID。在这里ID与URL是存储MySQL数据的一个表中，此表就两个字段，id,url.表存储引擎类型MyISAM，当然URL字段是加了索引的。现在每天按小时定时分析数据，开始程序没有问题，当几年的数据积累下来，特别是最近业务量增加时，明显感觉到程序的执行过程变得很漫长，通过xdebug分析一次执行过程的所有执行时间，发现瓶颈在于前面所提到的通过URL地址对应的ID。下面就开始了漫长的优化之路。</p>
<style>
.entry p {margin:13px 5px 0 5px;}
</style>
<p></p>
<h3>第一次优化：优化细节，针对特殊地址的优化</h3>
<p>由于应急，先考虑一些细节的调整：<br />
首先，我们针对一些特殊的地址进行了处理，直接返回ID，比如没有URL的情况，比如Google的首页地址；<br />
然后，我们针对一些常见的地址作为Hash存储，以及一些热数据进行内存缓存。</p>
<p>结果，提高一些性能，能满足当时的业务需求，特别是当特殊的地址较多的时候，其情况还是非常乐观的。</p>
<p>过了一段时间，到了另一个高峰点，发现此分析模块仍然会时不时的出些问题，导致数据不准确，各种情况，各种应付&#8230;不得已，开始思考另一个方案。</p>
<p></p>
<h3>第一个方案： key-value数据库</h3>
<p> 依据前面的对于热点数据，将其存储到内存缓存的思路，我们考虑是否将所有数据都作为热点数据，于是先做前期预研，将部分数据导到redis中，发现速度有了极大的提升，大概在500倍的一个量级，那叫一个激动，于是决定将所有数据都迁移到redis中，想法是美好的，结果是不言而喻的。失败了。为什么呢？很简单，没资源，我们没有那么大的内存存储N个上千万的表。</p>
<p></p>
<h3>第二个方案：基于单词查找树的文件结构</h3>
<p>从前面的key-value数据库方案看，存储url到id的映射，在实现思路上还是可行的，于是开动了小心思，既然内存不够用，那么用硬盘呢？</p>
<p>这个既便宜又实惠，是居家旅行，杀人越货的必备良品。于是计划以URL的主域名为目录，以每个地址的md5值为文件名，每个文件存储这个地址对应的ID。考虑到域名可能很多，于是计划使用变形的单词查找树来设置多级目录，但是当域名地址太长时，文件系统在生成目录的时候会出错，只得去掉单词查找树，使用某种简单的按主域名转化后的字符串转建立目录。在预研时，先生成了5万数据的文件结构，发现生成的速率很慢，并且由于大量小文件的生成，对于整个目录的查询会很慢，但对于单个文件的读取还是很快的，然而将随着数据生成越来越多，当达到40万时，整个目录占用了大概4G+的硬盘空间。</p>
<p>假想一下，当1千万的数据以这种方式存储到硬盘上，会是多少？如果数据持续增加呢？到达一个亿呢？感觉到不靠谱了。于是继续想下一种方案，这个方案继续执行。</p>
<p></p>
<h3>第三个方案：信赖于数据库，增加md5值存储的字段</h3>
<p>在第二种方案中，我们是使用md5值作为文件名，在想在数据库对于md5后的文件名的查找会更快一些呢？在ID和URL映射表中，增加一个字段url_md5，存储url使用md5后的值，在这里我们直接使用MySQL的md5函数更新表数据，测试后，发现其速度有了显著提升，单个查找操作大概能提升50倍+。整体性能提升5倍的样子。于是，果断采用此方案，结果是该分析模块一直没啥事发生。(^_^)</p>
<p></p>
<h3>总结</h3>
<ol>
<li>不要过早优化</li>
<li>优化要先找瓶颈</li>
<li>数据结构和对工具的使用很重要</li>
<li>优化时需要理清问题的根本原因是什么，最好能到理论层面或实现原理层面。</li>
</ol>
]]></content:encoded>
			<wfw:commentRss>https://www.phppan.com/2012/01/query-optimization/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
