Author Archives: wolftankk

解决因jbd2导致mysql性能过低问题

在几个月前,有同事因为需要对某项功能进行压力测试,测试mysql的读写性能。当时测下来的结果非常不满意,本地测试环境性能巨差无比,当时我帮忙找问题,top看cpu、内存完全没有问题,iotop看到有jbd2 I/O占用到了99%。压力测试一结束,也随之降下来。当时排查了一段时间,始终没有搞定,就放弃了。就在这周,还是需要测试mysql性能,还是因为这个问题,我就花了一个下午的时间,终于解决了。

首先我们应该了解一下jbd2是什么东西。为什么I/O会如此之高。随便google一下,都有问到jbd2如何关闭。但是没有一个答案是能搞定的。wikipedia下是这样定义的(JBD):

The Journaling Block Device (JBD) provides a filesystem-independent interface for filesystem journaling. ext3, ext4 and OCFS2 are known to use JBD. OCFS2 starting from linux 2.6.28 and ext4 use a fork of JBD called JBD2

那如何关闭JBD2呢?

首先用 dumpe2fs /dev/sda6 | more 查看Filesystem Feature下有木有has_journal。如果没有就不用看了 -_-

tune2fs -o journal_data_writeback /dev/sda6
tune2fs -O "^has_journal" /dev/sda6
e2fsck -f /dev/sda6

同时在fstab下重新设定一下,在defaults之后增加

defaults,data=writeback,noatime,nodiratime

重启之后,在使用dumpe2fs查看,若没有了,那说明已经把jbd关闭了。

如果使用tune2fs时候,提示disk正在mount,如果是非系统盘下,你可以使用

fuser -km /home #杀死所有使用/home下的进程
umount /dev/sda6 #umount

之后在使用上面的命令进行移除has_journal

你现在在google上搜索“jbd2/sda1-8 high io”,仍然没有一个完整的解决方案,希望这文章能帮到你。

如何找出僵死进程的父进程

今天在查看进程的时候,看到无数个cat,就像这样
Screen Shot 2015-03-24 at 23.14.53

然后我killall杀掉进程后变成这样:

Screen Shot 2015-03-24 at 23.16.09

但这个时候死活杀不掉了。。这是为毛??接下来我只能通过查看进程状态才行了,如下:

> cat /proc/32741/status                                                                                                      
Name:	cat
State:	Z (zombie)
Tgid:	32741
Ngid:	0
Pid:	32741
PPid:	32710
TracerPid:	0
Uid:	1000	1000	1000	1000
Gid:	100	100	100	100
FDSize:	0
Groups:	2 7 50 91 92 93 95 96 100
Threads:	1
SigQ:	27/63909
SigPnd:	0000000000000000
ShdPnd:	0000000000004000
SigBlk:	0000000000000000
SigIgn:	0000000000000000
SigCgt:	0000000180000000
CapInh:	0000000000000000
CapPrm:	0000000000000000
CapEff:	0000000000000000
CapBnd:	0000003fffffffff
Seccomp:	0
Cpus_allowed:	ff
Cpus_allowed_list:	0-7
Mems_allowed:	00000000,00000001
Mems_allowed_list:	0
voluntary_ctxt_switches:	2
nonvoluntary_ctxt_switches:	1

查出PPid,就是parent pid(父进程id),有了这个PPid,再查出导致此问题的进程,把它kill掉即可!

> cat /proc/32710/status                                                                                                     
Name:	chrome
State:	S (sleeping)
Tgid:	32710
Ngid:	0
Pid:	32710
PPid:	1
TracerPid:	0
Uid:	1000	1000	1000	1000
Gid:	100	100	100	100
FDSize:	256
Groups:	2 7 50 91 92 93 95 96 100
VmPeak:	  341320 kB
VmSize:	  341080 kB
VmLck:	       0 kB
VmPin:	       0 kB
VmHWM:	   47000 kB
VmRSS:	   47000 kB
VmData:	    1660 kB
VmStk:	     136 kB
VmExe:	   90652 kB
VmLib:	   60344 kB
VmPTE:	     464 kB
VmSwap:	       0 kB
Threads:	1
SigQ:	27/63909
SigPnd:	0000000000000000
ShdPnd:	0000000000000000
SigBlk:	0000000000000000
SigIgn:	0000000000000000
SigCgt:	0000000180000000
CapInh:	0000000000000000
CapPrm:	0000000000000000
CapEff:	0000000000000000
CapBnd:	0000003fffffffff
Seccomp:	0
Cpus_allowed:	ff
Cpus_allowed_list:	0-7
Mems_allowed:	00000000,00000001
Mems_allowed_list:	0
voluntary_ctxt_switches:	261533773
nonvoluntary_ctxt_switches:	45982
> killall chrome

查出来的结果!居然是chrome!!

PHP中执行Mongo的一个性能问题

最近正好在使用mongodb,在完成基础功能的时候,我习惯性的会对其性能进行一些测试,首先在mongo 2.6.4下进行测试,php版本会5.6。测试工具我使用的wrt,这是一个非常好用的压力测试工具,而且可以配合lua脚本,测试和聚合。

在Mongo 2.6.4下测试情况

由于业务的需要,我用500个并发+1分钟请求来进行了测试,主要针对读取,插入,更新已经调用执行js脚本。

1. mongo数据读取操作,结果是1320 reqs/sec。

2. mongo数据插入操作,结果是1240 reqs/sec。

3. mongo数据更新操作,结果是1250 reqs/sec。

以上三个结果,我还算能接受,但是接下来的测试,让我彻底惊到了。

由于业务上的需求,我在设计整个构架的时候,是考虑把一部分逻辑放在mongo中调用js直接执行的。测试脚本是

function getGameInfo(gameid) {
   if (gameid == undefined) {
       return false;
   }

   game = db.games.findOne({ gameid : gameid });

   if (game) {
      return game.toArray();
   }

   return false;
}

然后在php中执行,php的脚本为

$client = new MongoClient("mongodb://localhost:27017");
$db = $client->selectDB("test");
$code = "function getGameInfo(gameid) {
   if (gameid == undefined) {
       return false;
   }
   game = db.games.findOne({ gameid : gameid });
   if (game) {
      return game.toArray();
   }
   return false;
}";

$db->execute($code, array(10));
$client->close();

测试下来的结果只有35reqs/sec,而且mongo还报了连接数过多。首先mongodb的链接,在php中默认就是长链接,也就是说链接数过多,因为前面的数据执行了没有断开链接。但是我明明关闭链接了。

我Google了一下,在mongo中select是non-block,insert和update是collection级锁,而运行js脚本是全局锁,同时在mongo2.6.4下js是spidermonkey。主要原因找到了,因为全局锁,就必须等待前面一个脚本运行完成返回结果才行。同时现在Mongo已经升级到了3.0,它的JS引擎也从spidermonkey变成了V8,但同时在3.0,execute/eval函数了已经deprecated。

在Mongo 3.0.0下测试情况

MongoDB已经正式发布了3.0,根据官方的测试提高接近4倍的性能,我以1000个并发+1分钟请求进行测试,其结果确实让人满意不少。

1. mongo数据读取操作,结果是2320 reqs/sec。

2. mongo数据插入操作,结果是1840 reqs/sec。

3. mongo数据更新操作,结果是1950 reqs/sec。

探求PHP-FPM最佳运行模式

我们安装php,在配置PHP-FPM的时候一般搜到的配置,大多数都是一样的,推荐你使用’dynamic’进程管理(process manager,在配置中简称位pm)模式来运行。虽然这没什么问题,但是本文将会探究最佳运行模式。

为什么要选择’ondemand’来代替’dynamic’来做为进程管理器

在大多数的配置指南中,都是这样配置的,然后你会直接复制粘贴放在你的配置文件中。

Read more »

在路由器配置openvpn

首先你的路由器必须可以刷dd-wrt或者openwrt这种三方的rom和可使用的openvpn服务。然后才能来谈论如何翻墙。

在这里推荐新款的路由器,比如我现在就在使用的netgear wndr3700 v4。优势在于有128m的flash。首先我们先给路由器刷上最新的dd-wrt系统。这里可以google,教程很多这里不再多说。

当你配置一些配置可以正常上网后,我们就开始配置路由器上的openvpn客户端。

在你配置路由器的时候,你会发现,“咦,这里可以配置openvpn client。”

我想告诉你,这里配置了是用不了的。

1、 开启sshd服务
服务 -> 服务 -> Secure Shell 启用sshd服务

sshd服务启用

2、 开启ssh远程管理
管理 -> 管理 -> 远程管理 启用SSH管理

启用ssh远程管理

注意:这里一定要按照顺序来操作。否则第二步的按钮是被禁用状态。

3、 启用jffs
管理 -> 管理 -> JFFS2支持

开启jffs2

如果你看到总共大小不符(这里主要说netgear wndr3700v4),你可以如下操作:

首先ssh连接进你的路由器, ssh root@路由器地址 密码为你设定的路由器密码

root@DD-WRT:~# mkfs.jffs2 -o /dev/mtdblock/3 -n -b -e 0x20000 

root@DD-WRT:~# mount -t jffs2 /dev/mtdblock/3 /jffs 

root@DD-WRT:~# mount 
   rootfs on / type rootfs (rw) 
   /dev/root on / type squashfs (ro,relatime) 
   proc on /proc type proc (rw,relatime) 
   sysfs on /sys type sysfs (rw,relatime) 
   debugfs on /sys/kernel/debug type debugfs (rw,relatime) 
   ramfs on /tmp type ramfs (rw,relatime) 
   none on /dev type tmpfs (rw,relatime,size=512k) 
   devpts on /dev/pts type devpts (rw,relatime,mode=600) 
   devpts on /proc/bus/usb type usbfs (rw,relatime) 
   /dev/mtdblock/3 on /jffs type jffs2 (rw,relatime)

4、 配置openvpn
配置openvpn client,网上教程非常之多。这里只说几个重点:
4.1 配置文件请放在/jffs目录下。 我推荐是在/jffs目录下创建一个openvpn目录,将所有配置放进去。

4.2 配置完文件之后, 首先使用openvpn /jffs/openvpn/client.conf 文件进行一下测试,是否可以连接成功

4.3 配置nat,这里和一般的客户端所有不一样,需要做一个nat转发,你可以在openvpn目录下创建一个nat.sh脚本,并chmod a+x,里面写入

#!/bin/sh
iptables -t nat -A POSTROUTING -o tun1 -j MASQUERADE

tun1 为openvpn连接成功后的eth。

4.4 以上三步完成之后,你可以现在windows上进行一下测试,能否ping同twitter。

4.5 如其他openvpn一样,这个需要一个只能路由脚本,来判断是国内还是国外。脚本下载地址:
vpnup.sh
下载之后放到路由器/jffs/openvpn/目录下,进行重命名
mv vpnup.sh.txt vpnup.sh
chmod a+x vpnup.sh

4.6 配置自动运行脚本
在/jffs/openvpn目录下创建一个可以直接run.sh脚本

#!/bin/sh

sh /jffs/openvpn/nat.sh
sh /jffs/openvpn/vpnup.sh
openvpn /jffs/openvpn/client.conf

然后打开路由器设置页面在管理-》命令中加入
/jffs/openvpn/run.sh
保存为启动指令。

now, Enjoy it!

Nginx porxy的一些是使用总结

一、前言

Nginx的Proxy功能很强大,可以做负载均衡,可以做反向代理,可以做页面缓存等等等功能。今天就来详细说一下在我使用的一些经验之谈。也许里面就有所需要的。

Nginx的porxy默认就是自带的,无须任何第三方的模块就可以直接使用,至于nginx的配置以及安装,直接google查一下,相关的文章非常之多。

二、反向代理

1. proxy_pass

指定nginx需要代理谁。语法: proxy_pass URL。作用域在location。首先我们可以来尝试一下,将设你已经有了一个网站 http://a.com,但是我需要http://b.com访问相同的a.com。这个时候你可以这样,

server {
   listen 80;
   server_name b.com;
   index index.html;
   location / {
     proxy_pass http://a.com;
   }
}

现在你访问的b.com就是nginx把a.com反向代理回来的结果。这个时候,你需要问,我程序在a.com需要获得用户的IP地址这些,全是b.com的地址呀。那这个时候就需要下面一个命令了。

2. proxy_set_header

发送给原服务器的时候增加或者修改请求头的信息。 语法: proxy_set_header key value。作用域在location。

proxy_set_header        Host            $host;
proxy_set_header        X-Real-IP       $remote_addr;
proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;

把这3行加入到刚刚前面的配置下面,这时候a.com就能获得真实的用户IP地址了。

三、负载均衡

网站发展初期可能只有一台服务器,但是随着你的业务增长,你的服务器越来越多。这时候,你需要服务器能智能的分配用户到不同的服务器上。现有方式的有两种,第一种通过DNS,第二种就是我们接下来要说的nginx负载均衡了。

1. upstream
Nginx的upstream通proxy一样,默认就自带了。通过upstream他将会智能的分配后台的服务器。首先我们看一下配置

upstream backend {
    ip_hash;
    server backend1.example.com       weight=5;
    server backend2.example.com:8080;
    server backup1.example.com:8080   backup;
    server backup2.example.com:8080   backup;
}

server {
 listen 80;
 server_name a.com;
 index index.html;
 location / {
  proxy_pass http://backend; #这里填的就是upstream。必须要有http头
  proxy_set_header        Host            $host;
  proxy_set_header        X-Real-IP       $remote_addr;
  proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
 }
}

现在你访问a.com,nginx将会自动向你分配一台服务器为你服务。可能上面这些配置你看得一头雾水,下面将会一一进行详解。
如何添加负载均衡呢? 通过upstream创建, 语法 upstream NAME。

1.1 server
语法: server address [parameters]
将指定的服务器绑定在负载均衡上,可以使用网址、ip地址或者unix://地址。如果没有特别指定端口号的话,默认就为80。就如上面backup1.example.com:8080 和 backend1.example.com。 一个是指向8080端口,一个是指向80端口。

parameters参数详解:
1.1.1 weight=number
服务器权重,默认为1。值越大被访问的几率也就越大。

1.1.2 max_fails=number
允许最大请求失败数,默认为1。当超过最大次数时,将会在proxy_next_upstream设定的错误。

1.1.3 fail_timeout=time
在经历了max_fails次失败后,暂停服务的时间。max_fails可以和fail_timeout一起使用。

1.1.4 backup
预留的备份机器。当其他所有的非backup机器出现故障或者忙的时候,才会请求backup机器,因此这台机器的压力最轻。

1.1.5 down
表示当前的server暂时不参与负载均衡。只作用于ip_hash轮询。

另外一些非常用的参数:
1.1.6 max_conns=number
设定单台服务器的最大连接数(从1.5.9版本生效)。默认为0, 0表示无连接数限制。

1.1.7 resolve
监控域名地址的变化,将会自动更改解析地址,不需要重启服务器。(从1.5.12版本生效)。特别要说明的是,如果要设定resolve,必须要设定在http作用域范围内。

http {
    resolver 10.0.0.1;
    upstream u {
        zone ...;
        ...
        server example.com resolve;
    }
}

1.1.8 route=string
设定服务器的路由名称

1.1.9 slow_start=time
设定一个恢复正常的服务器从0到设定的weight值所需的时间。主要是针对 服务器挂了重启,访问过慢等等情况。默认为0,禁用。

1.2 负载均衡的分配方式
nginx有多种负载均衡的方式。在默认情况下是使用轮询的方式,即通过weight值来轮询访问。第二种为ip_hash,每个请求按访问IP的hash结果分配,这样来自同一个IP的访客固定访问一个后端服务器,有效解决了动态网页存在的session共享问题。

其余还有health_check, keepalive等等。详细可以预约官方文档: http://nginx.org/en/docs/http/ngx_http_upstream_module.html#health_check

四、页面缓存

说完负载均衡,现在来说说页面缓存。Nginx的页面缓存功能与专业的(squid)web缓存器来比一点也不差。当然配置起来也有点小麻烦。Nginx的proxy默认请下是没有任何配置的所以,你首先要指定缓存规则,缓存的路径等等信息。
1. proxy_cache_path PATH [levels=levels] keys_zone=name:size [inactive=time] [max_size=size] [loader_files=number] [loader_sleep=time] [loader_threshold=time];

PATH缓存的目录,levels参数指定缓存的子目录数。levels指定目录结构,可以使用任意的1位或2位数字作为目录结构,如 X, X:X,或X:X:X 例如: “2”, “2:2”, “1:1:2“,但是最多只能是三级目录。 所有活动的key和元数据存储在共享的内存池中,这个区域用keys_zone参数指定。name指的是共享池的名称,size指的是共享池的大小,可以使用k,m,g。如果在inactive参数指定的时间内缓存的数据没有被请求则被删除,默认inactive为10分钟。一个名为cache manager的进程控制磁盘的缓存大小,它被用来删除不活动的缓存和控制缓存大小,这些都在max_size参数中定义,当目前缓存的值超出max_size指定的值之后,超过其大小后最少使用数据(LRU替换算法)将被删除。

2. proxy_cache_key
设定了缓存路径,接下来就需要缓存key,避免数据重复。默认为$scheme$proxy_host$request_uri 。 你可以添加nginx下的各种参数来组合不同的key值

3. proxy_cache
设置一个缓存区域的名称,一个相同的区域可以在不同的地方使用。
在0.7.48后,缓存遵循后端的”Expires”, “Cache-Control: no-cache”, “Cache-Control: max-age=XXX”头部字段,0.7.66版本以后,”Cache-Control:“private”和”no-store”头同样被遵循。nginx在缓存过程中不会处理”Vary”头,为了确保一些私有数据不被所有的用户看到,后端必须设置 “no-cache”或者”max-age=0”头,或者proxy_cache_key包含用户指定的数据如$cookie_xxx,使用cookie的值作为proxy_cache_key的一部分可以防止缓存私有数据,所以可以在不同的location中分别指定proxy_cache_key的值以便分开私有数据和公有数据。
缓存指令依赖代理缓冲区(buffers),如果proxy_buffers设置为off,缓存不会生效。

注意: 如上面所说,nginx是根据expires,max-age等信息来进行缓存的,如果你需要强制缓存的话,你可以使用proxy_ignore_headers指令,强制进行缓存。

4. proxy_cache_valid
设定缓存的时间。例如对应200 10m, 对应404 1h 写法如下:

proxy_cache_valid 200 10m;
proxy_cache_valid 404 1h;
proxy_cache_valid any 1d;

5. proxy_cache_use_stale , proxy_next_upstream
设定下次更新缓存的触发条件。

6. $upstream_cache_status
在配置底部增加一条set_header X-Cache $upstream_cache_status 这时候你访问页面的时候,就可以了解当前页面是否被nginx缓存成功。一共有多种状态:MISS, EXPIRED, HIT

需要了解更多信息,可以仔细查看官方的proxy文档,文档是介绍指令的方式来说明的,因此有些你需要的东西,可能要花一定的时间全部看完,才知道。就如怎么强制进行缓存。我是看了四五遍之后才发现,可以如此干。

Nginx还有很多功能还没发掘,需要在实际使用中好好研究。会有更多收获。

了解正则中的特殊匹配技巧

正则,不一定每个程序员都会使用,正则中的语法也不是每一个人都能非常的了解,每个语言对正则的支持度又是一样的,一般来说正则表达式有POSIX类,PERL类等等。在这里不会把正则的初始教程也写下来,主要为已经会使用正则,需要知道一些技巧的朋友提供一些帮助。在这里主要以python为主,正则大致上都是相同的,所以如果用不了,可以看看所用语言的文档。

首先来说一下正则的一些基础知识:

  1. 正则中的一些特殊字符
  2. Read more »

Spyder介绍

Spyder这个项目本来也没打算要写,但是最近因为一些业务上的需求,需要重新使用,就顺手写一点介绍。Spyder是一个用python2写出来的页面抓取工具,代码托管在Github上。原先Spyder是需要mysql才行的,这次改版我把管理界面和采集器本身进行了一次分离。因此这一次只来说说采集器的本身,Web管理界面等到下一次再说吧。

在src目录里spyder可以采集器,web为web管理界面,libs放了一些通用的函数在里面。用Spyder前,你需要安装lxml。这个是一个非常有用的库,可以对采集回来的html数据进行dom操作。

Spyder中最基础的单元为Seed,也就是所谓的种子。一个种子他包含了你需要采集的需求。看下这例子:

import os, sys
parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)));
if parentdir not in sys.path:
    sys.path.insert(0, parentdir)

from spyder.seed import Seed

config = {
    'listtype': u'html',
    'tries': 5,
    'frequency': 7200,
    'lang': u'zhCN',
    'seed_name': u'抓取在线人数',
    'enabled': 1,
    'rule': {
        'urlformat': 'http://www.douyu.tv/directory/all?offset=$page&limit=30',
        'pageparent': '',
        'maxpage': 25,
        'step': 30,
        'startpage': 0,
        'contenturl': '',
        'listparent': 'div[id="item_data"] ul li',
        'urltype': 'createLink',#链接模式
        'contentparent': 'a[class="list"]',
        'zero': 1,
        'entryparent': '',
        'filters': [
            #filterid, value, fetch_all, type(content/list)
        ],
        'extrarules':[
            ('title', 'h1[class="title"].text()', 0, 'list'),
            ('view', 'span[class="view"].text()', 0, "list"),
            ('name', 'span[class="nnt"].text()', 0, "list"),
            ('game', 'span[class="zbName"].text()', 0, "list"),
        ]
    },
    'timeout': 5,
    'sid': 1000L
}

seed = Seed(config);

上面就是一个抓取某网站直播在线人数的配置文本。这里包含了listtype(列表页面类型:html, json, feed)、tries(尝试次数,默认为5次)、seed_name(种子名称)、sid(种子ID,必须要分配一个)、enable(是否启用)、rule(采集配置)。
rule中包含了基础的采集信息和过滤器以及额外需要抓取的数据。 先来说说基础的

  1. 生成列表链接
  2. urlformat 列表页面模板,这个需要和下面的urltype结合在一起使用。一般来说需要抓取的列表页面都是有规则的。所以我在urltype中设定了三种模式inputLink(自定义模式)、createLink(根据设定的step, maxpage, startpage,来生成列表链接)、dateLink(根据设定的日期格式)。 在这些列表中可选参数是$page。这个用于指定需要填充的位置。比如上面例子中的,就是createLink类型。根据所指定的规则,他会生成为:
    http://www.douyu.tv/directory/all?offset=0&limit=30
    http://www.douyu.tv/directory/all?offset=30&limit=30

  3. 提取列表及获取文章链接
  4. 在你生成列表链接之后,就可以开始抓取了。抓取下来的都是html页面。这个时候你需要配置需要采集的列表区域以及文章链接。这里需要涉及listparent,contentparent。这里的配置很简单,如果你熟悉jquery的话,那么配置起来相当简单。

上面已经简单的描述了如何去获取列表内容。在这里,可能我会囉嗦几句关于配置正则方面的东西。先来说下extrarules。这里面包含了你需要采集的其他信息。rule本身只是去抓取页面和获取文章链接,其余信息是都没有的。所以你需要配置extrarules让Spyder知道你需要从页面上获取哪里信息。

('title', 'h1[class="title"].text()', 0, 'list'),

上面这个就是一个简单的extrarules中的一条。 分别对应name,parent, fetchall, type。 name就是在你后期获取数据时候的一个key。第二个为抓取正则,这个正则如同上面一样,都是和jquery差不多。获取文字用text(),如果你要获取html就用html()。这个就可以在浏览器先试成功之后,再复制粘帖进来。第三个为是否全部抓取,第四个为类型(list列表, content 文章)。

除了extrarules还有一个叫做filters,过滤器功能可以用做替换,可以改文本内容。这个下次时候我在述说。

通过上面的配置,一个采集的种子已经配置完成了。

第二步就是数据的采集过程了。

import os, sys
parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)));
if parentdir not in sys.path:
    sys.path.insert(0, parentdir)

from spyder.seed import Seed
from spyder.document import Grab

seed = Seed(config);
data = Grab(seed);

你只要引用document中的Grab就可以了。他会采集完成后的数据全部放在items中。你可以只需要进行数据遍历即可。

以上简单介绍了一个Spyder,它与其他采集器不一样的地方在于灵活,配置方便。可以用于各种情况。项目在一年前写的,但是集成了相当多的功能,比如图片存储转换链接,存储到数据库,可扩展插件等等。目前我正在所以对代码进行一次整理,这篇文章也算是对原有代码逻辑的一次梳理。

利用Redis开发预约系统

这次来讲讲redis的一些高级使用机制,redis就不在这里过于介绍了,不了解的可以google,可以wiki,也可以去redis官网看看。上面都有它的历史和使用说明。

Redis主要提供了集中类型:字符串,hash,list,有序/无序集合。每个类型都有不同的使用场景,例如字符串可以保存一些固定key的数据。比如用户基础数据等等。Redis是一个单进程单线程的内存数据库,对于并发来说,基本上不会出现重复的问题,同时它所处理的速度也是极快的。因此在门票预约或者发红包的时候,利用Redis这一特性,可以很好的解决并发所带来的问题,同时也提高了运行效率,处理速度。

对于门票预约来说,一般都会事先生成好所有的座位,这里的座位我们称之为ticket。Redis中List是提供了push和pop功能。第一步需要先把所有生成的ticket放入list中,用户在点击预约时,用lpop命令弹出一个。如果想退订或者失败回滚处理用rpush命令插入。另外,门票每个人只能预约一张,你可以创建一个hash表,用userid做为这个hash表的key,在每次获取订单的时候,你可以去这个hash表上查询,此userid时候已经存在,若不存在,可以将预约成功者的userid以及ticket、状态、时间。存储到这个hash表中。

在这套预约系统中,有一个问题,就是我创建了这个预约事件,那么我想调整人数时候就遇到难题了。增加人数还好说,通过差值,在list中rpush那些数量的ticket。而需要减少的时候,可就没办法了。除非你把这个预约事件全部取消并删除。

在并发测试的时候,没有遇到同时会分配到两个相同的ticket。而且redis的并发量,超出了我们的想象,若是只使用Mysql这类关系数据库,对服务器的压力可能会有所增加,同时,mysql因为行锁原因,可能会比Redis低一些。

通过我上面那么多的废话,你设计这套只需要4个:
1. ticketqueue:2 ticket队列表 后面的数字代表预约事件id。 list类型
2. orderedlist:2 成功预约列表。hash表类型
3. userlist 用户数据表,存储用户的联系方式。 hash表
4. eventlist 预约表。 这个存储每个预约的信息。 hash表

除了你用Redis之外,你还可以用mysql。这个Mysql只是用来存储最终的结果,就是当整个预约流程成功跑完获得ticket之后,将状态,预约信息,用户信息存在数据库中,便于后台的检索。

PHP Routing Part 2:Router

上次讲完Request之后,一直在研究Router(路由器)部分的东东。看过PHP、Python等其他框架内实现的路由器。

路由一共分为两个功能:
1、标准路由器, 这里会把链接上的uri根据配置文件分析后,得到Controller和Action。
2、跳转路由和代理路由, 也就是redirect和proxy。同样和1一样,需要写入uri的配置规则,只不过是会根据配置跳转链接,并且赋予301或者302状态码。

在PHP5.4开始的版本,基本上都是使用PHP-FPM模式,当用户访问一个页面,webserver(比如Nginx),会将请求传给php,此时PHP会进行初始化,这时候会将一些变量常量写入到了$_SERVER中。就如上面所说,每次请求都是一个完整的链接,我们分割为3个部分:基本地址,路径,请求参数。

  1. 基本地址: 也就是你的基础路径,可以是一个域名也可以是一个带有path的地址链接。这些将会忽略不计。
  2. 路径: 这里的路径就是你所需的有效路径,比如 /users/show, /video/categories, /news/content/2014-03-02/1313.html
  3. 请求参数: 这里包含了_COOKIE,_GET,_POST以及php://input

一般情况下,我们需要的是路径这一部分,一般可以通过REQUEST_URI获得。这些都是在Request类中的,还没开始进入的Router类中哦,接下来,是路径解析。首先来说说一说标准路由,首先你要对你的期望制定一些路由规则。在路由器中以:开头的,紧跟着的就是变量名称,例如 /:controller/:action 。那么所有在:controller位置的变量都会当作controller名来处理。一般来说我们链接有以下几种需求:
1. 根目录形式 / 需要定义controller和action,指定首页所显示的内容
2. /:controller/:action/* 最常见的链接模型
3. /api/:contoller/:action 链接是有一个prefix的。
4. /news/content/:date/:id.html 一种伪静态的链接。 像这种:date :id是一种自定参数,我们可以在路由配置后面加上 date 的匹配规则, [0-9]{4}-[0-9]{2}-[0-9]{2}。

上面4个例子都是我们常看到的,这些配置有了,还不能正常使用。我们需要通过将这些配置转换成程序可读的正则表达式。首先我们将:controller分割出来,controller将会用于匹配后的key名。这时候正则表达式需要利用标签格式(?exp),这个模式在匹配成功后,返回的结果中,将会以你定的name来做为key,就不需要通过数据索引来一一访问。在匹配成功之后,有了controller和action,这时候Dispatcher就会正式调用。

至此,路由解析已经完成,所有的处理已经全部交给了Controller。Controller处理完结果之后,将会输出给Response,显示给用户的面前。