Managing Hierarchical Data in MySQL

引言
大多数用户都曾在数据库中处理过分层数据(hierarchical data),认为分层数据的管理不是关系数据库的目的。之所以这么认为,是因为关系数据库中的表没有层次关系,只是简单的平面化的列表;而分层数据具有父-子关系,显然关系数据库中的表不能自然地表现出其分层的特性。
我们认为,分层数据是每项只有一个父项和零个或多个子项(根项除外,根项没有父项)的数据集合。分层数据存在于许多基于数据库的应用程序中,包括论坛和邮件列表中的分类、商业组织图表、内容管理系统的分类、产品分类。我们打算使用下面一个虚构的电子商店的产品分类:

这些分类层次与上面提到的一些例子中的分类层次是相类似的。在本文中我们将从传统的邻接表(adjacency list)模型出发,阐述2种在MySQL中处理分层数据的模型。

邻接表模型
上述例子的分类数据将被存储在下面的数据表中(我给出了全部的数据表创建、数据插入的代码,你可以跟着做):

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
CREATE TABLE category(
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
parent INT DEFAULT NULL);


INSERT INTO category
VALUES(1,'ELECTRONICS',NULL),(2,'TELEVISIONS',1),(3,'TUBE',2),
(4,'LCD',2),(5,'PLASMA',2),(6,'PORTABLE ELECTRONICS',1),
(7,'MP3 PLAYERS',6),(8,'FLASH',7),
(9,'CD PLAYERS',6),(10,'2 WAY RADIOS',6);

SELECT * FROM category ORDER BY category_id;

+-------------+----------------------+--------+
| category_id | name | parent |
+-------------+----------------------+--------+
| 1 | ELECTRONICS | NULL |
| 2 | TELEVISIONS | 1 |
| 3 | TUBE | 2 |
| 4 | LCD | 2 |
| 5 | PLASMA | 2 |
| 6 | PORTABLE ELECTRONICS | 1 |
| 7 | MP3 PLAYERS | 6 |
| 8 | FLASH | 7 |
| 9 | CD PLAYERS | 6 |
| 10 | 2 WAY RADIOS | 6 |
+-------------+----------------------+--------+
10 rows in set (0.00 sec)

在邻接表模型中,数据表中的每项包含了指向其父项的指示器。在此例中,最上层项的父项为空值(NULL)。邻接表模型的优势在于它很简单,可以很容易地看出FLASH是MP3 PLAYERS的子项,哪个是portable electronics的子项,哪个是electronics的子项。虽然,在客户端编码中邻接表模型处理起来也相当的简单,但是如果是纯SQL编码的话,该模型会有很多问题。

检索整树
通常在处理分层数据时首要的任务是,以某种缩进形式来呈现一棵完整的树。为此,在纯SQL编码中通常的做法是使用自连接(self-join):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
WHERE t1.name = 'ELECTRONICS';

+-------------+----------------------+--------------+-------+
| lev1 | lev2 | lev3 | lev4 |
+-------------+----------------------+--------------+-------+
| ELECTRONICS | TELEVISIONS | TUBE | NULL |
| ELECTRONICS | TELEVISIONS | LCD | NULL |
| ELECTRONICS | TELEVISIONS | PLASMA | NULL |
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH |
| ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS | NULL |
| ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL |
+-------------+----------------------+--------------+-------+
6 rows in set (0.00 sec)

检索所有叶子节点
我们可以用左连接(LEFT JOIN)来检索出树中所有叶子节点(没有孩子节点的节点):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SELECT t1.name FROM
category AS t1 LEFT JOIN category as t2
ON t1.category_id = t2.parent
WHERE t2.category_id IS NULL;


+--------------+
| name |
+--------------+
| TUBE |
| LCD |
| PLASMA |
| FLASH |
| CD PLAYERS |
| 2 WAY RADIOS |
+--------------+

检索单一路径
通过自连接,我们也可以检索出单一路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
WHERE t1.name = 'ELECTRONICS' AND t4.name = 'FLASH';

+-------------+----------------------+-------------+-------+
| lev1 | lev2 | lev3 | lev4 |
+-------------+----------------------+-------------+-------+
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH |
+-------------+----------------------+-------------+-------+
1 row in set (0.01 sec)

这种方法的主要局限是你需要为每层数据添加一个自连接,随着层次的增加,自连接变得越来越复杂,检索的性能自然而然的也就下降了。

邻接表模型的局限性
用纯SQL编码实现邻接表模型有一定的难度。在我们检索某分类的路径之前,我们需要知道该分类所在的层次。另外,我们在删除节点的时候要特别小心,因为潜在的可能会孤立一棵子树(当删除portable electronics分类时,所有他的子分类都成了孤儿)。部分局限性可以通过使用客户端代码或者存储过程来解决,我们可以从树的底部开始向上迭代来获得一颗树或者单一路径,我们也可以在删除节点的时候使其子节点指向一个新的父节点,来防止孤立子树的产生。

嵌套集合(Nested Set)模型
我想在这篇文章中重点阐述一种不同的方法,俗称为嵌套集合模型。在嵌套集合模型中,我们将以一种新的方式来看待我们的分层数据,不再是线与点了,而是嵌套容器。我试着以嵌套容器的方式画出了electronics分类图:

从上图可以看出我们依旧保持了数据的层次,父分类包围了其子分类。在数据表中,我们通过使用表示节点的嵌套关系的左值(left value)和右值(right value)来表现嵌套集合模型中数据的分层特性:

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
CREATE TABLE nested_category (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL
);


INSERT INTO nested_category
VALUES(1,'ELECTRONICS',1,20),(2,'TELEVISIONS',2,9),(3,'TUBE',3,4),
(4,'LCD',5,6),(5,'PLASMA',7,8),(6,'PORTABLE ELECTRONICS',10,19),
(7,'MP3 PLAYERS',11,14),(8,'FLASH',12,13),
(9,'CD PLAYERS',15,16),(10,'2 WAY RADIOS',17,18);


SELECT * FROM nested_category ORDER BY category_id;


+-------------+----------------------+-----+-----+
| category_id | name | lft | rgt |
+-------------+----------------------+-----+-----+
| 1 | ELECTRONICS | 1 | 20 |
| 2 | TELEVISIONS | 2 | 9 |
| 3 | TUBE | 3 | 4 |
| 4 | LCD | 5 | 6 |
| 5 | PLASMA | 7 | 8 |
| 6 | PORTABLE ELECTRONICS | 10 | 19 |
| 7 | MP3 PLAYERS | 11 | 14 |
| 8 | FLASH | 12 | 13 |
| 9 | CD PLAYERS | 15 | 16 |
| 10 | 2 WAY RADIOS | 17 | 18 |
+-------------+----------------------+-----+-----+

我们使用了lft和rgt来代替left和right,是因为在MySQL中left和right是保留字。http://dev.mysql.com/doc/mysql/en/reserved-words.html,有一份详细的MySQL保留字清单。
那么,我们怎样决定左值和右值呢?我们从外层节点的最左侧开始,从左到右编号:

这样的编号方式也同样适用于典型的树状结构:

当我们为树状的结构编号时,我们从左到右,一次一层,为节点赋右值前先从左到右遍历其子节点给其子节点赋左右值。这种方法被称作改进的先序遍历算法

检索整树
我们可以通过自连接把父节点连接到子节点上来检索整树,是因为子节点的lft值总是在其父节点的lft值和rgt值之间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SELECT node.name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND parent.name = 'ELECTRONICS'
ORDER BY node.lft;


+----------------------+
| name |
+----------------------+
| ELECTRONICS |
| TELEVISIONS |
| TUBE |
| LCD |
| PLASMA |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS |
| FLASH |
| CD PLAYERS |
| 2 WAY RADIOS |
+----------------------+

不像先前邻接表模型的例子,这个查询语句不管树的层次有多深都能很好的工作。在BETWEEN的子句中我们没有去关心node的rgt值,是因为使用node的rgt值得出的父节点总是和使用lft值得出的是相同的。

检索所有叶子节点
检索出所有的叶子节点,使用嵌套集合模型的方法比邻接表模型的LEFT JOIN方法简单多了。如果你仔细得看了nested_category表,你可能已经注意到叶子节点的左右值是连续的。要检索出叶子节点,我们只要查找满足rgt=lft+1的节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT name
FROM nested_category
WHERE rgt = lft + 1;


+--------------+
| name |
+--------------+
| TUBE |
| LCD |
| PLASMA |
| FLASH |
| CD PLAYERS |
| 2 WAY RADIOS |
+--------------+

检索单一路径
在嵌套集合模型中,我们可以不用多个自连接就可以检索出单一路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT parent.name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'FLASH'
ORDER BY parent.lft;

+----------------------+
| name |
+----------------------+
| ELECTRONICS |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS |
| FLASH |
+----------------------+

检索节点的深度
我们已经知道怎样去呈现一棵整树,但是为了更好的标识出节点在树中所处层次,我们怎样才能检索出节点在树中的深度呢?我们可以在先前的查询语句上增加COUNT函数和GROUP BY子句来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SELECT node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;

+----------------------+-------+
| name | depth |
+----------------------+-------+
| ELECTRONICS | 0 |
| TELEVISIONS | 1 |
| TUBE | 2 |
| LCD | 2 |
| PLASMA | 2 |
| PORTABLE ELECTRONICS | 1 |
| MP3 PLAYERS | 2 |
| FLASH | 3 |
| CD PLAYERS | 2 |
| 2 WAY RADIOS | 2 |
+----------------------+-------+

我们可以根据depth值来缩进分类名字,使用CONCAT和REPEAT字符串函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SELECT CONCAT( REPEAT(' ', COUNT(parent.name) - 1), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;

+-----------------------+
| name |
+-----------------------+
| ELECTRONICS |
| TELEVISIONS |
| TUBE |
| LCD |
| PLASMA |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS |
| FLASH |
| CD PLAYERS |
| 2 WAY RADIOS |
+-----------------------+

当然,在客户端应用程序中你可能会用depth值来直接展示数据的层次。Web开发者会遍历该树,随着depth值的增加和减少来添加

标签。

检索子树的深度
当我们需要子树的深度信息时,我们不能限制自连接中的node或parent,因为这么做会打乱数据集的顺序。因此,我们添加了第三个自连接作为子查询,来得出子树新起点的深度值:

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
SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
nested_category AS parent,
nested_category AS sub_parent,
(
SELECT node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'PORTABLE ELECTRONICS'
GROUP BY node.name
ORDER BY node.lft
)AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.name = sub_tree.name
GROUP BY node.name
ORDER BY node.lft;


+----------------------+-------+
| name | depth |
+----------------------+-------+
| PORTABLE ELECTRONICS | 0 |
| MP3 PLAYERS | 1 |
| FLASH | 2 |
| CD PLAYERS | 1 |
| 2 WAY RADIOS | 1 |
+----------------------+-------+

这个查询语句可以检索出任一节点子树的深度值,包括根节点。这里的深度值跟你指定的节点有关。

检索节点的直接子节点
可以想象一下,你在零售网站上呈现电子产品的分类。当用户点击分类后,你将要呈现该分类下的产品,同时也需列出该分类下的直接子分类,而不是该分类下的全部分类。为此,我们只呈现该节点及其直接子节点,不再呈现更深层次的节点。例如,当呈现PORTABLEELECTRONICS分类时,我们同时只呈现MP3 PLAYERS、CD PLAYERS和2 WAY RADIOS分类,而不呈现FLASH分类。

要实现它非常的简单,在先前的查询语句上添加HAVING子句:

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
SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
nested_category AS parent,
nested_category AS sub_parent,
(
SELECT node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'PORTABLE ELECTRONICS'
GROUP BY node.name
ORDER BY node.lft
)AS sub_tree
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.name = sub_tree.name
GROUP BY node.name
HAVING depth <= 1
ORDER BY node.lft;

+----------------------+-------+
| name | depth |
+----------------------+-------+
| PORTABLE ELECTRONICS | 0 |
| MP3 PLAYERS | 1 |
| CD PLAYERS | 1 |
| 2 WAY RADIOS | 1 |
+----------------------+-------+

如果你不希望呈现父节点,你可以更改HAVING depth <= 1HAVING depth = 1

嵌套集合模型中集合函数的应用
让我们添加一个产品表,我们可以使用它来示例集合函数的应用:

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
CREATE TABLE product(
product_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(40),
category_id INT NOT NULL
);


INSERT INTO product(name, category_id) VALUES('20" TV',3),('36" TV',3),
('Super-LCD 42"',4),('Ultra-Plasma 62"',5),('Value Plasma 38"',5),
('Power-MP3 5gb',7),('Super-Player 1gb',8),('Porta CD',9),('CD To go!',9),
('Family Talk 360',10);

SELECT * FROM product;

+------------+-------------------+-------------+
| product_id | name | category_id |
+------------+-------------------+-------------+
| 1 | 20" TV | 3 |
| 2 | 36" TV | 3 |
| 3 | Super-LCD 42" | 4 |
| 4 | Ultra-Plasma 62" | 5 |
| 5 | Value Plasma 38" | 5 |
| 6 | Power-MP3 128mb | 7 |
| 7 | Super-Shuffle 1gb | 8 |
| 8 | Porta CD | 9 |
| 9 | CD To go! | 9 |
| 10 | Family Talk 360 | 10 |
+------------+-------------------+-------------+

现在,让我们写一个查询语句,在检索分类树的同时,计算出各分类下的产品数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SELECT parent.name, COUNT(product.name)
FROM nested_category AS node ,
nested_category AS parent,
product
WHERE node.lft BETWEEN parent.lft AND parent.rgt
AND node.category_id = product.category_id
GROUP BY parent.name
ORDER BY node.lft;


+----------------------+---------------------+
| name | COUNT(product.name) |
+----------------------+---------------------+
| ELECTRONICS | 10 |
| TELEVISIONS | 5 |
| TUBE | 2 |
| LCD | 1 |
| PLASMA | 2 |
| PORTABLE ELECTRONICS | 5 |
| MP3 PLAYERS | 2 |
| FLASH | 1 |
| CD PLAYERS | 2 |
| 2 WAY RADIOS | 1 |
+----------------------+---------------------+

这条查询语句在检索整树的查询语句上增加了COUNT和GROUP BY子句,同时在WHERE子句中引用了product表和一个自连接。

新增节点
到现在,我们已经知道了如何去查询我们的树,是时候去关注一下如何增加一个新节点来更新我们的树了。让我们再一次观察一下我们的嵌套集合图:

当我们想要在TELEVISIONS和PORTABLE ELECTRONICS节点之间新增一个节点,新节点的lft和rgt 的 值为10和11,所有该节点的右边节点的lft和rgt值都将加2,之后我们再添加新节点并赋相应的lft和rgt值。在MySQL 5中可以使用存储过程来完成,我假设当前大部分读者使用的是MySQL 4.1版本,因为这是最新的稳定版本。所以,我使用了锁表(LOCK TABLES)语句来隔离查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
LOCK TABLE nested_category WRITE;


SELECT @myRight := rgt FROM nested_category
WHERE name = 'TELEVISIONS';



UPDATE nested_category SET rgt = rgt + 2 WHERE rgt > @myRight;
UPDATE nested_category SET lft = lft + 2 WHERE lft > @myRight;

INSERT INTO nested_category(name, lft, rgt) VALUES('GAME CONSOLES', @myRight + 1, @myRight + 2);

UNLOCK TABLES;

我们可以检验一下新节点插入的正确性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;


+-----------------------+
| name |
+-----------------------+
| ELECTRONICS |
| TELEVISIONS |
| TUBE |
| LCD |
| PLASMA |
| GAME CONSOLES |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS |
| FLASH |
| CD PLAYERS |
| 2 WAY RADIOS |
+-----------------------+

如果我们想要在叶子节点下增加节点,我们得稍微修改一下查询语句。让我们在2 WAYRADIOS叶子节点下添加FRS节点吧:

1
2
3
4
5
6
7
8
9
10
11
12
LOCK TABLE nested_category WRITE;

SELECT @myLeft := lft FROM nested_category

WHERE name = '2 WAY RADIOS';

UPDATE nested_category SET rgt = rgt + 2 WHERE rgt > @myLeft;
UPDATE nested_category SET lft = lft + 2 WHERE lft > @myLeft;

INSERT INTO nested_category(name, lft, rgt) VALUES('FRS', @myLeft + 1, @myLeft + 2);

UNLOCK TABLES;

在这个例子中,我们扩大了新产生的父节点(2 WAY RADIOS节点)的右值及其所有它的右边节点的左右值,之后置新增节点于新父节点之下。正如你所看到的,我们新增的节点已经完全融入了嵌套集合中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;


+-----------------------+
| name |
+-----------------------+
| ELECTRONICS |
| TELEVISIONS |
| TUBE |
| LCD |
| PLASMA |
| GAME CONSOLES |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS |
| FLASH |
| CD PLAYERS |
| 2 WAY RADIOS |
| FRS |
+-----------------------+

删除节点
最后还有个基础任务,删除节点。删除节点的处理过程跟节点在分层数据中所处的位置有关,删除一个叶子节点比删除一个子节点要简单得多,因为删除子节点的时候,我们需要去处理孤立节点。
删除一个叶子节点的过程正好是新增一个叶子节点的逆过程,我们在删除节点的同时该节点右边所有节点的左右值和该父节点的右值都会减去该节点的宽度值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LOCK TABLE nested_category WRITE;


SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
WHERE name = 'GAME CONSOLES';


DELETE FROM nested_category WHERE lft BETWEEN @myLeft AND @myRight;


UPDATE nested_category SET rgt = rgt - @myWidth WHERE rgt > @myRight;
UPDATE nested_category SET lft = lft - @myWidth WHERE lft > @myRight;

UNLOCK TABLES;

我们再一次检验一下节点已经成功删除,而且没有打乱数据的层次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;


+-----------------------+
| name |
+-----------------------+
| ELECTRONICS |
| TELEVISIONS |
| TUBE |
| LCD |
| PLASMA |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS |
| FLASH |
| CD PLAYERS |
| 2 WAY RADIOS |
| FRS |
+-----------------------+

这个方法可以完美地删除节点及其子节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LOCK TABLE nested_category WRITE;


SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
WHERE name = 'MP3 PLAYERS';


DELETE FROM nested_category WHERE lft BETWEEN @myLeft AND @myRight;


UPDATE nested_category SET rgt = rgt - @myWidth WHERE rgt > @myRight;
UPDATE nested_category SET lft = lft - @myWidth WHERE lft > @myRight;

UNLOCK TABLES;

再次验证我们已经成功的删除了一棵子树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;


+-----------------------+
| name |
+-----------------------+
| ELECTRONICS |
| TELEVISIONS |
| TUBE |
| LCD |
| PLASMA |
| PORTABLE ELECTRONICS |
| CD PLAYERS |
| 2 WAY RADIOS |
| FRS |
+-----------------------+

有时,我们只删除该节点,而不删除该节点的子节点。在一些情况下,你希望改变其名字为占位符,直到替代名字的出现,比如你开除了一个主管(需要更换主管)。在另外一些情况下,你希望子节点挂到该删除节点的父节点下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LOCK TABLE nested_category WRITE;


SELECT @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
WHERE name = 'PORTABLE ELECTRONICS';


DELETE FROM nested_category WHERE lft = @myLeft;


UPDATE nested_category SET rgt = rgt - 1, lft = lft - 1 WHERE lft BETWEEN @myLeft AND @myRight;
UPDATE nested_category SET rgt = rgt - 2 WHERE rgt > @myRight;
UPDATE nested_category SET lft = lft - 2 WHERE lft > @myRight;

UNLOCK TABLES;

在这个例子中,我们对该节点所有右边节点的左右值都减去了2(因为不考虑其子节点,该节点的宽度为2),对该节点的子节点的左右值都减去了1(弥补由于失去父节点的左值造成的裂缝)。我们再一次确认,那些节点是否都晋升了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
WHERE node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;


+---------------+
| name |
+---------------+
| ELECTRONICS |
| TELEVISIONS |
| TUBE |
| LCD |
| PLASMA |
| CD PLAYERS |
| 2 WAY RADIOS |
| FRS |
+---------------+

有时,当删除节点的时候,把该节点的一个子节点挂载到该节点的父节点下,而其他节点挂到该节点父节点的兄弟节点下,考虑到篇幅这种情况不在这里解说了。

最后的思考
我希望这篇文章对你有所帮助,SQL中的嵌套集合的观念大约有十年的历史了,在网上和一些书中都能找到许多相关信息。在我看来,讲述分层数据的管理最全面的,是来自一本名叫《Joe Celko’s Trees and Hierarchies in SQL for Smarties》的书,此书的作者是在高级SQL领域倍受尊敬的Joe Celko。Joe Celko被认为是嵌套集合模型的创造者,更是该领域内的多产作家。我把Celko的书当作无价之宝,并极力地推荐它。在这本书中涵盖了在此文中没有提及的一些高级话题,也提到了其他一些关于邻接表和嵌套集合模型下管理分层数据的方法。
在随后的参考书目章节中,我列出了一些网络资源,也许对你研究分层数据的管理会有所帮助,其中包括一些PHP相关的资源(处理嵌套集合的PHP库)。如果你还在使用邻接表模型,你该去试试嵌套集合模型了,在Storing Hierarchical Data in a Database 文中下方列出的一些资源链接中能找到一些样例代码,可以去试验一下。

转自 http://www.cnblogs.com/phaibin/archive/2009/06/09/1499687.html

PHP 无极分类生成树状数组

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
<?php
$arr=[
['id' => 1, 'text' => 'Parent 1', 'pid' => 0],
['id' => 2, 'text' => 'Parent 2', 'pid' => 0],
['id' => 3, 'text' => 'Parent 3', 'pid' => 0],
['id' => 4, 'text' => 'Child 1', 'pid' => 1],
['id' => 5, 'text' => 'Parent 4', 'pid' => 0],
['id' => 6, 'text' => 'Child 2', 'pid' => 1],
['id' => 7, 'text' => 'Child 3', 'pid' => 1],
['id' => 8, 'text' => 'Parent 5', 'pid' => 0],
['id' => 9, 'text' => 'Child 1', 'pid' => 2],
['id' => 10, 'text' => 'Child 4', 'pid' => 1],
['id' => 11, 'text' => 'Child 1', 'pid' => 5],
['id' => 12, 'text' => 'GrandChild 1', 'pid' => 10]
];

class createTree {
private static $table = [];

private function __construct() {}

private static function tree($pid = 0) {
$tree = array();
foreach (self::$table as $row) {
if ($row['pid'] === $pid) {
$tmp = self::tree($row['id']);
if ($tmp) {
$row['children'] = $tmp;
}
$tree[] = $row;
}
}
return $tree;
}

public static function get($table) {
self::$table = $table;
return self::tree();
}
}

var_dump(createTree::get($arr));

一个简单的 MySQL 队列问题

最近有个朋友要实现队列任务方面的工作,我们就 mysql(innodb) 的事务和锁的特性聊了一些有趣的话题。
其中,最终的解决方案来自大神 https://github.com/fengmk2 之前的一个队列实现。 我做了一个小改进,使得之前表级锁的表现可以恢复到行级锁水平。

任务的大致描述是这样的:
有一个表,里面存了很多的用户id,大概100w条,表的结构简化如下:

1
2
3
4
5
create table user_block_status {
user_id bigint // 用户的id
status int // 用户的状态。1 ok 2 not ok
updated_time timestamp // 更新时间戳
}

这个表里面,每隔10秒就要去检查用户是否存在违规页面。如果存在的话,则需要把 status 置为 2,默认是 1。
有 100 个 worker 会并发地从表里面读取 user_id,所以我们要设计一个策略,使得这 100 个 worker 在并发时, 读到的是独立的 100 个条目。

方案1
一开始的方案是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 这一句不一定会发请求,可能会优化成跟接下来的第一个 query 一起发出
sql.begin_transaction

// 第一次io发生。
// 如果一个用户在 10s 内没有被更新,那么取出来
// 这时候由于程序拿得到 user_id 的值,所以网络io是发生了的。否则拿不到 user_id 的值
outdate_time = now() - 10s
line = sql.query('select user_id where updated_time < ? order by updated_time asc limit 1', [outdate_time])

// 第二次 io 发生
// 更新这一行的 updated_time,免得被其他worker重复读取
user_id = line.user_id
sql.query('update user_block_status set updated_time=now() where user_id = ?',
[user_id])

// 第三次 io 发生
sql.commit

// do something with user_id

可以看到,这个地方我们发起了 3 次 io 请求。当然,请求数不是很关键,因为请求数以及对应的时间是一个恒定量, 而随着 worker 的增加,这一块并不会带来额外的性能瓶颈。但由于我们使用了事务,所以当 worker 由 100 增加到 1000 的时候,数据库由于存在大量的事务操作,这些事务都需要掌握写锁,所以有潜在的写锁排队问题。
而且关键是,方案是不可行的,根本没有起到队列的效果。
为什么呢?我们假设网络io无限快,而数据库每条语句的执行时间是1s,那么我们这个事务的执行时间是 2s。 这时如果 3 个 worker 并发地在同一秒(00:00)执行,那么假设 worker1 读到的 user_id 是 10086, 由于读锁是共享的,worker2 和 worker3 读到的 user_id 也是 10086。这时他们三个都想要更新 10086 的值, 而 worker1 抢先加了写锁,所以 worker2 和 worker3 就需要等待 worker1 的事务执行完毕, 才能重新获得 10086 的写锁并进行写入。 所以当 worker2 执行的时候,是 00:02 的时候,当 worker3 执行的时候,是 00:04 的时刻。 而且由于他们都是在对 10086 进行更新,所以没有起到队列的效果。
这里的查询条件太特殊,导致所有并发的事务需要的都是同一条数据, 这时候 innodb 行级锁的特性也没有发挥出来。
这个方案不仅并发时的表现类似表级锁的特性,而且也没有达到队列的效果。

方案2
将 update 语句在先,select 语句在后。
update 语句改成

1
2
3
4
5
outdate_time = now() - 10s
result = sql.query('update user_block_status set updated_time=now() where updated_time < ? order by updated_time asc limit 1',
[outdate_time])

## each worker can get different result.user_id

这样在 update 的时候,3 个 worker 会排队,分别更新不同的 user_id 条目。然后返回来的 也是不同的 user_id。
可关键是,update 语句并不会将被 update 了的 id 返回给程序,所以我们后面的 select 语句拿不到对应的 user_id。 这个方案先否决。

方案3
方案1的基础上,在 select 语句中,手工地干扰一下,使得不同的 worker 取到不同的条目

1
2
3
4
outdate_time = now() - 10s
random_number = random_int(0, worker_count * 2)
line = sql.query('select user_id where updated_time < ? order by updated_time asc limit 1 offset ?',
[outdate_time, random_number])

这时,我们的 worker 有很大的几率可以取出不同 user_id。但这里也还有个问题就是,很可能两个 worker 的 random_number 是同一个值。那么就发生了两次重复读取,不过对于我们的业务来说,重复读取只会造成资源的浪费, 而不会带来数据一致性的问题。只要尽量减少重复读的几率,那么这个方案就是可被接受的。
其中 worker_count * 2 是拍脑袋决定的数,如果数据库中始终有大量需要处理的数据,可以加大点。

方案4
方案3还是挺不完美的,虽然能解决问题,但是从概念上来说,我们需要的是队列。 队列的意思就是:排队!排队!排队!
方案3只是从业务逻辑层面出发,做出了一些规避,模拟了我们需要的效果。
那么回到方案2,其实方案2是更接近队列的。因为不同的 worker 真正在等待另一个 worker 更新东西。 可方案2无奈的是,我们拿不到被更新的id。那么有没有办法拿到呢?
其实是有的,用 mysql 的 LAST_INSERT_ID() 函数。

1
LAST_INSERT_ID(): Value of the AUTOINCREMENT column for the last INSERT

关于这个函数可以看看 https://dev.mysql.com/doc/refman/5.7/en/information-functions.html 这里的详细介绍。
这个函数本来的含义是,拿到 AUTO_INCREMENT 那一列的最新值。也就是我们最新 insert 进表的那个 id。 但实际上,它也可以作为一个 sql 语句中的变量来使用,它可以被赋值,然后取出。 而且它的作用域是同一 connection 内,这样我们多个 worker 如果对 LAST_INSERT_ID 赋了不同的值, 也不会互相干扰,因为不同的 worker 使用不同的 connection。
这时,我们的查询在方案2的基础上就变成:

1
2
3
4
5
6
7
8
9
10
11
12
sql.begin_transaction

outdate_time = now() - 10s
sql.query('update user_block_status set updated_time=now(),
id=LAST_INSERT_ID(id) where updated_time < ? order by updated_time asc limit 1',
[outdate_time])

line = sql.query('select user_id where id = LAST_INSERT_ID()')

## do sth with line.user_id

sql.commit

ok,已经能排队了,业务上已经可以满足了。
目前性能上说,网络io还是三个,而且,【行级锁】没有被利用的特定依然存在。 写锁依然要排队,为什么这么说?因为不管 worker 有多少个,当他们并发的时候,where 条件都始终把它们 指向同一行数据,所以还是要为了同一行数据排队。即使目前我们已经达成了【排队之后,互相更新不同条目】这个目的。
方案4就总的性价比来说,目前跟方案3相比,还不一定谁好谁坏。 方案4的性能在于多个worker抢一个锁,大家总是等;方案3是无脑乱取,造成资源浪费,降低worker的效率,浪费机器。
什么情况下方案3好? 如果总是有一大堆数据没有被处理的话,那么把方案3的乱取范围开大点,就能更好避免浪费。 而当一大堆数据等待处理的时候,方案4却不停在排队,这就等于堵住了。
还有一种情况就是,方案4的写锁排队已经成为瓶颈。但其实这跟上面是一回事,当总是有一大堆 worker 来取 东西的话,说明就是有一大堆数据没有被处理。否则开那么多 worker 干嘛。
什么情况下方案4好? 前提就是,写锁排队并不成为瓶颈。如果要处理的数据并不是那么多,那么使用方案4的话,可以降低我们需要的 worker 数量,节约机器。 而且 worker 数量评估可以更加理性。

方案5
那么,我们把方案3的 offset 思想加进来吧。可惜啊可惜,update 语法只支持 limit,不支持 offset。

1
2
3
4
5
UPDATE [LOW_PRIORITY] [IGNORE] table_reference
SET col_name1={expr1|DEFAULT} [, col_name2={expr2|DEFAULT}] ...
[WHERE where_condition]
[ORDER BY ...]
[LIMIT row_count]

那就绕一绕。
不用 offset,而是通过更改 outdate_time 的值,让他们获得不同的行数据。
我们的程序是要求 10s 算作过期,那么 11s、20s、30s 肯定也算过期吧。那就这样写:

1
2
3
4
5
6
7
// 在 10 到 30s 之间随机取值
outdate_time = now() - (random(10, 30))s
sql.query('update user_block_status set updated_time=now(),
id=LAST_INSERT_ID(id) where updated_time < ? order by updated_time asc limit 1',
[outdate_time])
where updated_time < now() - 10s 与 where updated_time < now() - 12s 与 where updated_time < now() - 15s
//(不要在 where 条件里面写计算,这只是示例) 还是有可能锁定同一条数据。但至少,这个方案既利用上了行级锁,也不会造成多个 worker 处理同一 user_id 的 资源浪费。

方案6
锁的问题差不多就这么解决了。
我们再回头看看,发现还有个 io 问题可以再弄弄。现在还是 3 个 io 嘛。
其实到了现在这步,begin_transaction 可以去掉了。因为我们只有一个涉及写锁的操作在里面,这个操作本身作为单一语句, 就已经是原子性的了。
但由于我们利用了 LAST_INSERT_ID,所以我们要保证 update 语句和它之后的 select 语句在同一个 connection 中。
很多的 mysql 库实现都是用了连接池的,所以同一段代码中的两条 sql 有可能会利用两条 connection, 导致得到我们非预期的 user_id。
但就我们的业务来说,LAST_INSERT_ID 混了其实是没关系的。每个 worker 始终还是会得到一个 unique 的 user_id。 这就够了。那么我们也不必加一些多余的逻辑,保证这两条语句取到同一个 connection。
这时,io 操作从 3,降低到了 2。
那么,有没有可能降到 1 呢。
其实也可以啊…………因为基本所有 mysql 库都支持 multistatements 特性。
我们可以在一条 query 写两个语句,返回接口会是一个数组,分别表示这两个语句的值。
类似这样,sql.query(‘update …..; select ….;’)。这是支持的。而且这么一来, 同一 connection 的问题也解决了。避免为以后留坑。
重写方案

1
2
3
4
5
6
7
8
outdate_time = now() - (random(10, 30))s
result = sql.query('update user_block_status set updated_time=now(),
user_id=LAST_INSERT_ID(user_id) where updated_time < ? order by updated_time asc limit 1;

select * from user_block_status where user_id = LAST_INSERT_ID()',
[outdate_time])

// do something with result[1].user_id

。。。。。。。。。。。。。
还是有坑的。。。。。。。。。。。。。。。
如果 where updated_time < ? 一条都不命中,那么会发生什么结果?
首先,update 没有改变任何行。而 LAST_INSERT_ID 还是会返回一个合理的 id,有可能是真正的 LAST_INSERT_ID, 也可能是这条 connection 中上次手工设置的。
在这里可以多说一下 LAST_INSERT_ID 的特性。默认情况下,LAST_INSERT_ID() 不带参数会返回最新插入那条的 id。 带参数的情况下 LAST_INSERT_ID(id) 本身的返回值就是参数,然后在接下来的调用中,如果不发生任何 insert,那么 值会在 connection 中一直保持。如果发生了 insert,就会被更新。
如果不处理这个 update nothing 的异常情况,当队列全部被处理完的时候, 我们的 worker 会一直工作,不会停下来。所以我们要在取 LAST_INSERT_ID 的值时, 判断一下上一条 update 语句到底有没有发生作用。
这时候我们需要用到另一个跟 LAST_INSERT_ID 一起出现在文档中的函数,

1
ROW_COUNT(): The number of rows updated

判断一下 ROW_COUNT,如果是 0 的话,就条件不符,这时候我们在程序里面拿到的值就是空。
最终方案

1
2
3
4
5
6
7
8
9
outdate_time = now() - (random(10, 30))s
result = sql.query('update user_block_status set updated_time=now(),
user_id=LAST_INSERT_ID(user_id) where updated_time < ? order by updated_time asc limit 1;

select * from user_block_status where user_id = LAST_INSERT_ID()
and ROW_COUNT() <> 0',
[outdate_time])

// do something with result[1].user_id

当然,mysql 用来解决这种队列问题可能不是一个好的方案。队列相关的知识,我还在努力学习中。
参考资料:

转载自 https://ruby-china.org/topics/27814

附:虽然用的是 ruby 语言,但其中最关键的还是 sql 语句。最近做个基于 laravel 的应用中使用到了队列的概念,因为对并发要求不高,所以直接用了 MariaDB,记下源码留作备用:

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
<?php

namespace App\Http\Controllers\api\v1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class server extends Controller {
public function create(Request $request) {
$inputFilters = [
"gid" => ["filter" => FILTER_VALIDATE_INT, "options" => ['min_range' => 1]],
"sid" => ["filter" => FILTER_VALIDATE_INT, "options" => ['min_range' => 1]]
];
$inputData = $request->all();
$insertData = filter_var_array($inputData, $inputFilters);
foreach ($insertData as $value) {
if (!$value) {
return response()->json(["errno" => -1], 500);
}
}
DB::table('srv')
->where([
['conf', '=', $insertData["gid"]],
['state', '=', '0'],
['power', '=', '0']
])
->orderBy('id', 'desc')
->take(1)
->update([
'state' => 1,
'power' => 1,
'sid' => $insertData["sid"],
'id' => DB::raw('LAST_INSERT_ID(id)')
]);
$data = DB::table('srv')
->where([
['id', '=', DB::raw('LAST_INSERT_ID()')],
[DB::raw('ROW_COUNT()'), '<>', 0]
])
->first();
if (!$data) {
return response()->json(["errno" => -2], 500);
}
return response()->json(["errno" => 0, "data" => $data]);
}
}

jQuery(selector).html() 过滤 script tag 的解决方法

  之前用 pjax 做个项目,使用了 .html() 方法将获取到的数据插入 container。但是却发现其会自动过滤 script tag,现找到解决方法 (jquery html() strips out script tags),在此记录一下
  以下是我应用到项目里的部分代码,对 stackoverflow 的答案多进行了一次判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$(document).on("pjax:end", function(event, data) {
var responseDom = $(data.responseText);
if (!$(event.target).filter("script").length) {
responseDom.filter('script').each(function(){
if (this.src) {
var script = document.createElement('script'), i, attrName, attrValue, attrs = this.attributes;
for(i = 0; i < attrs.length; i++) {
attrName = attrs[i].name;
attrValue = attrs[i].value;
script[attrName] = attrValue;
}
event.target.appendChild(script);
} else {
$.globalEval(this.text || this.textContent || this.innerHTML || '');
}
});
}
});

安装 Laravel,撞墙,采用 Packageist 的中国镜像

参考:

安装 Laravel,创建 blog 项目
安装方法有两种:

  1. 全局安装 Laravel Installer,然后用下面的指令创建新项目: laravel new blog
  1. 不安装啥,直接用 Composer 创建新项目:composer create-project –prefer-dist laravel/laravel blog

看起来第一种方案比较好,然而:
!说明!由于墙的存在,全局安装 Laravel Installer 的方案可能不会成功。

全局安装 Laravel Installer

1
composer global require "laravel/installer"

执行命令

1
laravel new blog

悲剧了,出现错误:
cURL error 7: Failed to connect to cabinet.laravel.com port 80: Timed out……
直接用 Composer 创建 Laravel 项目

参照网上的方案,先执行加速 composer 的执行(用国内的镜像,好人呐!):

1
composer config -g repo.packagist composer https://packagist.phpcomposer.com

然后执行

1
composer global require "laravel/installer"

创建项目

转载自 http://blog.sina.com.cn/s/blog_6262a50e0102ws9z.html,有删改

CentOS 7 grub Linux 修改默认的启动操作系统

可以用下面的方法修改grub默认的启动OS。

查看当前的启动内核

1
2
[root@localhost ~]# grub2-editenvlist
saved_entry=CentOS Linux(3.10.0-123.20.1.el7.x86_64) 7 (Core)

查找要默认启动的操作系统名字

1
2
3
4
[root@localhost ~]# cat /etc/grub2.cfg | grep 3.4.44
menuentry 'CentOS Linux (3.4.44) 7(Core)' --class centos --class gnu-linux --class gnu --class os--unrestricted $menuentry_id_option'gnulinux-3.10.0-123.el7.x86_64-advanced-e3146a2a-a237-4081-ba08-dbf258de434a'{
linux16 /vmlinuz-3.4.44 root=/dev/mapper/centos-rootro rd.lvm.lv=centos/swap vconsole.font=latarcyrheb-sun16 rd.lvm.lv=centos/rootcrashkernel=auto vconsole.keymap=us rhgbquiet LANG=en_US.UTF-8
initrd16 /initramfs-3.4.44.img

设置新的默认启动操作系统选项

1
[root@localhost ~]# grub2-set-default  "CentOSLinux (3.4.44) 7 (Core)"

查看是否生效

1
2
[root@localhost ~]# grub2-editenv list
saved_entry=CentOS Linux (3.4.44) 7 (Core)

转载自 http://blog.csdn.net/wjw7869/article/details/47302107

CentOS 7 主机名的修改

如何在CentOS 7上修改主机名

在CentOS中,有三种定义的主机名:静态的(static),瞬态的(transient),和灵活的(pretty)。“静态”主机名也称为内核主机名,是系统在启动时从/etc/hostname自动初始化的主机名。“瞬态”主机名是在系统运行时临时分配的主机名,例如,通过DHCP或mDNS服务器分配。静态主机名和瞬态主机名都遵从作为互联网域名同样的字符限制规则。而另一方面,“灵活”主机名则允许使用自由形式(包括特殊/空白字符)的主机名,以展示给终端用户(如Linuxidc)。

在CentOS 7中,有个叫hostnamectl的命令行工具,它允许你查看或修改与主机名相关的配置。

1.要查看主机名相关的设置:

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost ~]# hostnamectl  

Static hostname: localhost.localdomain
Icon name: computer
Chassis: n/a
Machine ID: 80a4fa4970614cf6be9597ecd6f097a9
Boot ID: 28420e272e1847a583718262758bd0f7
Virtualization: vmware
Operating System: CentOS Linux 7 (Core)
CPE OS Name: cpe:/o:centos:centos:7
Kernel: Linux 3.10.0-123.el7.x86_64
Architecture: x86_64

1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]# hostnamectl status
Static hostname: localhost.localdomain
Icon name: computer
Chassis: n/a
Machine ID: 80a4fa4970614cf6be9597ecd6f097a9
Boot ID: 28420e272e1847a583718262758bd0f7
Virtualization: vmware
Operating System: CentOS Linux 7 (Core)
CPE OS Name: cpe:/o:centos:centos:7
Kernel: Linux 3.10.0-123.el7.x86_64
Architecture: x86_64

2.只查看静态、瞬态或灵活主机名,分别使用“–static”,“–transient”或“–pretty”选项。

1
2
3
4
5
[root@localhost ~]# hostnamectl --static
localhost.localdomain
[root@localhost ~]# hostnamectl --transient
localhost.localdomain
[root@localhost ~]# hostnamectl --pretty

3.要同时修改所有三个主机名:静态、瞬态和灵活主机名:

1
2
3
4
5
6
7
[root@localhost ~]# hostnamectl set-hostname Linuxidc
[root@localhost ~]# hostnamectl --pretty
Linuxidc
[root@localhost ~]# hostnamectl --static
Linuxidc
[root@localhost ~]# hostnamectl --transient
Linuxidc

就像上面展示的那样,在修改静态/瞬态主机名时,任何特殊字符或空白字符会被移除,而提供的参数中的任何大写字母会自动转化为小写。一旦修改了静态主机名,/etc/hostname 将被自动更新。然而,/etc/hosts 不会更新以保存所做的修改,所以你每次在修改主机名后一定要手动更新/etc/hosts,之后再重启CentOS 7。否则系统再启动时会很慢。

4.手动更新/etc/hosts

1
2
3
4
5
vim /etc/hosts

127.0.0.1 Linuxidc hunk_zhu
#127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain
::1 localhost localhost.localdomain localhost6 localhost6.localdomai

5.重启CentOS 7 之后(reboot -f ),

1
2
3
4
5
6
7
8
[root@Linuxidc ~]# hostname
Linuxidc
[root@hunk_zhu ~]# hostnamectl --transient
Linuxidc
[root@hunk_zhu ~]# hostnamectl --static
Linuxidc
[root@hunk_zhu ~]# hostnamectl --pretty
Linuxidc

6.如果你只想修改特定的主机名(静态,瞬态或灵活),你可以使用“–static”,“–transient”或“–pretty”选项。
例如,要永久修改主机名,你可以修改静态主机名:

1
2
3
4
5
6
7
8
9
[root@localhost ~]# hostnamectl --static set-hostname Linuxidc
重启CentOS 7 之后(reboot -f ),
[root@Linuxidc ~]# hostnamectl --static
Linuxidc
[root@Hunk_zhu ~]# hostnamectl --transient
Linuxidc
[root@Hunk_zhu ~]# hostnamectl --pretty
Linuxidc
[root@Hunk_zhu ~]# hostname

其实,你不必重启机器以激活永久主机名修改。上面的命令会立即修改内核主机名。注销并重新登入后在命令行提示来观察新的静态主机名。

转载自 http://www.linuxidc.com/Linux/2014-11/109238.htm

编译wndr4300 openwrt 15.05固件

为wndr4300编译openwrt 15.05系统,内容如下:

操作环境 ubuntu 14.04 64位

  1. 安装依赖包

    1
    sudo apt-get install subversion build-essential libncurses5-dev zlib1g-dev gawk git ccache gettext libssl-dev xsltproc
  2. 下载源码

    1
    2
    3
    4
    5
    6
    cd /home
    git clone git://git.openwrt.org/15.05/openwrt.git
    cd openwrt
    git checkout 15.05
    ./scripts/feeds update -a
    ./scripts/feeds install luci
  3. 编译 获取官方配置

    1
    wget https://downloads.openwrt.org/chaos_calmer/15.05/ar71xx/nand/config.diff

将 config.diffg 文件

1
CONFIG_TARGET_ar71xx_nand_R6100=y

修改为

1
CONFIG_TARGET_ar71xx_nand_WNDR4300=y

生成配置

1
2
cat config.diff >> .config
make defconfig

修改 /root 为128MB
修改 target/linux/ar71xx/image/Makefile 文件, 修改 wndr4300_mtdlayout 中 23552k(ubi) 为 120832k(ubi), 25600k@0x6c0000(firmware) 为 122880k@0x6c0000(firmware)

也可以使用配置向导

1
make menuconfig

运行编译

1
make V=99

  1. 生成文件位置
    最后文件生成在 bin 目录下: openwrt/bin/ar71xx/openwrt-15.05-ar71xx-nand-wndr4300-squashfs-sysupgrade.tar openwrt/bin/ar71xx/openwrt-15.05-ar71xx-nand-wndr4300-ubi-factory.img

转载自 http://www.jayclub.net/make-wndr4300-openwrt-15-05.html

针对 bs 的 collapse 写了个添加项的 func

最近在做个项目,用到了 bs 的 collapse,模拟了 select 的操作。但是其中选择的项不是很方便更改,于是写了个 function 来更改其中的 li。在这做个笔记,避免日后忘记,大神见笑。

1
2
3
4
var createPanel=function(to,id,title,lis) {
var collapseIn=typeof arguments[4]!=="undefined"?arguments[4]:$("#"+to+" .panel").length?1:0;
$("#"+to).append($('<div class="panel panel-default" id="panel_'+to+'_'+id+'"><div class="panel-heading" role="tab" id="heading_'+to+'_'+id+'"><h4 class="panel-title"><a'+(collapseIn?'':' class="collapsed"')+' role="button" data-toggle="collapse" data-parent="#'+to+'" href="#collapse_'+to+'_'+id+'" aria-expanded="'+(collapseIn?'false':'true')+'" aria-controls="collapse_'+to+'_'+id+'">'+title+'</a></h4></div><div id="collapse_'+to+'_'+id+'" class="panel-collapse collapse'+(collapseIn?'':' in')+'" role="tabpanel" aria-labelledby="heading_'+to+'_'+id+'"><ul class="list-group">'+((function() { var op=""; for (var i in lis) { op+='<li class="list-group-item" data-menuid="'+i+'">'+lis[i]+'</li>'; } return op; })())+'</ul></div></div>'));
};

用法:

1
mixed createPanel ( string appendToDomId, mixed panelId, mixed title, object liData, [ bool collapseIn ] )

1
<div class="panel-group" id="subject" role="tablist" aria-multiselectable="true"></div>
1
2
createPanel("subject",0,"学科1",{1:"Chinese",2:"English",3:"German"},true);
createPanel("subject","1","学科2",{1:"Physics",2:"Economics"});

我不忍吐槽 bs 的文档,汉化根本不完全。。。这是原文档链接 http://v3.bootcss.com/javascript/#collapse。嗯,就这样吧。。。

iptables防火墙规则的添加、删除、修改、保存

本文介绍iptables这个Linux下最强大的防火墙工具,包括配置iptables三个链条的默认规则、添加iptables规则、修改规则、删除规则等。

一、查看规则集

1
iptables --list -n // 加一个-n以数字形式显示IP和端口,看起来更舒服

二、配置默认规则

1
2
3
iptables -P INPUT DROP  // 不允许进
iptables -P FORWARD DROP // 不允许转发
iptables -P OUTPUT ACCEPT // 允许出

三、增加规则

1
2
3
4
5
6
7
iptables -A INPUT -s 192.168.0.0/24 -j ACCEPT
//允许源IP地址为192.168.0.0/24网段的包流进(包括所有的协议,这里也可以指定单个IP)
iptables -A INPUT -d 192.168.0.22 -j ACCEPT
//允许所有的IP到192.168.0.22的访问
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
//开放本机80端口
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

//开放本机的ICMP协议

四、删除规则

1
2
iptables -D INPUT -s 192.168.0.21 -j ACCEPT
//删除刚才建立的第一条规则

五、规则的保存

1
2
3
4
5
6
iptables -F
//清空规则缓冲区(这个操作会将上面的增加操作全部清空,若须保留建议先执行一下句:保存)
service iptables save
//将规则保存在/etc/sysconfig/iptables文件里
service iptables restart
//重启Iptables服务

最后说明一下,iptables防火墙的配置文件存放于:/etc/sysconfig/iptables

转载自 http://www.splaybow.com/post/iptables-rule-add-delete-modify-save.html