斐讯 K3 流光金开箱与 TTL 刷 LEDE 教程(适用于 v21.5.39.260 系统)

注意:V21.5.39.260 集成的 CFE 编译日期为 8月2日,斐讯封堵了 CFE 网页执行命令,而且固件采用公私钥验证,除了 TTL 手动执行命令基本无法刷入其他固件(斐讯K3 官方固件root版本 安装插件 entware

本文分四段:开箱,拆机加 TTL,CFE 刷官改固件,网页刷 LEDE 固件

在刷入 LEDE 固件之前,不要给 k3 连网!避免自动升级!
在刷入 LEDE 固件之前,不要给 k3 连网!避免自动升级!
在刷入 LEDE 固件之前,不要给 k3 连网!避免自动升级!

11 月 1 号 K3 流光金首发送贼快的闪迪 U 盘,趁活动入了两台:

先上购买链接:

到手开箱全家福:

四网口 + USB 3.0:

整机照:

mmp 的系统版本 v21.5.39.260:

嗯废话说完,先给各位拆个机

第一步先扯下底部脚垫,卸下四个螺丝。注意这里有保修易碎贴,如果还要保修的话,拿电吹风吹下挑开:

第二步从如图位置插入塑料卡片撬开两侧侧面面板。最好不要用美工刀、钢尺等锐利的物品操作,会留下撬痕:

第三步拆开侧面面板,轻轻取下上方天线(注意侧面有卡扣,把塑料外壳往外掰一下就可以取出来了),取下后搁在上面就行,目的是为了卸螺丝。

第四步卸下固定两块主板用的八颗螺丝(在螺丝边上上胶的家伙我谢谢你全家):

第五步在下方主板的右侧 TX RX GND 塞 TTL 线并固定(如果要保修的话,不要固定,我是为了方便以后刷机)我用的热熔胶,如果希望牢固一点的话可以用焊锡。线可以从边上散热孔引出:

第六步接 TTL 小板调试(TX 接 RXD,RX 接 TXD,GND 接 GND),115200,如果有输出,就是正常的:

最后合上侧面面板:

PS 最后找公司的硬件工程师帮忙把引出的线改成了座儿,用热熔胶固定在了散热口:

有了 TTL,我们需要先刷入破解了 root 的官改固件并降级,为刷入 LEDE 固件做准备:

以下是需要的软件:

第一步下载固件,安装运行 Tftpd 工具。使用网线连接至 k3 上,设置静态 IP 192.168.2.100,网关 192.168.2.1,将 Tftpd 中的目录切换到你存放解压出来的固件目录,并切换网卡(应该是 192.168.2.100,我因为刷机后截的图地址不一样):

第二步给路由器断电,连接 TTL,捅 Reset 通电开机,如果成功进入 CFE 会出现下一步图中以 CFE> 开头的界面

第三步输入命令,CFE 会拉取你本机上的固件:

1
flash -noheader 192.168.2.100:/k3_root.bin nflash0.trx

第四步等写入完成后输入 reboot 重启

第五步在 TTL 输入以下命令,将 mtd6 镜像拉至路由器上并写入(镜像大小 44M,刷写时间比较长,大约需要 20 ~ 30 分钟(尽量多等一段时间),刷写过程中不要断开路由器的电源或拔网线,以免变砖!!!):

1
2
3
4
cd /tmp
tftp -g -l K3-linux-partition-mtd6.img -r K3-linux-partition-mtd6.img 192.168.2.100
cat /tmp/K3-linux-partition-mtd6.img > /dev/mtd6
reboot

最后进入功能设置 –> 手动升级看到如图系统版本就说明成功了

随后开始刷 LEDE

固件下载:

第一步刷入基础包,进入功能设置 –> 手动升级,上传:

第二步打开 192.168.1.1,进入 System –> Backup / Flash Firmware,上传升级包固件

最后重启完成,重新打开 192.168.1.1,使用 root / password 登录即可:

该固件带屏幕驱动,感谢 Lean 大的无私奉献!

参考以下文章:

  1. 拆机部分:斐讯路由器怎么样?斐讯K3拆机图解
  1. 官改固件:斐讯K3 官方固件root版本 安装插件 entware
  1. 固件降级:K3原厂固件从217版降级212版方法
  1. Lean 大 LEDE:斐讯 K3 OPENWRT LEDE R7.3 固件,Adbyby Plus,潘多拉多拨,S…

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
/**
* Created by PhpStorm.
* User: qishou
* Date: 15-8-2
* Time: 上午12:00
*/
//准备数组,代替从数据库中检索出的数据(共有三个必须字段id,name,pid)
header("content-type:text/html;charset=utf-8");
$categories = array(
array('id'=>1,'name'=>'电脑','pid'=>0),
array('id'=>2,'name'=>'手机','pid'=>0),
array('id'=>3,'name'=>'笔记本','pid'=>1),
array('id'=>4,'name'=>'台式机','pid'=>1),
array('id'=>5,'name'=>'智能机','pid'=>2),
array('id'=>6,'name'=>'功能机','pid'=>2),
array('id'=>7,'name'=>'超级本','pid'=>3),
array('id'=>8,'name'=>'游戏本','pid'=>3),
);

/*======================非递归实现========================*/
$tree = array();
//第一步,将分类id作为数组key,并创建children单元
foreach($categories as $category){
$tree[$category['id']] = $category;
$tree[$category['id']]['children'] = array();
}
//第二步,利用引用,将每个分类添加到父类children数组中,这样一次遍历即可形成树形结构。
foreach($tree as $key=>$item){
if($item['pid'] != 0){
$tree[$item['pid']]['children'][] = &$tree[$key];//注意:此处必须传引用否则结果不对
if($tree[$key]['children'] == null){
unset($tree[$key]['children']); //如果children为空,则删除该children元素(可选)
}
}
}
////第三步,删除无用的非根节点数据
foreach($tree as $key=>$category){
if($category['pid'] != 0){
unset($tree[$key]);
}
}

print_r($tree);

/*======================递归实现========================*/
$tree = $categories;
function get_attr($a,$pid){
$tree = array(); //每次都声明一个新数组用来放子元素
foreach($a as $v){
if($v['pid'] == $pid){ //匹配子记录
$v['children'] = get_attr($a,$v['id']); //递归获取子记录
if($v['children'] == null){
unset($v['children']); //如果子元素为空则unset()进行删除,说明已经到该分支的最后一个元素了(可选)
}
$tree[] = $v; //将记录存入新数组
}
}
return $tree; //返回新数组
}
echo "<br/><br/><br/>";

print_r(get_attr($tree,0));

转载自 http://blog.csdn.net/qishouzhang/article/details/47204359

aria2配置示例

其实面对man的存在,写什么总结完全没有必要,一切宝藏都在manual。不过反正不会有人会读就是了。那我就写一下吧

##基础
首先,aria2或者叫做aria2c,它是一个下载器,嗯。
常用的两种模式是直接下载,比如 aria2c “http://host/file.zip" 这样,当它完成后就退出了,就像wget(估计你们也不知道吧)那样。
另一种就是rpc server模式,特点就是,它启动之后什么都不干,然后等着从rpc接口添加任务,下载完也不退出,而是一直等着。对,就像迅雷干的那样,当然,它不会上传你硬盘上的数据。
因为第一种方式要每次都敲命令,除非像我是原生nix,没有命令行就没法用电脑,估计也没什么用,于是常用的就是第二种。一般启动命令是 aria2c –enable-rpc –rpc-listen-all=true –rpc-allow-origin-all -c -D 。但是,其实*这个命令是不好的!不要使用这种启动方式。
首先,用命令方式导致配置不方便修改保存,-D导致无法看到出错信息。
推荐启动方式是使用配置文件 $HOME/.aria2/aria2.conf 。嗯,我知道路由上这个地址是无法修改或者重启后会丢失的,那么你可以放到别的地方,然后 aria2c –conf-path= 注意 填完整路径,因为鬼知道这个程序是从那个路径启动的。-D (用于后台执行, 这样ssh断开连接后程序不会退出) 只有在确认OK之后在启动脚本中使用。

以下方案都基于配置文件方式

##图形界面
aria2是没有图形界面的,已知相对好用的图形界面有:

请使用chrome,firefox等现代浏览器访问。这两个东西都可以直接使用,除了看英文不爽以外,有什么必要下载回来使用?(吐槽:难道你们就不觉得webui-aria2的title总是被压成好几行,诡异的配色(对,说的就是那个蓝色背景,深蓝颜色的 Use custom IP and port settings 按钮)不难看吗?)
图形界面基本都基于RPC模式,所以一定确定开启了RPC,IP端口可访问,并且在管理器中填写了正确的地址

##配置
请将所有配置置于配置文件中
只有在确认配置无误后再加上 -D 选项
请阅读出错信息!

###RPC
需要1.14及以上版本
http://aria2.sourceforge.net/manual/en/html/aria2c.html#rpc-options

1
2
3
4
5
6
7
8
#允许rpc
enable-rpc=true
#允许所有来源, web界面跨域权限需要
rpc-allow-origin-all=true
#允许非外部访问
rpc-listen-all=true
#RPC端口, 仅当默认端口被占用时修改
#rpc-listen-port=6800

如果启动时出现 Initializing EpollEventPoll failed. 或相似错误, 在配置中加上 event-poll=select

使用token验证(建议使用,需要1.18.4以上版本,帐号密码方式将在后续版本中停用!)

1
2
# token验证
rpc-secret=secret

在YAAW中使用 http://token:secret@hostname:port/jsonrpc 的地址格式设置secret.
如果需要使用密码验证(需要1.15.2以上,1.18.6以下版本)

1
2
3
4
#用户名
rpc-user=username
#密码
rpc-passwd=passwd

在YAAW中使用 http://username:passwd@hostname:port/jsonrpc 的地址格式设置密码.
对于RPC模式来说, 界面和后端是分离的, 只要给后端设置密码即可. 前端认证什么的是毫无意义的.
如果你比较新潮, 在YAAW中也可以用 ws:// 为前缀,只用websocket连接aria2c, 如果你不知道websocket是什么. 那就算了.

###速度相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#最大同时下载数(任务数), 路由建议值: 3
max-concurrent-downloads=5
#断点续传
continue=true
#同服务器连接数
max-connection-per-server=5
#最小文件分片大小, 下载线程数上限取决于能分出多少片, 对于小文件重要
min-split-size=10M
#单文件最大线程数, 路由建议值: 5
split=10
#下载速度限制
max-overall-download-limit=0
#单文件速度限制
max-download-limit=0
#上传速度限制
max-overall-upload-limit=0
#单文件速度限制
max-upload-limit=0
#断开速度过慢的连接
#lowest-speed-limit=0
#验证用,需要1.16.1之后的release版本
#referer=*

###进度保存相关
aria2c只有在正常退出时(ctrl-c), 突然断电是无法保存进度的. 在第一次使用的时候会出现会话文件不存在的错误, 手动创建一个空文件即可. 如果您编写的是自动启动脚本,在启动aria2前加上 touch aria2.session 这句命令。

1
2
3
4
input-file=/some/where/aria2.session
save-session=/some/where/aria2.session
#定时保存会话,需要1.16.1之后的release版
#save-session-interval=60

###磁盘相关

1
2
3
4
5
6
7
8
9
#文件保存路径, 默认为当前启动位置
dir=/some/where
#文件缓存, 使用内置的文件缓存, 如果你不相信Linux内核文件缓存和磁盘内置缓存时使用, 需要1.16及以上版本
#disk-cache=0
#另一种Linux文件缓存方式, 使用前确保您使用的内核支持此选项, 需要1.15及以上版本(?)
#enable-mmap=true
#文件预分配, 能有效降低文件碎片, 提高磁盘性能. 缺点是预分配时间较长
#所需时间 none < falloc ? trunc << prealloc, falloc和trunc需要文件系统和内核支持
file-allocation=prealloc

###BT相关
http://aria2.sourceforge.net/manual/en/html/aria2c.html#bittorrent-specific-options

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
#启用本地节点查找
bt-enable-lpd=true
#添加额外的tracker
#bt-tracker=<URI>,…
#单种子最大连接数
#bt-max-peers=55
#强制加密, 防迅雷必备
#bt-require-crypto=true
#当下载的文件是一个种子(以.torrent结尾)时, 自动下载BT
follow-torrent=true
#BT监听端口, 当端口屏蔽时使用
#listen-port=6881-6999
aria2亦可以用于PT下载, 下载的关键在于伪装
#不确定是否需要,为保险起见,need more test
enable-dht=false
bt-enable-lpd=false
enable-peer-exchange=false
#修改特征
user-agent=uTorrent/2210(25130)
peer-id-prefix=-UT2210-
#修改做种设置, 允许做种
seed-ratio=0
#保存会话
force-save=true
bt-hash-check-seed=true
bt-seed-unverified=true
bt-save-metadata=true
#定时保存会话,需要1.16.1之后的某个release版本(比如1.16.2)
#save-session-interval=60

##常见问题

###Internal server error
手动访问你的JSON-RPC地址 http://hostname:port/jsonrpc?jsoncallback=1 如果没有返回, 请确认aria2是否启动以及连通性. 如果aria2在路由器后或没有公网IP, 请做好端口映射.

###如何使用迅雷离线
http://binux.github.com/ThunderLixianExporter/
安装后, 在迅雷离线的右上角的设置中设置RPC地址.
提供chrome插件: https://chrome.google.com/webstore/detail/thunderlixianassistant/eehlmkfpnagoieibahhcghphdbjcdmen

###如何使用旋风离线(QQ离线)
http://userscripts.org/scripts/show/142624
安装脚本后, 在旋风离线页面使用.

转载自 https://binux.blog/2012/12/aria2-examples/

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