TinyDbRouter开源喽~~~

前面有过一篇文章介绍TinyDbRouter,但是当时没有开出来,主要原因是:1偶的粉丝数太少,期望到100的时候,纪念性的发布这个重量级框架,另外一个原因是当时有个编译问题没有完美的解决,偶担心同学们使用的时候不方便--其实偶也不方便,尤其是发布和测试的时候。

现在粉够100了,那个编译问题也顺利的解决了,OK,没有什么理由不快些把它开放给大家。

前面偶起的名字是TinyDBCluster,后来由于有同学们反应说这个与数据库集群歧义,因此还是改成TinyDBRouter了,如果看到两个名字,请把它们当成一样的,后面就专门用TinyDBRouter

其实在开发TinyDbRouter之前,偶主要是想找一个比较合适的数据库分区、分表方案,为此也学习了各种实现方案,比如就了解过routing4db,偶也专门做了对比,当然由于对routing4db的了解毕竟有不足,因此可能有许多不准确的地方;另外也对淘宝系的tddl做了相关研究,但是最后偶还是决定自己尝试写一下,当然写完之后感觉还是不错的,因此才有现在开源的TinyDbRouter。

好的,上面是一些背景情况,现在言归正传,我们正式说框架。

关于Tiny DBRouter的原理性文章,请移步查阅,这里主要讲使用。

要想使用Tiny DBRouter,很简单,首先搞清楚是jdbc3(JDK1.5)还是jdbc4(JDK1.6及以上)的规范。

然后选择对应的Maven坐标:

?


1

2

3

4

5

<dependency>

    <groupId>org.tinygroup</groupId>

    <artifactId>org.tinygroup.dbrouterjdbc3</artifactId>

    <version>0.1.0-SNAPSHOT</version>

</dependency>

或者

?


1

2

3

4

5

<dependency>

    <groupId>org.tinygroup</groupId>

    <artifactId>org.tinygroup.dbrouterjdbc4</artifactId>

    <version>0.1.0-SNAPSHOT</version>

</dependency>

之所以是SNAPSHOT版本,是因为Tiny框架的升级是阶段性升级的,过一段时间就会变成0.0.13正式版本。

当把相关jar包下载到本地之后,接下来就是配置分区分表数据源了。

我们拿一个例子来说明:

differentSchemaAggregate.xml

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

<routers>

    <router id="aggregate" user-name="luog" password="123456"

             key-generator-class="org.tinygroup.dbrouter.impl.keygenerator.RouterKeyGeneratorLong">

        <key-generator-config increment="1" step="100" data-source-id="ds0"/>

        <data-source-configs>

            <data-source-config id="ds0" driver="com.mysql.jdbc.Driver"

                                user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test0"

                                test-sql=""/>

            <data-source-config id="ds1" driver="com.mysql.jdbc.Driver"

                                user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test1"

                                test-sql=""/>

            <data-source-config id="ds2" driver="com.mysql.jdbc.Driver"

                                user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test2"

                                test-sql=""/>

        </data-source-configs>

        <partitions>

            <partition id="abc" mode="2">

                <partition-rules>

                    <partition-rule

                            class="org.tinygroup.dbrouter.impl.partionrule.PartionRuleByTableName"

                            table-name="score"/>

                </partition-rules>

                <shards>

                    <shard id="shard0" data-source-id="ds0">

                        <shard-rules>

                            <shard-rule

                                    class="org.tinygroup.dbrouter.impl.shardrule.ShardRuleByIdDifferentSchema"

                                    table-name="score" primary-key-field-name="id" remainder="0"/>

                        </shard-rules>

                    </shard>

                    <shard id="shard1" data-source-id="ds1">

                        <shard-rules>

                            <shard-rule

                                    class="org.tinygroup.dbrouter.impl.shardrule.ShardRuleByIdDifferentSchema"

                                    table-name="score" primary-key-field-name="id" remainder="1"/>

                        </shard-rules>

                    </shard>

                    <shard id="shard2" data-source-id="ds2">

                        <shard-rules>

                            <shard-rule

                                    class="org.tinygroup.dbrouer.impl.shardrule.ShardRuleByIdDifferentSchema"

                                    table-name="score" primary-key-field-name="id" remainder="2"/>

                        </shard-rules>

                    </shard>

                </shards>

            </partition>

        </partitions>

    </router>

</routers>

内容虽然比较长,但是其实很简单的,听偶娓娓道来:

一个配置文件可以配置多个数据库集群,因此根节点叫routers,接下来一段router就是一个集群喽。

?


1

2

<router id="aggregate" user-name="luog" password="123456"

         key-generator-bean="routerKeyGeneratorLong">

id非常重要,在通过jdbc访问数据库集群的时候,在url中要用到id,用户名和密码就是在通过jdbc连接时的用户名密码,呵呵,现在密码是明码,后续版本密码部分,会改为加密存储。

采用逻辑主键时,经常需要生成一个主键,由于集群环境中,主键的生成是一个细致活,原来采用数据库的自动生成序列、自增长啥的都不好用了,因此,一定需要一个集群模式的主键生成器。不过不用担心,框架已经提供了整型、长整型、UUID三种分布式主键生成器,大多数的情况下都够用了,如果再不够,请给我们提需求或者自已动手丰衣足食,自行进行扩展。

?


1

<key-generator-config increment="1" step="100" data-source-id="ds0"/>

这里定义了数据主键生成的一些参数配置,首先,需要一个数据源的名称,因为有一些数据需要在数据库中存储。increment表示每次主键增长幅度,step表示每申请一次缓冲多个主键。当然,这两个参数都可以忽略,这时就采用系统默认值了--多数情况下都够了。

?


1

2

3

4

5

<data-source-configs>

   <data-source-config id="ds0" driver="com.mysql.jdbc.Driver" user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test0" test-sql=""/>

   <data-source-config id="ds1" driver="com.mysql.jdbc.Driver" user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test1" test-sql=""/>

   <data-source-config id="ds2" driver="com.mysql.jdbc.Driver" user-name="root" password="123456" url="jdbc:mysql://192.168.51.29:3306/test2" test-sql=""/>

        </data-source-configs>

这里定义的就是集群中要用到的数据源的列表,熟悉jdbc的同学一看就知道什么意思就不讲了,为什么这里统一一个区域定义数据源呢??因为如果是同库分表的话,数据源实际上就是一个,这个时候只用定义一个就够了。

接下来就是定义分区了:

一个集群可以包含多个分区,一个分区可以包含多个分片。

?


1

<partition id="abc" mode="2">

mode这里用于声明分区的模式,分区有两种方式,为1的时候表示读写分离模式,为2的时候表示分表模式。 

?


1

2

<partition-rule class="org.tinygroup.dbrouter.impl.partionrule.PartionRuleByTableName" table-name="score"/>

</partition-rules>

一个分区可以包含多个分区规则,分区规则主要用于确定哪些表跑到一个分区。这里很简单,配置的是只要表名是score,就跑到本分区来执行。

一个区分又可以有多个分片,每个分片可以有一到多个分片规则,以决定是否到当前分片执行。

?


1

2

3

4

5

6

7

<shard id="shard0" data-source-id="ds0">

    <shard-rules>

        <shard-rule

                class="org.tinygroup.dbrouter.impl.shardrule.ShardRuleByIdDifferentSchema"

                table-name="score" primary-key-field-name="id" remainder="0"/>

    </shard-rules>

</shard>

上面的规则是指根据score表的id值对shard数进行取余,余数为0的命中。 另外的两个就是说余数为1和为2的时候执行。

很明显分片规则和分区规则都是可以自行扩展的---凡是可以指定bean或类名的,都是可以进行扩展滴。

用白话总结一下,上面的配置:

?


1

2

3

4

5

6

7

定义了一个标识为“aggregate”的集群,其用户名密码为“luog”和"123456",定义的主键产生器是每次增加1,每次取100个,用完之后,再去取100个,以此类推。

 

定义了三个数据源,备用。

 

定义了一个分区abc,把所有score表的处理都交给此分区进行处理,它的分区模式是分表模式。也就是说score表中的数据会被分解到多个表当中去。

 

接下来给分区abc定义了三个分片,这三个分片分别指向上面的三个数据源中的一个,第一个负责处理socre表中的id对3取余余数为0的数据;第二个负责处理score表中的id对3取余余数为1的数据;第三个负责处理score表中的id对3取余余数为2的数据;

OK,上面的定义就算完成了,下面上大菜,看测试用例:

?


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

public class AggregateTest extends TestCase {

 

 

    Statement stmt;

 

 

    private static boolean hasInit;

 

 

    @Override

    protected void setUp() throws Exception {

        super.setUp();

        RouterManager routerManager = RouterManagerBeanFactory.getManager();

        routerManager.addRouters("/differentSchemaAggregate.xml");

        Class.forName("org.tinygroup.dbrouterjdbc3.jdbc.TinyDriver");

        Connection conn = DriverManager.getConnection("jdbc:dbrouter://aggregate", "luog", "123456");

        stmt = conn.createStatement();

        prepareRecord();

    }

 

 

    private void prepareRecord() throws SQLException {

        //删除数据

        if (!hasInit) {

            stmt.execute("delete from score");

            stmt.executeUpdate("insert into score(id,name,score,course) values(1,'xiaohuihui',99,'shuxue')");

            stmt.executeUpdate("insert into score(id,name,score,course) values(2,'xiaohuihui',97,'yuwen')");

            stmt.executeUpdate("insert into score(id,name,score,course) values(3,'xiaom',95,'shuxue')");

            stmt.executeUpdate("insert into score(id,name,score,course) values(4,'xiaof',97,'yingyu')");

            stmt.executeUpdate("insert into score(id,name,score,course) values(5,'xiaom',100,'yuwen')");

            stmt.executeUpdate("insert into score(id,name,score,course) values(6,'xiaof',95,'yuwen')");

            stmt.executeUpdate("insert into score(id,name,score,course) values(7,'xiaohuihui',95,'yingyu')");

            stmt.executeUpdate("insert into score(id,name,score,course) values(8,'xiaom',96,'yingyu')");

            stmt.executeUpdate("insert into score(id,name,score,course) values(9,'xiaof',96,'shuxue')");

            hasInit = true;

        }

    }

 

 

 

 

    @Override

    protected void tearDown() throws Exception {

        super.tearDown();

    }

 

 

    public void testCount() throws SQLException {

        String sql = "select count(*),name from score group by name";

        ResultSet resultSet = stmt.executeQuery(sql);

        while (resultSet.next()) {

            String name = resultSet.getString(2);

            if (name.equals("xiaohuihui")) {

                assertEquals(3, resultSet.getInt(1));

            } else if (name.equals("xiaom")) {

                assertEquals(3, resultSet.getInt(1));

            } else if (name.equals("xiaof")) {

                assertEquals(3, resultSet.getInt(1));

            }

        }

    }

 

 

    public void testMax() throws SQLException {

        String sql = "select max(score) score,course from score group by course";

        ResultSet resultSet = stmt.executeQuery(sql);

        while (resultSet.next()) {

            String course = resultSet.getString(2);

            if (course.equals("shuxue")) {

                assertEquals(99, resultSet.getInt(1));

            } else if (course.equals("yingyu")) {

                assertEquals(97, resultSet.getInt(1));

            } else if (course.equals("yuwen")) {

                assertEquals(100, resultSet.getInt(1));

            }

        }

    }

 

 

 

 

    public void testMaxSingle() throws SQLException {

        String sql = "select max(score) score from score";

        ResultSet resultSet = stmt.executeQuery(sql);

        resultSet.next();

        assertEquals(100, resultSet.getInt(1));

    }

 

 

    public void testSum() throws SQLException {

        String sql = "select sum(score) score,name from score group by name";

        ResultSet resultSet = stmt.executeQuery(sql);

        while (resultSet.next()) {

            String name = resultSet.getString(2);

            if (name.equals("xiaohuihui")) {

                assertEquals(291, resultSet.getInt(1));

            } else if (name.equals("xiaom")) {

                assertEquals(291, resultSet.getInt(1));

            } else if (name.equals("xiaof")) {

                assertEquals(288, resultSet.getInt(1));

            }

        }

    }

 

 

    public void testMin() throws SQLException {

        String sql = "select min(score) score,name from score group by name";

        ResultSet resultSet = stmt.executeQuery(sql);

        while (resultSet.next()) {

            String name = resultSet.getString(2);

            if (name.equals("xiaohuihui")) {

                assertEquals(95, resultSet.getInt(1));

            } else if (name.equals("xiaom")) {

                assertEquals(95, resultSet.getInt(1));

            } else if (name.equals("xiaof")) {

                assertEquals(95, resultSet.getInt(1));

            }

        }

    }

 

 

    public void testMinSingle() throws SQLException {

        String sql = "select min(score) score from score";

        ResultSet resultSet = stmt.executeQuery(sql);

        resultSet.next();

        assertEquals(95, resultSet.getInt(1));

    }

 

 

    public void testAvg() throws SQLException {

        String sql = "select avg(score) score,name from score group by name";

        ResultSet resultSet = stmt.executeQuery(sql);

        while (resultSet.next()) {

            String name = resultSet.getString(2);

            if (name.equals("xiaohuihui")) {

                assertEquals(97.0, resultSet.getDouble(1));

            } else if (name.equals("xiaom")) {

                assertEquals(97.0, resultSet.getDouble(1));

            } else if (name.equals("xiaof")) {

                assertEquals(96.0, resultSet.getDouble(1));

            }

        }

    }

 

 

    public void testMultiWithOrderby() throws SQLException {

        String sql = "select min(score) minscore,max(score) maxscore,sum(score) sumscore,avg(score) avgscore, name from score group by name order by name";

        ResultSet resultSet = stmt.executeQuery(sql);

        while (resultSet.next()) {

            String name = resultSet.getString("name");

            if (name.equals("xiaohuihui")) {

                assertEquals(95.0, resultSet.getDouble(1));

                assertEquals(99.0, resultSet.getDouble(2));

                assertEquals(291.0, resultSet.getDouble(3));

                assertEquals(97.0, resultSet.getDouble(4));

            } else if (name.equals("xiaom")) {

                assertEquals(95.0, resultSet.getDouble(1));

                assertEquals(100.0, resultSet.getDouble(2));

                assertEquals(291.0, resultSet.getDouble(3));

                assertEquals(97.0, resultSet.getDouble(4));

            } else if (name.equals("xiaof")) {

                assertEquals(95.0, resultSet.getDouble(1));

                assertEquals(97.0, resultSet.getDouble(2));

                assertEquals(288.0, resultSet.getDouble(3));

                assertEquals(96.0, resultSet.getDouble(4));

            }

        }

    }

 

 

    public void testMultiSingle() throws SQLException {

        String sql = "select min(score) minscore,max(score) maxscore,sum(score) sumscore,avg(score) avgscore from score";

        ResultSet resultSet = stmt.executeQuery(sql);

        resultSet.next();

        assertEquals(95.0, resultSet.getDouble(1));

        assertEquals(100.0, resultSet.getDouble(2));

        assertEquals(870.0, resultSet.getDouble(3));

        assertEquals(97.0, Math.ceil(resultSet.getDouble(4)));

    }

 

 

    public void testMaxWithFirstAndLast() throws SQLException {

        String sql = "select max(score) score,name,course from score group by name order by score";

        ResultSet resultSet = stmt.executeQuery(sql);

        resultSet.absolute(1);

        assertEquals(97, resultSet.getInt(1));

        assertEquals("xiaof", resultSet.getString(2));

        resultSet.first();

        assertTrue(resultSet.isFirst());

        assertEquals(97, resultSet.getInt(1));

        assertEquals("xiaof", resultSet.getString(2));

        resultSet.last();

        assertTrue(resultSet.isLast());

        assertEquals(100, resultSet.getInt(1));

        assertEquals("xiaom", resultSet.getString(2));

 

 

    }

 

 

    public void testMaxWithOrderBy() throws SQLException {

        String sql = "select max(score) score,course from score group by course order by score";

        ResultSet resultSet = stmt.executeQuery(sql);

        resultSet.next();

        assertEquals("yingyu", resultSet.getString(2));

        assertEquals(97, resultSet.getInt(1));

        resultSet.next();

        assertEquals("shuxue", resultSet.getString(2));

        assertEquals(99, resultSet.getInt(1));

        resultSet.next();

        assertEquals("yuwen", resultSet.getString(2));

        assertEquals(100, resultSet.getInt(1));

    }

 

 

 

 

}

 

  

  

  

  

 <div>

 

 

  

  

  

  

 </div>

上面首先在setUp中做了一点初始化工作,主要就是下面两句:用于加载一个集群配置,实际使用有两种方法:

编程方式,约定方式。下面用的就是编译方式,如果用编写方式就简单了,只要按约定放在合适的位置,框架会自动加载配置文件,就可以不写下面的两行了。

?


1

2

RouterManager routerManager = RouterManagerBeanFactory.getManager();

routerManager.addRouters("/differentSchemaAggregate.xml");

其它的工作就与普通的JDBC没有任何不同了。

我们看看初始化之后, 数据的情况:

从上面可以看到,数据确实已经插入到三个数据表中。

后面的几个测试用例主要测试的是聚合统计方面的处理,实际上,所有的SQL语句都可以正常的执行,对于上层应用来说,它根本就不知道分表了。

急性子的同学们可能要问:

那如果我输入select * from score where id=3,结果会正确出来么?当然

那如果我输入select * from score order by id,结果会正确出来么?当然

我要说的,还远不止如此:

实际上TinyDBRouter已经竭尽全力,来支持数据库的特性:

比如:自增长

还是上面的score类子,如果在插入的时候不指定id值,如下:

?


1

insert into score(name,score,course) values('xiaohuihui',97,'yuwen')

TinyDBRouter会同样进行正常的插入,完全透明的处理好分布式主键的问题。这个与类似的框架比就先进许多了。类似的框架都是需要必须输入id,并且自己保证或必须调用其分库分表方案中提供的API来获取主键。这实际上就是有侵入性,也就是人编程人员可以感知到分库分表的存在,且必须按照相应规范进行使用。而使用TinyDBRouter,开发人员可以完全不知道有这么一层存在。

比如统计处理:

假设在一个表中有9条数据,我们执行下面的语句:

?


1

select avg(score) score,name from score group by name

我们都知道实际处理是名字相同的score值加起来,然后除以记录数,得到平均值。

但是现在数据都分成3个表了,如果在3个表上执行同样的处理:

?


1

select avg(score) score,name from score group by name

然后再进行平均值计算,可不可以呢???答案是否定的。卖个关子这里不做解释,有不理解的同学,下面回帖发问。但是TinyDBRouter框架却可以保证结果的正确性。

数据库支持的普适性:

TinyDBRouter理论上支持各种数据库,各种ORMapping框架,而一般的框架是针对某种ORMapping框架做的,比如:专门针对iBatis,Hibernate的;有的只针对MySql或Oracle等。

SQL支持的普适性:

TinyDBRouter理论上支持所有不违反TinyDBRouter适应规则的SQL。而许多同类框架则有诸多限制。

TinyDBRouter使用限制:

  • 不支持跨分区关联查询
  • 分表模式中只支持光标分页,不支持SQL分页
  • 不支持savePoint
  • 暂时不支持存储过程

总结

TinyDBRouter确实是非常优秀的分区分表方案,当然它也有缺点,那就是测试还不够充分,没有得到充分的验证。

时间: 2024-12-30 21:55:55

TinyDbRouter开源喽~~~的相关文章

TinySpider开源喽~~~

TinySpider是一个基于Tiny HtmlParser的网络数据抓取框架. Maven引用坐标: ? 1 2 3 4 5 <dependency> <groupId>org.tinygroup</groupId> <artifactId>tinyspider</artifactId> <version>0.0.12</version> </dependency> 网络爬虫,一般用在全文检索或内容获取上面.

TinyHtmlParser开源喽~~~

优点: 高效.简单.易用的Html解析器. 学习时间,分分钟. 解析速度超过,查找速度超快,支持格式化. 有强悍的容错性,即使HTML标签不规范,也会尽可以进行匹配,弥补,正确纠错率达95%以上.即使不能正确纠错,也不会导致解析不下去. 支持大小写混合匹配,即开始标签与结束标签为<HTML>和</html>也可以正确的进行匹配. Maven引用坐标: 1 <dependency> 2 <groupId>org.tinygroup</groupId>

TinyXmlParser开源喽~~~

优点: 高效.简单.易用的Xml解析器. 学习时间,分分钟. 支持中文标签名与属性名,支持下划线,减号等分隔符. 解析速度超过,查找速度超快,支持格式化. 缺点:不支持Xml Schema,DTD校验. Maven引用坐标: ? 1 2 3 4 5 <dependency> <groupId>org.tinygroup</groupId> <artifactId>xmlparser</artifactId> <version>0.0.

使用TinySpider实战抓取自己博客中的内容

因为做官网,没有内容,因此就想办法从OSChina中写的博客里弄点内容,这就要用到爬虫了. 然后就花了几分钟搞了一下,步骤如下: 第一步,写个方法抓目录: ? 1 2 3 4 5 6 7 8 9 10 11 public static void processCategory(String categoryId) {         Watcher watcher = new WatcherImpl();         Spider spider = new SpiderImpl();    

上个月全职工作(含加班费)18000,靠,加班费也要交税

问题描述 上个月全职工作(含加班费)18000,靠,加班费也要交税,一起交了2620的税,坑爹吖.上次哪个家伙说在北京7500要有安全感,算了吧?哥在上海,这个水平,我也没有安全感,还在一边做兼职赚钱,就是要赚钱.顺便说下,哥是86年的娃,一在家外企做IT软件开发.85后的,赚多少有安全感? 解决方案 解决方案二:其实我想交一亿元的税.只不过不给机会啊.解决方案三:上海待遇就是不错解决方案四:爷就是讨厌你这样的!有多少本事拿出来说说,扯蛋没用的.爷是在读博士,导师每月给生活费800,你18000

java开源项目研究 1. 引子

项目       一夜之间,开源项目仿佛无处不在,几乎覆盖了包括操作系统.网络通讯.桌面环境.教育.办公.安全.文字处理.数据库.中间件.应用娱乐在内的所有软件类型.在最大的开源项目发布平台www.sourceforge.net上,已经有近十万件开源项目,并且每天都有更多新的开源项目加入.而著名的www.apache.org则为大家奉献了Apache.Tomcat.Struts .Axis等.       开源运动是程序员对智慧成果共享.自由的追求,每天都有无数程序员为开源世界添砖加瓦,还有更多

java开源项目研究

项目 一夜之间,开源项目仿佛无处不在,几乎覆盖了包括操作系统.网络通讯.桌面环境.教育.办公.安全.文字处理.数据库.中间件.应用娱乐在内的所有软件类型.在最大的开源项目发布平台www.sourceforge.net上,已经有近十万件开源项目,并且每天都有更多新的开源项目加入.而著名的www.apache.org则为大家奉献了Apache.Tomcat.Struts .Axis等. 开源运动是程序员对智慧成果共享.自由的追求,每天都有无数程序员为开源世界添砖加瓦,还有更多的程序员在学习开源软件的

没有这个黑客 就没有开源软件

你的手机.你家的电视机顶盒.甚至你取款的 ATM,都是基于林纳斯·托瓦兹做的 Linux 系统改的.幸好他也是不收专利费. 他是谁:林纳斯·托瓦兹(Linus Torvalds,1969- ),他是开源系统 Linux 的创造者,一个热爱自由的黑客.你现在用的电子产品,或多或少都跟这个人做的操作系统有关系.好奇心:林纳斯·托瓦兹说,他的成功主要是因为他很懒,而且喜欢授权别人.这个认为娱乐价值高于商业的黑客,成了我们大部分科技产品的源头. 如果不是这位讨厌知识产权的黑客,我们现在的电脑.手机.电视

没有这个黑客,就不会有你现在用的开源软件

他是谁:林纳斯·托瓦兹(Linus Torvalds,1969- ),他是开源系统Linux的创造者,一个热爱自由的黑客.你现在用的电子产品,或多或少都跟这个人做的操作系统有关系. 好奇心:林纳斯·托瓦兹说,他的成功主要是因为他很懒,而且喜欢授权别人.这个认为娱乐价值高于商业的黑客,成了我们大部分科技产品的源头. 如果不是这位讨厌知识产权的黑客,我们现在的电脑.手机.电视机,甚至是 ATM 机都不会是现在这个样子--事实上,如果没有林纳斯·托瓦兹(Linus Torvalds)创造的 Linux