1、面向程序员的数据库访问性能优化法则特别说明:1、 本文只是面对数据库应用开发的程序员,不适合专业 DBA,DBA 在数据库性能优化方面需要了解更多的知识;2、 本文许多示例及概念是基于 Oracle 数据库描述,对于其它关系型数据库也可以参考,但许多观点不适合于 KV 数据库或内存数据库或者是基于 SSD 技术的数据库;3、 本文未深入数据库优化中最核心的执行计划分析技术。读者对像:开发人员:如果你是做数据库开发,那本文的内容非常适合,因为本文是从程序员的角度来谈数据库性能优化。架构师:如果你已经是数据库应用的架构师,那本文的知识你应该清楚 90%,否则你可能是一个喜欢折腾的架构师。DBA(数
2、据库管理员):大型数据库优化的知识非常复杂,本文只是从程序员的角度来谈性能优化,DBA 除了需要了解这些知识外,还需要深入数据库的内部体系架构来解决问题。引言在网上有很多文章介绍数据库优化知识,但是大部份文章只是对某个一个方面进行说明,而对于我们程序员来说这种介绍并不能很好的掌握优化知识,因为很多介绍只是对一些特定的场景优化的,所以反而有时会产生误导或让程序员感觉不明白其中的奥妙而对数据库优化感觉很神秘。很多程序员总是问如何学习数据库优化,有没有好的教材之类的问题。在书店也看到了许多数据库优化的专业书籍,但是感觉更多是面向 DBA 或者是PL/SQL 开 发方面的知识,个人感觉不太适合普通程序
3、员。而要想做到数据库优化的高手,不是花几周,几个月就能达到的,这并不是因为数据库优化有多高深,而是因为要做 好优化一方面需要有非常好的技术功底,对操作系统、存储硬件网络、数据库原理等方面有比较扎实的基础知识,另一方面是需要花大量时间对特定的数据库进行实 践测试与总结。作为一个程序员,我们也许不清楚线上正式的服务器硬件配置,我们不可能像 DBA 那样专业的对数据库进行各种实践测试与总结,但我们都应该非常了解我们 SQL 的业务逻辑,我们清楚 SQL 中访问表及字段的数据情况,我们其实只关心我们的 SQL 是否能尽快返回结果。那程序员如何利用已知的知识进行数据库优化?如何能快速定位 SQL 性能问
4、题并找到正确的优化方向?面对这些问题,笔者总结了一些面向程序员的基本优化法则,本文将结合实例来坦述数据库开发的优化知识。一、数据库访问优化法则简介要正确的优化 SQL,我们需要快速定位能性的瓶颈点,也就是说快速找到我们 SQL 主要的开销在哪里?而大多数情况性能最慢的设备会是瓶颈点,如下载时网络速度可能会是瓶颈点,本地复制文件时硬盘可能会是瓶颈点,为什么这些一般的工作我们能快速确认瓶颈点呢,因为我们对这些慢速设备的性能数据有一些基本的认识,如网络带宽是 2Mbps,硬盘是每分钟 7200 转等等。因此,为了快速找到 SQL 的性能瓶颈点,我们也需要了解我们计算机系统的硬件基本性能指标,下图展示
5、的当前主流计算机性能指标数据。从图上可以看到基本上每种设备都有两个指标:延时(响应时间):表示硬件的突发处理能力;带宽(吞吐量):代表硬件持续处理能力。从上图可以看出,计算机系统硬件性能从高到代依次为:CPUCache(L1-L2-L3)内存SSD 硬盘 网络硬盘由于 SSD 硬盘还处于快速发展阶段,所以本文的内容不涉及 SSD 相关应用系统。根据数据库知识,我们可以列出每种硬件主要的工作内容:CPU 及内存:缓存数据访问、比较、排序、事务检测、SQL 解析、函数或逻辑运算;网络:结果数据传输、SQL 请求、远程数据库访问(dblink);硬盘:数据访问、数据写入、日志记录、大数据量排序、大表
6、连接。根据当前计算机硬件的基本性能指标及其在数据库中主要操作内容,可以整理出如下图所示的性能基本优化法则:这个优化法则归纳为 5 个层次:1、 减少数据访问(减少磁盘访问)2、 返回更少数据(减少网络传输或磁盘访问)3、 减少交互次数(减少网络传输)4、 减少服务器 CPU 开销(减少 CPU 及内存开销)5、 利用更多资源(增加资源)由于每一层优化法则都是解决其对应硬件的性能问题,所以带来的性能提升比例也不一样。传统数据库系统设计是也是尽可能对低速设备提供优化方法,因此针对低速设备问题的可优化手段也更多,优化成本也更低。我们任何一个SQL 的性能优化都应该按这个规则由上到下来诊断问题并提出解
7、决方案,而不应该首先想到的是增加资源解决问题。以下是每个优化法则层级对应优化效果及成本经验参考:优化法则 性能提升效果 优化成本减少数据访问 11000 低返回更少数据 1100 低减少交互次数 120 低减少服务器 CPU 开销 15 低利用更多资源 10 高接下来,我们针对 5 种优化法则列举常用的优化手段并结合实例分析。二、Oracle 数据库两个基本概念数据块(Block)数据块是数据库中数据在磁盘中存储的最小单位,也是一次 IO 访问的最小单位,一个数据块通常可以存储多条记录,数据块大小是 DBA 在创建数据库或表空间时指定,可指定为 2K、4K 、8K、16K 或 32K 字节。下
8、图是一个 Oracle 数据库典型的物理结构,一个数据库可以包括多个数据文件,一个数据文件内又包含多个数据块;ROWIDROWID 是每条记录在数据库中的唯一标识,通过 ROWID 可以直接定位记录到对应的文件号及数据块位置。ROWID 内容包括文件号、对像号、数据块号、记录槽号,如下图所示:三、数据库访问优化法则详解1、减少数据访问1.1、创建并使用正确的索引数据库索引的原理非常简单,但在复杂的表中真正能正确使用索引的人很少,即使是专业的 DBA 也不一定能完全做到最优。索引会大大增加表记录的 DML(INSERT,UPDATE,DELETE)开销,正确的索引可以让性能提升 100,1000
9、 倍以上,不合理的索引也可能会让性能下降 100 倍,因此在一个表中创建什么样的索引需要平衡各种业务需求。索引常见问题:索引有哪些种类?常见的索引有 B-TREE 索引、位图索引、全文索引,位图索引一般用于数据仓库应用,全文索引由于使用较少,这里不深入介绍。B-TREE 索引包括很多扩展类型,如组合索引、反向索引、函数索引等等,以下是 B-TREE 索引的简单介绍:B-TREE 索引也称为平衡树索引(Balance Tree),它是一种按字段排好序的树形目录结构,主要用于提升查询性能和唯一约束支持。B-TREE 索引的内容包括根节点、分支节点、叶子节点。叶子节点内容:索引字段内容+表记录 RO
10、WID根节点,分支节点内容:当一个数据块中不能放下所有索引字段数据时,就会形成树形的根节点或分支节点,根节点与分支节点保存了索引树的顺序及各层级间的引用关系。一个普通的 BTREE 索引结构示意图如下所示:如果我们把一个表的内容认为是一本字典,那索引就相当于字典的目录,如下图所示:图中是一个字典按部首+笔划数的目录,相当于给字典建了一个按部首+ 笔划的组合索引。一个表中可以建多个索引,就如一本字典可以建多个目录一样(按拼音、笔划、部首等等)。一个索引也可以由多个字段组成,称为组合索引,如上图就是一个按部首+笔划的组合目录。SQL 什么条件会使用索引?当字段上建有索引时,通常以下情况会使用索引:
11、INDEX_COLUMN = ?INDEX_COLUMN ?INDEX_COLUMN = ?INDEX_COLUMN ?INDEX_COLUMN not in (?,?,.,?)不等于操作不能使用索引function(INDEX_COLUMN) = ?INDEX_COLUMN + 1 = ?INDEX_COLUMN | a = ?经过普通运算或函数运算后的索引字段不能使用索引INDEX_COLUMN like %|?INDEX_COLUMN like %|?|%含前导模糊查询的 Like 语法不能使用索引INDEX_COLUMN is null B-TREE 索引里不保存字段为 NULL 值记
12、录,因此 IS NULL 不能使用索引NUMBER_INDEX_COLUMN=12345CHAR_INDEX_COLUMN=12345Oracle 在做数值比较时需要将两边的数据转换成同一种数据类型,如果两边数据类型不同时会对字段值隐式转换,相当于加了一层函数处理,所以不能使用索引。a.INDEX_COLUMN=a.COLUMN_1 给索引查询的值应是已知数据,不能是未知字段值。注:经过函数运算字段的字段要使用可以使用函数索引,这种需求建议与DBA 沟通。有时候我们会使用多个字段的组合索引,如果查询条件中第一个字段不能使用索引,那整个查询也不能使用索引如:我们 company 表建了一个 id
13、+name 的组合索引,以下 SQL 是不能使用索引的Select * from company where name=?Oracle9i 后引入了一种 index skip scan 的索引方式来解决类似的问题,但是通过 index skip scan 提高性能的条件比较特殊,使用不好反而性能会更差。我们一般在什么字段上建索引?这是一个非常复杂的话题,需要对业务及数据充分分析后再能得出结果。主键及外键通常都要有索引,其它需要建索引的字段应满足以下条件:1、字段出现在查询条件中,并且查询条件可以使用索引;2、语句执行频率高,一天会有几千次以上;3、通过字段条件可筛选的记录集很小,那数据筛选比例
14、是多少才适合?这个没有固定值,需要根据表数据量来评估,以下是经验公式,可用于快速评估:小表( 记录数小于 10000 行的表 ):筛选比例10;数据访问开销=索引 IO+索引全部记录结果对应的表数据 IO采用 rowid 分页语法优化原理是通过纯索引找出分页记录的 ROWID,再通过 ROWID 回表返回数据,要求内层查询和排序字段全在索引里。create index myindex on product(company_id,status);select b.* from (select * from (select a.*,rownum rn from (select rowid rid,
15、status from product a where company_id=? order by status) awhere rownum10) a, product bwhere a.rid=b.rowid;数据访问开销=索引 IO+索引分页结果对应的表数据 IO实例:一个公司产品有 1000 条记录,要分页取其中 20 个产品,假设访问公司索引需要 50 个 IO,2 条记录需要 1 个表数据 IO。那么按第一种 ROWNUM 分页写法,需要 550(50+1000/2)个 IO,按第二种ROWID 分页写法,只需要 60 个 IO(50+20/2);2.2、只返回需要的字段通过去除不
16、必要的返回字段可以提高性能,例:调整前:select * from product where company_id=?;调整后:select id,name from product where company_id=?;优点:1、减少数据在网络上传输开销2、减少服务器数据处理开销3、减少客户端内存占用4、字段变更时提前发现问题,减少程序 BUG5、如果访问的所有字段刚好在一个索引里面,则可以使用纯索引访问提高性能。缺点:增加编码工作量由于会增加一些编码工作量,所以一般需求通过开发规范来要求程序员这么做,否则等项目上线后再整改工作量更大。如 果你的查询表中有大字段或内容较多的字段,如备注信息
17、、文件内容等等,那在查询表时一定要注意这方面的问题,否则可能会带来严重的性能问题。如果表经常要 查询并且请求大内容字段的概率很低,我们可以采用分表处理,将一个大表分拆成两个一对一的关系表,将不常用的大内容字段放在一张单独的表中。如一张存储上 传文件的表:T_FILE(ID,FILE_NAME,FILE_SIZE,FILE_TYPE,FILE_CONTENT)我们可以分拆成两张一对一的关系表:T_FILE(ID,FILE_NAME,FILE_SIZE,FILE_TYPE)T_FILECONTENT(ID, FILE_CONTENT)通过这种分拆,可以大大提少 T_FILE 表的单条记录及总大小,
18、这样在查询T_FILE 时性能会更好,当需要查询 FILE_CONTENT 字段内容时再访问T_FILECONTENT 表。3、减少交互次数3.1、batch DML数据库访问框架一般都提供了批量提交的接口,jdbc 支持 batch 的提交处理方法,当你一次性要往一个表中插入 1000 万条数据时,如果采用普通的executeUpdate 处理,那么和服务器交互次数为 1000 万次,按每秒钟可以向数据库服务器提交 10000 次估算,要完成所有工作需要 1000 秒。如果采用批量提交模式,1000 条提交一次,那么和服务器交互次数为 1 万次,交互次数大大减少。采用 batch 操作一般不
19、会减少很多数据库服务器的物理 IO,但是会大大减少客户端与服务端的交互次数,从而减少了多次发起的网络延时开销,同时也会降低数据库的 CPU 开销。假设要向一个普通表插入 1000 万数据,每条记录大小为 1K 字节,表上没有任何索引,客户端与数据库服务器网络是 100Mbps,以下是根据现在一般计算机能力估算的各种 batch 大小性能对比值:单位:ms No batchBatch=10 Batch=100 Batch=1000 Batch=10000服务器事务处理时间0.1 0.1 0.1 0.1 0.1服务器 IO 处理时间0.02 0.2 2 20 200网络交互发起时间 0.1 0.1
20、 0.1 0.1 0.1网络数据传输时间 0.01 0.1 1 10 100小计 0.23 0.5 3.2 30.2 300.2平均每条记录处理时间 0.23 0.05 0.032 0.0302 0.03002从上可以看出,Insert 操作加大 Batch 可以对性能提高近 8 倍性能,一般根据主键的 Update 或 Delete 操作也可能提高 2-3 倍性能,但不如 Insert 明显,因为 Update 及 Delete 操作可能有比较大的开销在物理 IO 访问。以上仅是理论计算值,实际情况需要根据具体环境测量。3.2、In List很多时候我们需要按一些 ID 查询数据库记录,我们
21、可以采用一个 ID 一个请求发给数据库,如下所示:for :var in ids do beginselect * from mytable where id=:var;end;我们也可以做一个小的优化, 如下所示,用 ID INLIST 的这种方式写 SQL:select * from mytable where id in(:id1,id2,.,idn);通过这样处理可以大大减少 SQL 请求的数量,从而提高性能。那如果有10000 个 ID,那是不是全部放在一条 SQL 里处理呢?答案肯定是否定的。首先大部份数据库都会有 SQL 长度和 IN 里个数的限制,如 ORACLE 的 IN 里
22、就不允许超过 1000 个值。另外当前数据库一般都是采用基于成本的优化规则,当 IN 数量达到一定值时有可能改变 SQL 执行计划,从索引访问变成全表访问,这将使性能急剧变化。随着 SQL 中 IN 的里面的值个数增加,SQL 的执行计划会更复杂,占用的内存将会变大,这将会增加服务器 CPU 及内存成本。评估在 IN 里面一次放多少个值还需要考虑应用服务器本地内存的开销,有并发访问时要计算本地数据使用周期内的并发上限,否则可能会导致内存溢出。综合考虑,一般 IN 里面的值个数超过 20 个以后性能基本没什么太大变化,也特别说明不要超过 100,超过后可能会引起执行计划的不稳定性及增加数据库 C
23、PU 及内存成本,这个需要专业 DBA 评估。3.3、设置 Fetch Size当我们采用 select 从数据库查询数据时,数据默认并不是一条一条返回给客户端的,也不是一次全部返回客户端的,而是根据客户端 fetch_size 参数处理,每次只返回 fetch_size 条记录,当客户端游标遍历到尾部时再从服务端取数据,直到最后全部传送完成。所以如果我们要从服务端一次取大量数据时,可以加大 fetch_size,这样可以减少结果数据传输的交互次数及服务器数据准备时间,提高性能。以下是 jdbc 测试的代码,采用本地数据库,表缓存在数据库 CACHE 中,因此没有网络连接及磁盘 IO 开销,客
24、户端只遍历游标,不做任何处理,这样更能体现 fetch 参数的影响:String vsql =“select * from t_employee“;PreparedStatement pstmt = conn.prepareStatement(vsql,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);pstmt.setFetchSize(1000);ResultSet rs = pstmt.executeQuery(vsql);int cnt = rs.getMetaData().getColumnCount();Object o
25、;while (rs.next() for (int i = 1; i select * from employee3.4、使用存储过程大型数据库一般都支持存储过程,合理的利用存储过程也可以提高系统性能。如你有一个业务需要将 A 表的数据做一些加工然后更新到 B 表中,但是又不可能一条 SQL 完成,这时你需要如下 3 步操作:a:将 A 表数据全部取出到客户端;b:计算出要更新的数据;c:将计算结果更新到 B 表。如果采用存储过程你可以将整个业务逻辑封装在存储过程里,然后在客户端直接调用存储过程处理,这样可以减少网络交互的成本。当然,存储过程也并不是十全十美,存储过程有以下缺点:a、不可移植
26、性,每种数据库的内部编程语法都不太相同,当你的系统需要兼容多种数据库时最好不要用存储过程。b、学习成本高,DBA 一般都擅长写存储过程,但并不是每个程序员都能写好存储过程,除非你的团队有较多的开发人员熟悉写存储过程,否则后期系统维护会产生问题。c、业务逻辑多处存在,采用存储过程后也就意味着你的系统有一些业务逻辑不是在应用程序里处理,这种架构会增加一些系统维护和调试成本。d、存储过程和常用应用程序语言不一样,它支持的函数及语法有可能不能满足需求,有些逻辑就只能通过应用程序处理。e、如果存储过程中有复杂运算的话,会增加一些数据库服务端的处理成本,对于集中式数据库可能会导致系统可扩展性问题。f、为了
27、提高性能,数据库会把存储过程代码编译成中间运行代码(类似于java 的 class 文件),所以更像静态语言。当存储过程引用的对像(表、视图等等)结构改变后,存储过程需要重新编译才能生效,在 24*7 高并发应用场景,一般都是在线变更结构的,所以在变更的瞬间要同时编译存储过程,这可能会导致数据库瞬间压力上升引起故障(Oracle 数据库就存在这样的问题)。个人观点:普通业务逻辑尽量不要使用存储过程,定时性的 ETL 任务或报表统计函数可以根据团队资源情况采用存储过程处理。3.5、优化业务逻辑要通过优化业务逻辑来提高性能是比较困难的,这需要程序员对所访问的数据及业务流程非常清楚。举一个案例:某移
28、动公司推出优惠套参,活动对像为 VIP 会员并且 2010 年 1,2,3 月平均话费 20 元以上的客户。那我们的检测逻辑为:select avg(money) as avg_money from bill where phone_no=13988888888 and date between 201001 and 201003;select vip_flag from member where phone_no=13988888888;if avg_money20 and vip_flag=true thenbegin执行套参();end;如果我们修改业务逻辑为:select avg(mo
29、ney) as avg_money from bill where phone_no=13988888888 and date between 201001 and 201003;if avg_money20 thenbeginselect vip_flag from member where phone_no=13988888888;if vip_flag=true thenbegin执行套参();end;end;通过这样可以减少一些判断 vip_flag 的开销,平均话费 20 元以下的用户就不需要再检测是否 VIP 了。如果程序员分析业务,VIP 会员比例为 1%,平均话费 20 元以上
30、的用户比例为 90%,那我们改成如下:select vip_flag from member where phone_no=13988888888;if vip_flag=true thenbeginselect avg(money) as avg_money from bill where phone_no=13988888888 and date between 201001 and 201003;if avg_money20 thenbegin执行套参();end;end;这样就只有 1%的 VIP 会员才会做检测平均话费,最终大大减少了 SQL 的交互次数。以上只是一个简单的示例,实际
31、的业务总是比这复杂得多,所以一般只是高级程序员更容易做出优化的逻辑,但是我们需要有这样一种成本优化的意识。3.6、使用 ResultSet 游标处理记录现在大部分 Java 框架都是通过 jdbc 从数据库取出数据,然后装载到一个list 里再处理,list 里可能是业务 Object,也可能是 hashmap。由于 JVM 内存一般都小于 4G,所以不可能一次通过 sql 把大量数据装载到list 里。为了完成功能,很多程序员喜欢采用分页的方法处理,如一次从数据库取 1000 条记录,通过多次循环搞定,保证不会引起 JVM Out of memory 问题。以下是实现此功能的代码示例,t_e
32、mployee 表有 10 万条记录,设置分页大小为 1000:d1 = Calendar.getInstance().getTime();vsql = “select count(*) cnt from t_employee“;pstmt = conn.prepareStatement(vsql);ResultSet rs = pstmt.executeQuery();Integer cnt = 0;while (rs.next() cnt = rs.getInt(“cnt“);Integer lastid=0;Integer pagesize=1000;System.out.println
33、(“cnt:“ + cnt);String vsql = “select count(*) cnt from t_employee“;PreparedStatement pstmt = conn.prepareStatement(vsql);ResultSet rs = pstmt.executeQuery();Integer cnt = 0;while (rs.next() cnt = rs.getInt(“cnt“);Integer lastid = 0;Integer pagesize = 1000;System.out.println(“cnt:“ + cnt);for (int i
34、= 0; i ? order by id) where rownum20如果这里的 a 字段不能通过索引比较,那数据库会将字段与 in 里面的每个值都进行比较运算,如果记录数有上万以上,会明显感觉到 SQL 的 CPU 开销加大,这个情况有两种解决方式:a、 将 in 列表里面的数据放入一张中间小表,采用两个表 Hash Join 关联的方式处理;b、 采用 str2varList 方法将字段串列表转换一个临时表处理,关于str2varList 方法可以在网上直接查询,这里不详细介绍。以上两种解决方案都需要与中间表 Hash Join 的方式才能提高性能,如果采用了 Nested Loop 的
35、连接方式性能会更差。如果发现我们的系统 IO 没问题但是 CPU 负载很高,就有可能是上面的原因,这种情况不太常见,如果遇到了最好能和 DBA 沟通并确认准确的原因。4.4、大量复杂运算在客户端处理什么是复杂运算,一般我认为是一秒钟 CPU 只能做 10 万次以内的运算。如含小数的对数及指数运算、三角函数、3DES 及 BASE64 数据加密算法等等。如果有大量这类函数运算,尽量放在客户端处理,一般 CPU 每秒中也只能处理 1 万-10 万次这样的函数运算,放在数据库内不利于高并发处理。5、利用更多的资源5.1、客户端多进程并行访问多进程并行访问是指在客户端创建多个进程(线程),每个进程建立
36、一个与数据库的连接,然后同时向数据库提交访问请求。当数据库主机资源有空闲时,我们可以采用客户端多进程并行访问的方法来提高性能。如果数据库主机已经很忙时,采用多进程并行访问性能不会提高,反而可能会更慢。所以使用这种方式最好与 DBA 或系统管理员进行沟通后再决定是否采用。例如:我们有 10000 个产品 ID,现在需要根据 ID 取出产品的详细信息,如果单线程访问,按每个 IO 要 5ms 计算,忽略主机 CPU 运算及网络传输时间,我们需要 50s 才能完成任务。如果采用 5 个并行访问,每个进程访问 2000 个 ID,那么10s 就有可能完成任务。那是不是并行数越多越好呢,开 1000 个
37、并行是否只要 50ms 就搞定,答案肯定是否定的,当并行数超过服务器主机资源的上限时性能就不会再提高,如果再增加反而会增加主机的进程间调度成本和进程冲突机率。以下是一些如何设置并行数的基本建议:如果瓶颈在服务器主机,但是主机还有空闲资源,那么最大并行数取主机CPU 核数和主机提供数据服务的磁盘数两个参数中的最小值,同时要保证主机有资源做其它任务。如果瓶颈在客户端处理,但是客户端还有空闲资源,那建议不要增加 SQL的并行,而是用一个进程取回数据后在客户端起多个进程处理即可,进程数根据客户端 CPU 核数计算。如果瓶颈在客户端网络,那建议做数据压缩或者增加多个客户端,采用map reduce 的架
38、构处理。如果瓶颈在服务器网络,那需要增加服务器的网络带宽或者在服务端将数据压缩后再处理了。5.2、数据库并行处理数据库并行处理是指客户端一条 SQL 的请求,数据库内部自动分解成多个进程并行处理,如下图所示:并不是所有的 SQL 都可以使用并行处理,一般只有对表或索引进行全部访问时才可以使用并行。数据库表默认是不打开并行访问,所以需要指定 SQL 并行的提示,如下所示:select /*+parallel(a,4)*/ * from employee; 并行的优点:使用多进程处理,充分利用数据库主机资源(CPU,IO),提高性能。并行的缺点:1、单个会话占用大量资源,影响其它会话,所以只适合在主机负载低时期使用;2、只能采用直接 IO 访问,不能利用缓存数据,所以执行前会触发将脏缓存数据写入磁盘操作。注:1、并行处理在 OLTP 类系统中慎用,使用不当会导致一个会话把主机资源全部占用,而正常事务得不到及时响应,所以一般只是用于数据仓库平台。2、一般对于百万级记录以下的小表采用并行访问性能并不能提高,反而可能会让性能更差。 叶正盛(MKing)2010-12-3发表于 2010 年 12 月 06 日 20:08:00 | 评论( 41 ) | 举报| 收藏