标签归档:优化

用户参与记录存储的演变

有这样一个应用场景:用户有两个连续的操作A和操作B,必须是操作A完成后才能执行操作B,如果操作A没有完成就触发了操作B,则显示用户需要先执行操作A,即在操作B执行需要查询操作A是否执行过。这里引申出来的问题是,记录用户参与记录,提供针对用户和操作的查询方法。当不同的数据量时,我们的存储方案会大不相同,随着数据的增长,方案不断演变。

1、数据量较小,用户操作行为固定:
存储:MySQL
方案:我们以UID为key,一行一个用户,每个用户包括的用户作为列存储,比如UID=100,固定存储为操作A和操作B,则表结构大致如下:
table_operation
uid operation_a operation_b
100 1 1

如果我们要查询用户是否参与A或B时,直接使用SQL: SELECT * FROM table_operation WHERE uid=100 AND action_a=1就可以达成目标。

问题:用户操作固定,扩展较难,如果需要增加用户操作行为,则需要增加字段或增加表存储,增加字段的方法在一定的数据量级以下(比如100万)是可行的,如果行为间无关,则增加表存储方案的表现会很不错。

2、数据量较小、用户操作行为不固定:
与场景1相比,当前场景除了uid这个变量,增加了用户操作变量,即我们需要关注用户和用户操作两个变量。
存储:MySQL
方案1:增加操作表,生成操作id,用户操作行为表存储uid和oid。当用户执行一个新的操作时就在操作行为表插入一条记录。其表结构大致如下:

table_operation_info
oid name
1 operation_a
2 operation_b

table_operation
uid oid
1 1
1 2

当需要查询用户1是否执行过操作A时,使用SQL:SELECT * FROM table_operation WHERE oid=1 AND oid=1。
问题:当用户的操作行为较多时,用户操作行为增长速度很快,数据量也为逐渐增大,可能MySQL单表无法负载。解决方案在后续场景中说明。

3、数据量较大,用户行为固定
存储:MySQL
方案:与场景1相比,当前场景不同在于数据量比场景1大,数据量大到MySQL单表负载不过来。此方案解决的就是这个问题,当单表太大时,性价比较高的方法一般是采用分表。我们当前场景的变量是uid,只要依据uid按水平分表即可。

4、数据量较大,用户行为不固定
存储: MySQL
方案1:此方案应用于用户的操作行为可以分类的情况,即在场景1的基础上增加两次分表操作,按操作行为类分表和按用户分表。当前方案中我们需要应对两个变量:操作行为和用户。两次分表分别对应这两个变量,按业务规则做操作行为的分表操作,按用户id水平切分减少数据量。

方案2:此方案是完全的水平分表操作,在场景2的方案基础上,按用户水平切分。

5、数据量超大
存储: MySQL
方案1:分库分表,此时一个库已经无法满足需求,规则依据前面的场景实现,根据实际的需求可以考虑把不同的库放不同的机器上。
方案2:在分库分表的基础上,按位存储,因为一个操作行为有没有执行过是一个是否的状态,即0,1状态,因此我们可以用一个位来存储,64位可以存储64个操作行为的标记。

其它存储
key-value数据库
我们的需求实际上并不需要太多的关系型数据库的功能,简单的 k-v数据库就可以实现我们的功能,并且在性能上也会有所提升,毕竟做得少,会快。
先不管是选择基于内存的,还是非内存的(可以根据实际需求来选择,也可以是热点数据在内存,沉默数据在非内存中),假设我们有足够的空间存储。
方案1:
以uid+oid为key,值可以存储状态,也可以只存储是否参与(0和1),但是会存在key太多的情况,特别是当数据量超大时,uid的个数*oid的个数,可能是你无法相像的量级。
方案2:
一般来说,用户操作行为的数据量完全小于用户的量级,并且用户操作行为的数据可控。如果要减少key的个数,我们可以使用oid+用户分区索引id作为key,这里所谓的用户分区索引是指将用户以某个数量分成一个区,所有的用户都记录在这个这个区间内,比如以10000为一个区间,则uid为1到9999的用户分到区间0,这里可以以1和0存储用户是否执行了此操作,一个key对应的value初始化存储10000个0。当uid=100的用户执行了某操作,则将第100个0置为1。
方案3:
在方案2的基础上,将10000个0转换为10000个01位,假设一个位存储50位,则总共只需要200个。
方案4:
当用户量超大时,大多数的用户对于某个操作可能都是没有参与的,则在方案3的基础上我们增加简单的稀疏矩阵压缩,给每个存储位添加索引,当存储值不为0时才会存储。
方案5:
我还没想到,期待你的分享

小结

  • 随着数据量的增大,总的思路是分冶,当一个表搞不定时分表,当一个库搞不定时分库,当一台机器搞不定时加机器。
  • 对于不同的存储介质选择需要考虑成本和需求,所有的选择都是平衡后的结果。
  • 节省空间,按位存储。
  • 不要过早优化。

一次查询优化过程

问题描述

在正在维护的系统中有一个数据分析的模块,用于来分析一些用户访问的数据。其中有一个操作是查找某URL地址对应的ID。在这里ID与URL是存储MySQL数据的一个表中,此表就两个字段,id,url.表存储引擎类型MyISAM,当然URL字段是加了索引的。现在每天按小时定时分析数据,开始程序没有问题,当几年的数据积累下来,特别是最近业务量增加时,明显感觉到程序的执行过程变得很漫长,通过xdebug分析一次执行过程的所有执行时间,发现瓶颈在于前面所提到的通过URL地址对应的ID。下面就开始了漫长的优化之路。

第一次优化:优化细节,针对特殊地址的优化

由于应急,先考虑一些细节的调整:
首先,我们针对一些特殊的地址进行了处理,直接返回ID,比如没有URL的情况,比如Google的首页地址;
然后,我们针对一些常见的地址作为Hash存储,以及一些热数据进行内存缓存。

结果,提高一些性能,能满足当时的业务需求,特别是当特殊的地址较多的时候,其情况还是非常乐观的。

过了一段时间,到了另一个高峰点,发现此分析模块仍然会时不时的出些问题,导致数据不准确,各种情况,各种应付…不得已,开始思考另一个方案。

第一个方案: key-value数据库

依据前面的对于热点数据,将其存储到内存缓存的思路,我们考虑是否将所有数据都作为热点数据,于是先做前期预研,将部分数据导到redis中,发现速度有了极大的提升,大概在500倍的一个量级,那叫一个激动,于是决定将所有数据都迁移到redis中,想法是美好的,结果是不言而喻的。失败了。为什么呢?很简单,没资源,我们没有那么大的内存存储N个上千万的表。

第二个方案:基于单词查找树的文件结构

从前面的key-value数据库方案看,存储url到id的映射,在实现思路上还是可行的,于是开动了小心思,既然内存不够用,那么用硬盘呢?

这个既便宜又实惠,是居家旅行,杀人越货的必备良品。于是计划以URL的主域名为目录,以每个地址的md5值为文件名,每个文件存储这个地址对应的ID。考虑到域名可能很多,于是计划使用变形的单词查找树来设置多级目录,但是当域名地址太长时,文件系统在生成目录的时候会出错,只得去掉单词查找树,使用某种简单的按主域名转化后的字符串转建立目录。在预研时,先生成了5万数据的文件结构,发现生成的速率很慢,并且由于大量小文件的生成,对于整个目录的查询会很慢,但对于单个文件的读取还是很快的,然而将随着数据生成越来越多,当达到40万时,整个目录占用了大概4G+的硬盘空间。

假想一下,当1千万的数据以这种方式存储到硬盘上,会是多少?如果数据持续增加呢?到达一个亿呢?感觉到不靠谱了。于是继续想下一种方案,这个方案继续执行。

第三个方案:信赖于数据库,增加md5值存储的字段

在第二种方案中,我们是使用md5值作为文件名,在想在数据库对于md5后的文件名的查找会更快一些呢?在ID和URL映射表中,增加一个字段url_md5,存储url使用md5后的值,在这里我们直接使用MySQL的md5函数更新表数据,测试后,发现其速度有了显著提升,单个查找操作大概能提升50倍+。整体性能提升5倍的样子。于是,果断采用此方案,结果是该分析模块一直没啥事发生。(^_^)

总结

  1. 不要过早优化
  2. 优化要先找瓶颈
  3. 数据结构和对工具的使用很重要
  4. 优化时需要理清问题的根本原因是什么,最好能到理论层面或实现原理层面。