博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
使用Hadoop的MapReduce来完成大表join
阅读量:4185 次
发布时间:2019-05-26

本文共 17794 字,大约阅读时间需要 59 分钟。

我们都知道在数据库里,多个表之间是可以根据某个链接键进行join的,这也是数据库的范式规范,通过主外键的关联,由此来减少数据冗余,提高性能。当然近几年,随着NOSQL的兴起,出现了基于列的的列式数据库,典型的有Hbase,MongonDB,Cassdran,等等,NOSQL数据库弱化了关联,直接将一整条数据,存入一列,以及去掉了数据库的部分事务特性,从而在海量数据面前显得游刃有余,当然,大部分的NOSQL不支持join操作,也没有绝对的必要支持,因为现在,我们完全是把一整条数据存在了一起,虽然多了许多冗余,但也换来了比较高检索性能,扩展性能,可靠性能。但某些业务场景下,我们仍然需要Join操作,这时候怎么办?

如果数据量比较大的情况下,我们可以使用Hadoop的MapReduce来完成大表join,尤其对Hbase的某些表进行join操作,当然我们也可以使用Hive或Pig来完成,其实质在后台还是运行的一个MR程序。
那么,散仙今天就来看下如何使用MapReduce来完成一个join操作,Hadoop的join分为很多种例如;Reduce链接,Map侧链接,半链接和Reduce侧链接+BloomFilter等等,各个链接都有自己特定的应用场景,没有绝对的谁好谁坏。
今天散仙要说的是,基于Reduce侧的链接,原理如下:
1、在Reudce端进行连接。
   在Reudce端进行连接是MapReduce框架进行表之间join操作最为常见的模式,其具体的实现原理如下:
Map端的主要工作:为来自不同表(文件)的key/value对打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。
reduce端的主要工作:在reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在map阶段已经打标志)分开,最后进行笛卡尔只就ok了。
本次的实现是基于hadoop的旧版API+contribu扩展包里的,DataJoin的工具类辅助来完成的,下篇博客,将会给出,基于新版API,独立来完成Reduce侧的连接示例。
现在看下散仙的两个文件的测试数据,一个是a.txt,另一个是b.txt

Java代码  
  1. a文件的数据  
  2.   
  3. 1,三劫散仙,13575468248  
  4. 2,凤舞九天,18965235874  
  5. 3,忙忙碌碌,15986854789  
  6. 4,少林寺方丈,15698745862  
a文件的数据1,三劫散仙,135754682482,凤舞九天,189652358743,忙忙碌碌,159868547894,少林寺方丈,15698745862

Java代码  
  1. b文件的数据  
  2.   
  3. 3,A,99,2013-03-05  
  4. 1,B,89,2013-02-05  
  5. 2,C,69,2013-03-09  
  6. 3,D,56,2013-06-07  
b文件的数据3,A,99,2013-03-051,B,89,2013-02-052,C,69,2013-03-093,D,56,2013-06-07

源码如下:

Java代码  
  1. package com.qin.reducejoin;  
  2.   
  3.   
  4. import java.io.DataInput;    
  5. import java.io.DataOutput;    
  6. import java.io.IOException;    
  7. import java.util.Iterator;    
  8.     
  9. import org.apache.hadoop.conf.Configuration;    
  10. import org.apache.hadoop.conf.Configured;    
  11. import org.apache.hadoop.fs.FileSystem;  
  12. import org.apache.hadoop.fs.Path;    
  13. import org.apache.hadoop.io.Text;    
  14. import org.apache.hadoop.io.Writable;    
  15. import org.apache.hadoop.mapred.FileInputFormat;    
  16. import org.apache.hadoop.mapred.FileOutputFormat;    
  17. import org.apache.hadoop.mapred.JobClient;    
  18. import org.apache.hadoop.mapred.JobConf;    
  19. import org.apache.hadoop.mapred.KeyValueTextInputFormat;    
  20. import org.apache.hadoop.mapred.MapReduceBase;    
  21. import org.apache.hadoop.mapred.Mapper;    
  22. import org.apache.hadoop.mapred.OutputCollector;    
  23. import org.apache.hadoop.mapred.Reducer;    
  24. import org.apache.hadoop.mapred.Reporter;    
  25. import org.apache.hadoop.mapred.TextInputFormat;    
  26. import org.apache.hadoop.mapred.TextOutputFormat;    
  27. import org.apache.hadoop.util.ReflectionUtils;  
  28. import org.apache.hadoop.util.Tool;    
  29. import org.apache.hadoop.util.ToolRunner;    
  30.     
  31. import org.apache.hadoop.contrib.utils.join.DataJoinMapperBase;    
  32. import org.apache.hadoop.contrib.utils.join.DataJoinReducerBase;    
  33. import org.apache.hadoop.contrib.utils.join.TaggedMapOutput;    
  34.   
  35. import com.qin.joinreduceerror.JoinReduce;  
  36.     
  37.   
  38. /*** 
  39.  *  
  40.  * Hadoop1.2的版本,旧版本实现的Reduce侧连接 
  41.  *  
  42.  * @author qindongliang 
  43.  *  
  44.  *    大数据交流群:376932160 
  45.  *  搜索技术交流群:324714439 
  46.  *  
  47.  *  
  48.  */  
  49.   
  50. public class DataJoin extends Configured implements Tool {    
  51.         
  52.       
  53.     /** 
  54.      *  
  55.      * Map实现 
  56.      *  
  57.      * */  
  58.     public static class MapClass extends DataJoinMapperBase {    
  59.             
  60.         /** 
  61.          * 读取输入的文件路径 
  62.          *  
  63.          * **/  
  64.         protected Text generateInputTag(String inputFile) {    
  65.               
  66.             //返回文件路径,做标记  
  67.             return new Text(inputFile);    
  68.         }    
  69.             
  70.           
  71.         /*** 
  72.          * 分组的Key 
  73.          *  
  74.          * **/  
  75.           
  76.         protected Text generateGroupKey(TaggedMapOutput aRecord) {    
  77.             String line = ((Text) aRecord.getData()).toString();    
  78.             String[] tokens = line.split(",");    
  79.             String groupKey = tokens[0];    
  80.             return new Text(groupKey);    
  81.         }    
  82.             
  83.           
  84.           
  85.           
  86.         protected TaggedMapOutput generateTaggedMapOutput(Object value) {    
  87.             TaggedWritable retv = new TaggedWritable((Text) value);    
  88.             retv.setTag(this.inputTag);    
  89.             return retv;    
  90.         }    
  91.     }    
  92.         
  93.     /** 
  94.      *  
  95.      * Reduce进行笛卡尔积 
  96.      *  
  97.      * **/  
  98.     public static class Reduce extends DataJoinReducerBase {    
  99.             
  100.           
  101.         /*** 
  102.          * 笛卡尔积 
  103.          *  
  104.          * */  
  105.         protected TaggedMapOutput combine(Object[] tags, Object[] values) {    
  106.             if (tags.length < 2return null;      
  107.             String joinedStr = "";     
  108.             for (int i=0; i<values.length; i++) {    
  109.                 if (i > 0) {joinedStr += ",";}    
  110.                 TaggedWritable tw = (TaggedWritable) values[i];    
  111.                 String line = ((Text) tw.getData()).toString();    
  112.                 String[] tokens = line.split(","2);    
  113.                 joinedStr += tokens[1];    
  114.             }    
  115.             TaggedWritable retv = new TaggedWritable(new Text(joinedStr));    
  116.             retv.setTag((Text) tags[0]);     
  117.             return retv;    
  118.         }    
  119.     }    
  120.         
  121.     /** 
  122.      *  
  123.      * 自定义的输出类型 
  124.      *  
  125.      * ***/  
  126.     public static class TaggedWritable extends TaggedMapOutput {    
  127.         
  128.         private Writable data;    
  129.             
  130.         /** 
  131.          * 注意加上构造方法 
  132.          *  
  133.          * */  
  134.         public TaggedWritable() {  
  135.             // TODO Auto-generated constructor stub  
  136.         }  
  137.           
  138.         public TaggedWritable(Writable data) {    
  139.             this.tag = new Text("");    
  140.             this.data = data;    
  141.         }    
  142.             
  143.         public Writable getData() {    
  144.             return data;    
  145.         }    
  146.             
  147.         public void write(DataOutput out) throws IOException {    
  148.             this.tag.write(out);    
  149.             //此行代码很重要  
  150.             out.writeUTF(this.data.getClass().getName());  
  151.              
  152.             this.data.write(out);   
  153.               
  154.         }    
  155.             
  156.         public void readFields(DataInput in) throws IOException {    
  157.             this.tag.readFields(in);    
  158.               //加入此部分代码,否则,可能报空指针异常  
  159.             String temp=in.readUTF();  
  160.             if (this.data == null|| !this.data.getClass().getName().equals(temp)) {  
  161.                     try {  
  162.                     this.data = (Writable) ReflectionUtils.newInstance(  
  163.                     Class.forName(temp), null);  
  164.                     } catch (ClassNotFoundException e) {  
  165.                     e.printStackTrace();  
  166.                     }  
  167.                     }  
  168.             this.data.readFields(in);    
  169.         }    
  170.     }    
  171.         
  172.     public int run(String[] args) throws Exception {    
  173.         Configuration conf = getConf();    
  174.             
  175.         JobConf job = new JobConf(conf, DataJoin.class);    
  176.           
  177.         job.set("mapred.job.tracker","192.168.75.130:9001");  
  178.             读取person中的数据字段  
  179.              job.setJar("tt.jar");  
  180.         job.setJarByClass(DataJoin.class);  
  181.             System.out.println("模式:  "+job.get("mapred.job.tracker"));;  
  182.                
  183.                
  184.                
  185.                
  186.         String path="hdfs://192.168.75.130:9000/root/outputjoindb";  
  187.         FileSystem fs=FileSystem.get(conf);  
  188.         Path p=new Path(path);  
  189.         if(fs.exists(p)){  
  190.             fs.delete(p, true);  
  191.             System.out.println("输出路径存在,已删除!");  
  192.         }  
  193.        
  194.           
  195.           
  196.         Path in = new Path("hdfs://192.168.75.130:9000/root/inputjoindb");    
  197.       //  Path out = new Path(args[1]);    
  198.         FileInputFormat.setInputPaths(job, in);    
  199.         FileOutputFormat.setOutputPath(job, p);    
  200.             
  201.         job.setJobName("cee");    
  202.         job.setMapperClass(MapClass.class);    
  203.         job.setReducerClass(Reduce.class);    
  204.             
  205.         job.setInputFormat(TextInputFormat.class);    
  206.         job.setOutputFormat(TextOutputFormat.class);    
  207.         job.setOutputKeyClass(Text.class);    
  208.         job.setOutputValueClass(TaggedWritable.class);    
  209.         job.set("mapred.textoutputformat.separator"",");    
  210.             
  211.         JobClient.runJob(job);     
  212.         return 0;    
  213.     }    
  214.         
  215.     public static void main(String[] args) throws Exception {     
  216.           
  217.           
  218.           
  219.         int res = ToolRunner.run(new Configuration(),    
  220.                                  new DataJoin(),    
  221.                                  args);    
  222.             
  223.         System.exit(res);    
  224.     }    
  225. }    
package com.qin.reducejoin;import java.io.DataInput;  import java.io.DataOutput;  import java.io.IOException;  import java.util.Iterator;    import org.apache.hadoop.conf.Configuration;  import org.apache.hadoop.conf.Configured;  import org.apache.hadoop.fs.FileSystem;import org.apache.hadoop.fs.Path;  import org.apache.hadoop.io.Text;  import org.apache.hadoop.io.Writable;  import org.apache.hadoop.mapred.FileInputFormat;  import org.apache.hadoop.mapred.FileOutputFormat;  import org.apache.hadoop.mapred.JobClient;  import org.apache.hadoop.mapred.JobConf;  import org.apache.hadoop.mapred.KeyValueTextInputFormat;  import org.apache.hadoop.mapred.MapReduceBase;  import org.apache.hadoop.mapred.Mapper;  import org.apache.hadoop.mapred.OutputCollector;  import org.apache.hadoop.mapred.Reducer;  import org.apache.hadoop.mapred.Reporter;  import org.apache.hadoop.mapred.TextInputFormat;  import org.apache.hadoop.mapred.TextOutputFormat;  import org.apache.hadoop.util.ReflectionUtils;import org.apache.hadoop.util.Tool;  import org.apache.hadoop.util.ToolRunner;    import org.apache.hadoop.contrib.utils.join.DataJoinMapperBase;  import org.apache.hadoop.contrib.utils.join.DataJoinReducerBase;  import org.apache.hadoop.contrib.utils.join.TaggedMapOutput;  import com.qin.joinreduceerror.JoinReduce;  /*** *  * Hadoop1.2的版本,旧版本实现的Reduce侧连接 *  * @author qindongliang *  *    大数据交流群:376932160 *  搜索技术交流群:324714439 *  *  */public class DataJoin extends Configured implements Tool {        		/**	 * 	 * Map实现	 * 	 * */    public static class MapClass extends DataJoinMapperBase {                	/**    	 * 读取输入的文件路径    	 *     	 * **/        protected Text generateInputTag(String inputFile) {                      	//返回文件路径,做标记            return new Text(inputFile);          }                            /***         * 分组的Key         *          * **/                protected Text generateGroupKey(TaggedMapOutput aRecord) {              String line = ((Text) aRecord.getData()).toString();              String[] tokens = line.split(",");              String groupKey = tokens[0];              return new Text(groupKey);          }                                            protected TaggedMapOutput generateTaggedMapOutput(Object value) {              TaggedWritable retv = new TaggedWritable((Text) value);              retv.setTag(this.inputTag);              return retv;          }      }            /**     *      * Reduce进行笛卡尔积     *      * **/    public static class Reduce extends DataJoinReducerBase {                	    	/***    	 * 笛卡尔积    	 *     	 * */        protected TaggedMapOutput combine(Object[] tags, Object[] values) {              if (tags.length < 2) return null;                String joinedStr = "";               for (int i=0; i
0) {joinedStr += ",";} TaggedWritable tw = (TaggedWritable) values[i]; String line = ((Text) tw.getData()).toString(); String[] tokens = line.split(",", 2); joinedStr += tokens[1]; } TaggedWritable retv = new TaggedWritable(new Text(joinedStr)); retv.setTag((Text) tags[0]); return retv; } } /** * * 自定义的输出类型 * * ***/ public static class TaggedWritable extends TaggedMapOutput { private Writable data; /** * 注意加上构造方法 * * */ public TaggedWritable() { // TODO Auto-generated constructor stub } public TaggedWritable(Writable data) { this.tag = new Text(""); this.data = data; } public Writable getData() { return data; } public void write(DataOutput out) throws IOException { this.tag.write(out); //此行代码很重要 out.writeUTF(this.data.getClass().getName()); this.data.write(out); } public void readFields(DataInput in) throws IOException { this.tag.readFields(in); //加入此部分代码,否则,可能报空指针异常 String temp=in.readUTF(); if (this.data == null|| !this.data.getClass().getName().equals(temp)) { try { this.data = (Writable) ReflectionUtils.newInstance( Class.forName(temp), null); } catch (ClassNotFoundException e) { e.printStackTrace(); } } this.data.readFields(in); } } public int run(String[] args) throws Exception { Configuration conf = getConf(); JobConf job = new JobConf(conf, DataJoin.class); job.set("mapred.job.tracker","192.168.75.130:9001"); 读取person中的数据字段 job.setJar("tt.jar"); job.setJarByClass(DataJoin.class); System.out.println("模式: "+job.get("mapred.job.tracker"));; String path="hdfs://192.168.75.130:9000/root/outputjoindb"; FileSystem fs=FileSystem.get(conf); Path p=new Path(path); if(fs.exists(p)){ fs.delete(p, true); System.out.println("输出路径存在,已删除!"); } Path in = new Path("hdfs://192.168.75.130:9000/root/inputjoindb"); // Path out = new Path(args[1]); FileInputFormat.setInputPaths(job, in); FileOutputFormat.setOutputPath(job, p); job.setJobName("cee"); job.setMapperClass(MapClass.class); job.setReducerClass(Reduce.class); job.setInputFormat(TextInputFormat.class); job.setOutputFormat(TextOutputFormat.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(TaggedWritable.class); job.set("mapred.textoutputformat.separator", ","); JobClient.runJob(job); return 0; } public static void main(String[] args) throws Exception { int res = ToolRunner.run(new Configuration(), new DataJoin(), args); System.exit(res); } }

运行,日志

Java代码  
  1. 模式:  192.168.75.130:9001  
  2. 输出路径存在,已删除!  
  3. INFO - NativeCodeLoader.<clinit>(43) | Loaded the native-hadoop library  
  4. WARN - LoadSnappy.<clinit>(46) | Snappy native library not loaded  
  5. INFO - FileInputFormat.listStatus(199) | Total input paths to process : 2  
  6. INFO - JobClient.monitorAndPrintJob(1380) | Running job: job_201404222310_0025  
  7. INFO - JobClient.monitorAndPrintJob(1393) |  map 0% reduce 0%  
  8. INFO - JobClient.monitorAndPrintJob(1393) |  map 33% reduce 0%  
  9. INFO - JobClient.monitorAndPrintJob(1393) |  map 100% reduce 0%  
  10. INFO - JobClient.monitorAndPrintJob(1393) |  map 100% reduce 33%  
  11. INFO - JobClient.monitorAndPrintJob(1393) |  map 100% reduce 100%  
  12. INFO - JobClient.monitorAndPrintJob(1448) | Job complete: job_201404222310_0025  
  13. INFO - Counters.log(585) | Counters: 30  
  14. INFO - Counters.log(587) |   Job Counters   
  15. INFO - Counters.log(589) |     Launched reduce tasks=1  
  16. INFO - Counters.log(589) |     SLOTS_MILLIS_MAPS=14335  
  17. INFO - Counters.log(589) |     Total time spent by all reduces waiting after reserving slots (ms)=0  
  18. INFO - Counters.log(589) |     Total time spent by all maps waiting after reserving slots (ms)=0  
  19. INFO - Counters.log(589) |     Launched map tasks=3  
  20. INFO - Counters.log(589) |     Data-local map tasks=3  
  21. INFO - Counters.log(589) |     SLOTS_MILLIS_REDUCES=9868  
  22. INFO - Counters.log(587) |   File Input Format Counters   
  23. INFO - Counters.log(589) |     Bytes Read=207  
  24. INFO - Counters.log(587) |   File Output Format Counters   
  25. INFO - Counters.log(589) |     Bytes Written=172  
  26. INFO - Counters.log(587) |   FileSystemCounters  
  27. INFO - Counters.log(589) |     FILE_BYTES_READ=837  
  28. INFO - Counters.log(589) |     HDFS_BYTES_READ=513  
  29. INFO - Counters.log(589) |     FILE_BYTES_WRITTEN=221032  
  30. INFO - Counters.log(589) |     HDFS_BYTES_WRITTEN=172  
  31. INFO - Counters.log(587) |   Map-Reduce Framework  
  32. INFO - Counters.log(589) |     Map output materialized bytes=849  
  33. INFO - Counters.log(589) |     Map input records=8  
  34. INFO - Counters.log(589) |     Reduce shuffle bytes=849  
  35. INFO - Counters.log(589) |     Spilled Records=16  
  36. INFO - Counters.log(589) |     Map output bytes=815  
  37. INFO - Counters.log(589) |     Total committed heap usage (bytes)=496644096  
  38. INFO - Counters.log(589) |     CPU time spent (ms)=2080  
  39. INFO - Counters.log(589) |     Map input bytes=187  
  40. INFO - Counters.log(589) |     SPLIT_RAW_BYTES=306  
  41. INFO - Counters.log(589) |     Combine input records=0  
  42. INFO - Counters.log(589) |     Reduce input records=8  
  43. INFO - Counters.log(589) |     Reduce input groups=4  
  44. INFO - Counters.log(589) |     Combine output records=0  
  45. INFO - Counters.log(589) |     Physical memory (bytes) snapshot=623570944  
  46. INFO - Counters.log(589) |     Reduce output records=4  
  47. INFO - Counters.log(589) |     Virtual memory (bytes) snapshot=2908262400  
  48. INFO - Counters.log(589) |     Map output records=8  
模式:  192.168.75.130:9001输出路径存在,已删除!INFO - NativeCodeLoader.
(43) | Loaded the native-hadoop libraryWARN - LoadSnappy.
(46) | Snappy native library not loadedINFO - FileInputFormat.listStatus(199) | Total input paths to process : 2INFO - JobClient.monitorAndPrintJob(1380) | Running job: job_201404222310_0025INFO - JobClient.monitorAndPrintJob(1393) | map 0% reduce 0%INFO - JobClient.monitorAndPrintJob(1393) | map 33% reduce 0%INFO - JobClient.monitorAndPrintJob(1393) | map 100% reduce 0%INFO - JobClient.monitorAndPrintJob(1393) | map 100% reduce 33%INFO - JobClient.monitorAndPrintJob(1393) | map 100% reduce 100%INFO - JobClient.monitorAndPrintJob(1448) | Job complete: job_201404222310_0025INFO - Counters.log(585) | Counters: 30INFO - Counters.log(587) | Job Counters INFO - Counters.log(589) | Launched reduce tasks=1INFO - Counters.log(589) | SLOTS_MILLIS_MAPS=14335INFO - Counters.log(589) | Total time spent by all reduces waiting after reserving slots (ms)=0INFO - Counters.log(589) | Total time spent by all maps waiting after reserving slots (ms)=0INFO - Counters.log(589) | Launched map tasks=3INFO - Counters.log(589) | Data-local map tasks=3INFO - Counters.log(589) | SLOTS_MILLIS_REDUCES=9868INFO - Counters.log(587) | File Input Format Counters INFO - Counters.log(589) | Bytes Read=207INFO - Counters.log(587) | File Output Format Counters INFO - Counters.log(589) | Bytes Written=172INFO - Counters.log(587) | FileSystemCountersINFO - Counters.log(589) | FILE_BYTES_READ=837INFO - Counters.log(589) | HDFS_BYTES_READ=513INFO - Counters.log(589) | FILE_BYTES_WRITTEN=221032INFO - Counters.log(589) | HDFS_BYTES_WRITTEN=172INFO - Counters.log(587) | Map-Reduce FrameworkINFO - Counters.log(589) | Map output materialized bytes=849INFO - Counters.log(589) | Map input records=8INFO - Counters.log(589) | Reduce shuffle bytes=849INFO - Counters.log(589) | Spilled Records=16INFO - Counters.log(589) | Map output bytes=815INFO - Counters.log(589) | Total committed heap usage (bytes)=496644096INFO - Counters.log(589) | CPU time spent (ms)=2080INFO - Counters.log(589) | Map input bytes=187INFO - Counters.log(589) | SPLIT_RAW_BYTES=306INFO - Counters.log(589) | Combine input records=0INFO - Counters.log(589) | Reduce input records=8INFO - Counters.log(589) | Reduce input groups=4INFO - Counters.log(589) | Combine output records=0INFO - Counters.log(589) | Physical memory (bytes) snapshot=623570944INFO - Counters.log(589) | Reduce output records=4INFO - Counters.log(589) | Virtual memory (bytes) snapshot=2908262400INFO - Counters.log(589) | Map output records=8

运行结果,如下图所示:
可以看出,MR正确的完成了join操作,需要注意的是Reduce侧连接的不足之处:
之所以会存在reduce join这种方式,我们可以很明显的看出原:因为整体数据被分割了,每个map task只处理一部分数据而不能够获取到所有需要的join字段,因此我们需要在讲join key作为reduce端的分组将所有join key相同的记录集中起来进行处理,所以reduce join这种方式就出现了。这种方式的缺点很明显就是会造成map和reduce端也就是shuffle阶段出现大量的数据传输,效率很低。
另外一点需要注意的是,散仙在eclipse里进行调试,Local模式下会报异常,建议提交到hadoop的测试集群上进行测试。

转载地址:http://vfjoi.baihongyu.com/

你可能感兴趣的文章
如何有效又圆满地完成软件测试?
查看>>
技术布道——全程软件测试
查看>>
互联网:从流量经营到服务经营
查看>>
喜讯——软件测试在大学里开始红火
查看>>
再谈百度
查看>>
需要更多的 “教练式的领导”
查看>>
《软件质量保证和管理》电子课件
查看>>
享受生活、善待自己-奇迹就在身边
查看>>
基于过程的软件测试全景图 (2)
查看>>
世界是平的吗?——博客周年有感
查看>>
软件测试内容全貌——全景图 (1)
查看>>
面对当今的研究生教育——只有无奈
查看>>
如何定义测试用例的质量标准?
查看>>
《软件过程管理》电子课件
查看>>
软件本地化的质量不容乐观
查看>>
并非中庸之道——我看开源与微软
查看>>
做事的态度与工作态度
查看>>
一次痛苦的真实经历——感慨国产软件的质量
查看>>
走在技术和商业之间的平衡木上(感想英雄会)
查看>>
“七人分粥”- 介绍新书《软件过程管理》
查看>>