斐讯 N1 折腾记:运行 Linux 及优化

  咳咳咳,上篇教程教大家给斐讯 N1 降级并且刷了官改系统,可以当作一个电视盒子和下载机来使用。
  有些小伙伴可能不想把它当作电视盒子,就想把它当作 NAS 或者是服务器,但是总不能拿 Android 玩吧,Android 也不是针对服务器设计的。
  那么我们说过,N1 不仅能刷官改,还能运行 Linux,而且是完整的 Linux 发行版,用 Linux 当服务器、NAS、下载机,包括电视盒子,体验总是要比 Android 好的。
  这篇教程就教大家如何在斐讯 N1 运行 Linux 以及后续的优化,在此之前,需要将你的 N1 降级并刷入 webpad 的官改固件,或者只刷入降级关键分区(感谢群里的小伙伴反馈)。传送门:《斐讯N1折腾记:降级及刷入官改》

制作 Linux 启动盘

  N1 运行 Linux 的原理是通电的时候引导 U 盘里的系统
  所以我们需要准备一个 U 盘来制作启动盘,U 盘大小推荐不低于 8G,连续读写速度推荐达到 30M/s,随机读写速度推荐达到 10M/s。最好是买个全新的,如果是旧的,提前备份好 U 盘内数据。
  N1 运行的 Linux 发行版叫做 Armbian,玩过树莓派的小伙伴应该对这个系统不陌生,是适用于 ARM 架构的 Debian 和 Ubuntu。

下载地址

Armbian:
https://pan.baidu.com/s/1ux7zAF2QYoLWwTTB0Ah5ug 密码:axov
https://yadi.sk/d/pHxaRAs-tZiei
https://mega.nz/#F!j9QSDQSQ!6WpasOlbZYIInfw6yo4phQ
https://share.weiyun.com/5eCvcvS (部分搬运)密码:mivmcn
启动盘制作工具:https://www.alexpage.de/usb-image-tool/download/

  里面有好几个版本,下载最新的 5.44 版本,然后选择 kernel_3.14,3.14 内核在 N1 上面使用最好,WiFi、蓝牙都可以正常工作,如果你不需要蓝牙和 WiFi,也可以使用 4.16 版本。

IMG 文件名含义

例如:Armbian_5.44_S9xxx_Debian_stretch_3.14.29_server_20180601.img
Armbian:这个就不多说了
5.44:Armbian 的版本号
S9xxx:适用的 CPU
Debian:基于 Debian 编译的,另外还有 Ubuntu。
stretch:Debian 或 Ubuntu 的发行版代号,stretch 表示的是 Debian 9。
3.14.29:Linux 内核版本号
server:桌面环境,Server 代表的是服务器版,没桌面环境,其他的代表的是相应的桌面环境,比如:xfce、mate。
20180601:编译日期

  小白推荐使用 Ubuntu,桌面环境推荐使用 mate。我推荐使用 Debian Server,做一个真正的服务器。
  下面我会以 Armbian_5.44_S9xxx_Debian_stretch_3.14.29_server_20180601 版为例
  首先格式化你的 U 盘分区格式为 FAT32,推荐最好把 U 盘分区全部删除。
  打开启动盘制作工具:左侧选择你的 U 盘 → 点击 [Restore] → 选择 Linux 镜像文件 → 提示框点击 [是] → 等待制作完成
  制作完成后,会出现一个名为 BOOT 的分区,打开它。如果你把分区全部删除了,BOOT 分区默认是 FAT16 格式,Windows 是不会显示的,可以使用 Ubuntu 来访问。
  把dtb文件夹里的gxl_p230_2g.dtb复制到 BOOT 分区的根目录并重命名为dtb.img。如果是 4.16 内核,dtb 文件是meson-gxl-s905d-p230.dtb
  将 N1 断电,插上 U 盘、网线,最好是插靠近 HDMI 接口那个 USB 接口,通电开机。
  正常情况下顺利开机并进入 Linux 系统,如果没有进入 Linux 系统而是进入了 Android 系统,ADB 连接至 N1,输入adb shell reboot update
  接下来的操作我会以 SSH 远程连接为例,如果你使用 HDMI 连接显示器也可以,这种情况推荐你连接一个 USB HUB,方便连接鼠标和键盘,当然,如果你用的是 Server,那么就不需要鼠标了。
  用户:root
  密码:1234
  如何获取 N1 运行 Linux 的 IP?打开 cmd,输入nslookup amlogic,如果amlogic无法解析IP,可以尝试解析aml
  首次登陆需要重置密码,先输入当前密码(1234),再输入新密码以及确认新密码。
  设置完密码之后会让你新建一个普通用户,如果是使用 Server,直接按 Ctrl + C 跳过。如果是使用桌面版,推荐创建一个普通用户,按照提示操作即可。
  跳过之后,再次使用 SSH 连接。
  这时候,整个系统就可以用了,不过我们需要做一些小工作。

小工作

  加载 WiFi 驱动:modprobe dhd && echo dhd >> /etc/modules
  设置时区:echo "Asia/Shanghai" > /etc/timezone && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
  删除默认DNS:rm /etc/resolvconf/resolv.conf.d/head && touch /etc/resolvconf/resolv.conf.d/head && systemctl restart network-manager.service
  注:在重启系统后,输入cat /etc/resolv.conf,查看返回结果的第一行是否为nameserver 8.8.8.8,如果不是,则表示删除成功,如果是,则再执行一次删除命令。
  停止红外支持:systemctl stop lircd.service lircd-setup.service lircd.socket lircd-uinput.service lircmd.service
  删除红外支持:apt remove -y lirc && apt autoremove -y
  N1 没有红外,而且由于红外支持找不到红外,一直给系统日志写错误。
  重启系统:reboot
  更新软件包:apt update && apt upgrade -y

挂载外置存储设备

  推荐把外置存储设备分区格式化为 ext4 格式,不推荐使用 NTFS 格式。
  如果你的外置存储设备是 NTFS 格式,可以使用mkfs.ext4命令将你的外置存储设备格式化为 ext4 格式,格式化前提前备份数据。
  可以用fdisk -l查看你的外置存储设备是那个设备,一般是/dev/sdb,分区是/dev/sdb1,如果有多个分区,依次类推。
  输入mkfs.ext4 /dev/sdb1将分区格式化为 ext4,格式化完成后使用fdisk -l查看是否格式化成功,如果分区的 Type 属性为 Linux(如下图所示),表示分区为 ext4/3。

fdisk

  格式化成功后,为了方便挂载,可以给分区设置一个卷标。
  命令:e2label
  设置卷标示例:e2label /dev/sdb1 H1
  查看卷标示例:e2label /dev/sdb1
  然后就可以挂载分区了
  挂载分区示例:echo "LABEL=H1 /mnt ext4 defaults,noatime,nodiratime 0 2" >> /etc/fstab
  此命令会将卷标为 H1 的分区挂载到 /mnt 目录下
  重启系统使挂载生效

Samba 网络共享

  使用 Samba 将外置存储设备共享给其他设备
  Samba 可以使用 Armbian 自带的一个可视化菜单来配置,非常方便。
  启动 Armbian 配置菜单:armbian-config
  选择 [Software] → [Softy] → 选中 (空格) [Samba] → 回车 → 输入 samba 用户名和密码 → 工作组填写 WORKGROUP → 等待安装完成
  安装后,它会自动弹出修改配置文件的界面。

samba

  将红框里的内容删除
  将 ext 的 path 改为 /mnt,如果你的挂载路径不是 /mnt,则改为你的挂载路径。
  按下 TAB 键 → 选择 [OK] → 保存退出
  Samba 网络共享配置完成

后话

  小山觉得,既然斐讯 N1 的定位是 NAS,那么运行 Android 系统是显然满足不了 NAS 这个属性的,只有运行稳定且高效的 Linux 才可以称为 NAS。
  Linux 有着更多的扩展性,你可以在上面任意的折腾,比如搭建个开发环境,Web 服务器等等。
  总而言之,几十块钱买个 N1 这样的玩具,小山觉得是非常值的。
  还是那句话:文章有什么不懂的地方,欢迎在下方或者在QQ群告诉我。
  以后如果我想到 N1 的更多玩法会继续分享给大家的,再次感谢所有为 N1 可玩性付出的人。
  本篇文章参考了以下资料:

  转载自 https://www.mivm.cn/phicomm-n1-linux/

斐讯 N1 折腾记:降级及刷入官改

  上个月,斐讯 K2T 首发那天,小山一不小心抢了两台 K2T,这就尴尬了。退了吧,好不容易抢到的,不退吧,我只能激活一台。所以,两台 K2T 我自己用了一台,另一台我准备卖出去。然而一个月过去了,眼看K码就要到期了,还是没有人要。咋办呢,只能再注册个账号来激活这台 K2T,但是首次激活的机会不能浪费啊,所以我又买了 N1 + H1 。
  本来我根本不打算买 N1,因为我觉得这货没啥用,也没怎么关注过它。但是买来之后才发现,这货居然还能刷固件,刷完固件之后就是个电视盒子了,而且还能运行完整的 Linux。
  这就非常耐斯了,我的折腾之心按耐不住了,所以就开始一步步的折腾。
  这篇教程就先教大家给 斐讯 N1 降级以及刷入官改固件(电视盒子),这也是以后折腾的基础。

降级

  如果你的 N1 系统版本是 2.19,那么可以直接跳过降级过程,如果是其他版本,请按照下面的方法降级关键分区。
  降级准备:你需要有一根双公头的 USB 数据线,淘宝几块钱包邮,附上小山购买的链接(绝对不是广告):https://detail.tmall.com/item.htm?id=13036924933
  有些机智的小伙伴可能会说,我主板有 Type-C 接口,我拿手机的 Type-C 数据线,Type-C 这头插主板,A 头插 N1 行不行。
  这个方法是可以的,小山最开始也是这么做的。但是,只能用来降级,刷官改是不行的,这个问题在刷官改步骤会解释。

  1. 开启 ADB 调试

  非常简单,只需要用鼠标点击斐讯 N1 官方固件的那个版本号 4 次,出现 【ADB调试开启】的字样即可。

  1. 重启至 fastboot 模式

  如果经常给手机刷机的小伙伴,对这个模式肯定不陌生,也就是我们平常所说的线刷模式。
  这一步需要用到 ADB 调试工具,下载地址:https://dl.google.com/android/repository/platform-tools-latest-windows.zip
  下载完成后解压,然后打开解压后的文件夹。按住 Shift,鼠标右键点击空白处,点击 [在此处打开 Powershell 窗口] 或 [在此处打开 命令提示符 窗口]。
  输入.\adb.exe connect %IP%
  将 %IP% 替换为 N1 的 IP,也就是官方固件页面显示的那个 IP。
  示例:.\adb.exe connect 192.168.1.222
  如果返回类似connected to 192.168.1.222这样的提示,表示连接成功。
  连接成功后输入.\adb.exe shell reboot fastboot,重启至 fastboot 模式。

  1. 刷入降级关键分区

  将你的 N1 用双公头 USB 数据线与电脑连接,务必连接至靠近 HDMI 接口的那个 USB 接口,连接后查看设备管理器是否识别出新的硬件,如果新硬件驱动异常,右键新硬件点击 [更新驱动程序]。
  驱动正常后,输入.\fastboot.exe devices查看 fastboot 工具是否已识别设备。
  如果返回类似XXXXXXXXXXXX fastboot这样的提示,表示成功识别。
  注:XXXXX 为序列号
  然后就可以刷入降级分区了,分区文件下载地址:N1_V2.19_imgs.zip
  将压缩包内的分区文件解压至 ADB 工具包文件夹
  依次输入命令刷入各个分区

1
2
3
.\fastboot.exe flash boot boot.img
.\fastboot.exe flash bootloader bootloader.img
.\fastboot.exe flash recovery recovery.img

  如果没有返回任何错误信息,表示降级成功。
  输入.\fastboot.exe reboot重启设备
  至此,降级步骤就完成了,可以放心的刷官改固件了。
  可能有些小伙伴会说,系统版本哪里显示的不是 2.19 啊。这个降级并不是降级系统,而是降级引导等关键分区,所以系统版本哪里不会变的。

刷入官改

  这一步比较危险和麻烦,也正是因为这一步困扰了小山好久。
  上面我说了,小山试过用 Type-C 数据线刷官改,但是没成功,为什么呢?
  问题不是出在 Type-C 上面,而是出在那个垃圾烧录工具的驱动上面(国产芯片的烧录工具,你们懂的)。
  那个烧录工具的驱动程序虽然可以兼容 Windows 10,但是缺兼容不了最新 CPU 平台。
  小山用的是 Ryzen 平台,B350 主板,但是小山各种方法都试过了,插上去它就是识别不了。后来小山把以前的旧电脑拿出来,同一个系统环境,旧电脑插上去就识别了。所以我认为是那个驱动不支持最新的CPU平台,当然,我这里只测试了 Ryzen,酷睿系列没测试,但是我估计,酷睿 7 8 系也都不支持,如果有用酷睿新平台的小伙伴成功了记得在下面留言告诉我。
  所以说,这一步,如果你用的是 Ryzen 平台,那么还是去找一台比较旧的电脑吧。
  注:小山并没有看不起国产芯片,以上言论只针对晶晨USB烧录工具。
  首先下载USB烧录工具,下载地址:USB_Burning_Tool_v2.1.6.zip
  安装的时候记得创建桌面快捷方式,安装最后的驱动安装一定不要跳过。
  另外小山找到了一个 Linux 版本的烧录工具,但没来得及测试,有兴趣的小伙伴可以试一试:http://forum.khadas.com/t/burning-tools-for-linux-pc/1832
  安装完成后还有个小工作要做

小工作

打开设备管理器
随便点击一个硬件
点击 [操作] 菜单 → [添加过时硬件] → 下一步 → 选择 [安装我手动从列表选择的硬件] → 下一步 → 选择 [libusb-win32 Usb Devices] → 下一步 → 选择 [WorldCup Device] → 下一步 → 下一步 → 完成
Burning Tool 驱动
小工作做完之后,那个USB烧录工具才可以正常工作。

  官改固件我推荐刷 webpad 的官改固件,集成度高,也很好用,自带 Google 服务、Root、Xposed 框架、离线下载等常用软件。下载地址:http://www.right.com.cn/forum/thread-322736-1-1.html
  如果只是想当一个电视盒子用的,也可以刷 YYFROM 的语音版固件,下载地址:http://www.yyfrom.com/cms/yyfrom/product/2018-4-9/156.html
  下载你喜欢的官改固件,解压压缩包,得到 img 文件。
  打开 USB_Burning_Tool → 点击 [文件] 菜单 → 导入烧录包 → 选择官改固件的 img 文件 → 等待校验完成
  校验完成后,一定要去除勾选右边烧录配置的 [擦除flash] 和 [擦除bootloader],否则会报错。

Burning Tool

  然后点击烧录工具的 【开始】,这时候,工具就进入了待命状态。
  首先将 N1 彻底断电,然后用双公头 USB 数据线连接电脑,然后接通电源。
  如果不出意外,工具应该会识别出 N1 并且开始烧录。
  如果没有识别出 N1,而 N1 直接进入了系统。使用 ADB 连接 N1(方法见上),输入.\adb.exe shell reboot update重启至刷机模式,应该就可以正常识别了。
  小山懒得再搬出旧电脑了,这里的截图借用别人的。

Burning Tool

  等待上方那个紫色的进度条变绿,提示烧录成功,这时候不要着急拔掉 N1,先点击烧录工具的 【停止】,再关闭烧录工具,然后拔掉 N1,断电,通电开机。
  首次启动大约需要 3-4 分钟,请耐心等待。
  系统启动后,会提示遥控器配对,因为用了 T1 的一些东西,直接跳过即可。
  然后联网,安装一些直播、点播视频应用,就可以完完整整的当一个电视盒子来用了。
  操控方面,可以使用天天链里的遥控器,也可以自己买个蓝牙遥控器,这个看个人喜好。
  如果你刷了 webpad 的固件,在浏览器输入 IP:8081,既可管理离线下载、samba等软件。具体用法我就不多说了。

后话

  斐讯 N1 现在二手平台仅卖 70 元,但 N1 的配置是跟那些两三百的电视盒子不相上下的,可谓是非常的有性价比。
  如果你想入手一个电视盒子,或者是想买一个类似树莓派的玩具(不搞硬件开发)。那么可以考虑下 N1,做电视盒子绰绰有余,运行 Linux 后,就是一个没有硬件扩展性的树莓派,当然,我相信大多数人买树莓派是搞软件开发的。
  这是我目前觉得斐讯最有性价比的产品之一
  再次感谢 webpad 为 N1 可玩性做出的贡献
  下篇教程教大家如何在 N1 上运行完整的 Linux 发行版

  转载自 https://www.mivm.cn/phicomm-n1-unofficial/

流水账记录下 Centos 7 配置 frp 服务端与客户端的全过程

下载 frp:https://github.com/fatedier/frp/releases,解压到 /usr/local/frp 目录下

服务端:
编辑四个文件:
/usr/local/frp/frps_full.ini(配置文件,视情况编辑)

/usr/lib/systemd/system/frps.service

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=frps
After=network.target

[Service]
Type=forking
ExecStart=/etc/cron.hourly/frps
ExecReload=/usr/bin/frpsreload
ExecStop=/usr/bin/killall frps
PrivateTmp=true

[Install]
WantedBy=multi-user.target

/etc/cron.hourly/frps

1
2
3
4
5
6
#!/bin/sh
echo $(date +"%Y-%m-%d %H:%M:%S") > /tmp/frpTimestamp
ps aux | grep "frps -c" | grep -v grep
if [ $? -ne 0 ];then
nohup /usr/local/frp/frps -c /usr/local/frp/frps_full.ini >> /root/frp.log 2>&1 & echo $! > /var/run/frp-server.pid
fi;

/usr/bin/frpsreload

1
2
3
#!/bin/sh
kill -9 $(cat /var/run/frp-server.pid)
nohup /usr/local/frp/frps -c /usr/local/frp/frps_full.ini >> /root/frp.log 2>&1 & echo $! > /var/run/frp-server.pid

然后执行:

1
2
systemctl frps enable
service frps start

最后是 nginx 反代设置:
/usr/local/nginx/conf/vhost/pi.imjs.0cdn.cn.conf

1
2
3
4
5
6
7
8
9
10
11
server {
listen 80;
server_name pi.imjs.0cdn.cn *.pi.imjs.0cdn.cn;

location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host:8080;
}
}

客户端:
编辑四个文件:
/usr/local/frp/frpc_full.ini(配置文件,视情况编辑)

/usr/lib/systemd/system/frp.service

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=frp
After=network.target

[Service]
Type=forking
ExecStart=/etc/cron.hourly/frp
ExecReload=/usr/bin/frpreload
ExecStop=killall frp
PrivateTmp=true

[Install]
WantedBy=multi-user.target

/etc/cron.hourly/frp

1
2
3
4
5
6
#!/bin/sh
echo $(date +"%Y-%m-%d %H:%M:%S") > /tmp/frpTimestamp
ps aux | grep "frp -c" | grep -v grep
if [ $? -ne 0 ];then
ntpdate ntp1.aliyun.com && (nohup /usr/local/frp/frpc -c /usr/local/frp/frpc_full.ini >> /root/frp.log 2>&1 & echo $! > /var/run/frp-client.pid)
fi;

/usr/bin/frpreload

1
2
3
#!/bin/sh
kill -9 $(cat /var/run/frp-client.pid)
nohup /usr/local/frp/frpc -c /usr/local/frp/frpc_full.ini >/dev/null 2>&1 & echo $! > /var/run/frp-client.pid

最后执行:

1
2
systemctl frp enable
service frp start

清除windows访问Samba的访问记录

在 windows 中访问 Samba 服务器后,windows 会存储访问记录,比如密码。当我们想清除密码,重新输入时,步骤如下:
1. 获取访问记录

1
2
3
4
5
6
7
8
#在windows的命令行(cmd)中运行net use,可见访问Samba服务器的记录:
C:\Users\User>net use
会记录新的网络连接。
状态 本地 远程 网络

-------------------------------------------------------------------------------
OK \\192.168.2.1\IPC$ Microsoft Windows Network
命令成功完成。

2. 删除访问记录

1
2
C:\Users\User>net use \\192.168.2.1\IPC$ /del /y
\\192.168.2.1\IPC$ 已经删除。

3. 尝试重新登陆Samba

斐讯 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]);
}
}