安静
PHP技术博客

140822 PHPCMS v9 超级安全防范教程[转载]

admin阅读(503)

一、目录权限设置很重要:可以有效防范黑客上传木马文件.
如果通过 chmod 644 * -R 的话,php文件就没有权限访问了。
如果通过chmod 755 * -R 的话,php文件的权限就高了。

所以就需要分开设置目录权限和文件权限:

linux 服务器权限:经常要用到的命令:

find /path -type f -exec chmod 644 {} \; //设置文件权限为644
find /path -type d -exec chmod 755 {} \; //设置目录权限为755

设置完成后,再通过命令:chown root:root * -R 将目录和文件的所有者改为root。

这样就更加安全了。

FTP用户,确定使用的是linux主机。windows需要登录到服务器中设置。
进入到phpcms 安装根目录,选取所有文件:
设置数字值为:755,同时选定:选择递归处理子目录,只应用到目录
同样再选择所有文件,数字值为:644,选择递归处理子目录,只应用到文件
如果设置错了,重新再设置就可以了。

二、Linux find命令 查找可疑的木马文件
查找:30天内被修改的文件
find ./ -mtime -30 -type f -exec ls -l {} \;
找到目录下所有的txt文件
find ./ -name “*.txt” -print
找到目录下所有的txt文件并删除
find ./ -name “*.txt” -exec rm -rf {} \;
找到目录下所有的php文件 并且在30天之类被修改的文件
find ./ -name “*.php” -mtime -30 -typef -exec ls -l {} \;
找到目录下所有的php文件,同时,满足 30天以内,1天之前的
find ./ -name “*.php” -mtime -30 -mtime +1 -type f -execls -l {} \;

三、通过apache配置限定:
1、apache 下 禁止目录执行php
通过目录下面放置 .htaccess文件来限制权限。
该方法会将php文件当做附件并下载。同时,可以通过浏览器访问到文件。
php_flag engine off

使用场景:在下面目录放置
\uploadfile\
\statics\
\html\
\phpsso_server\uploadfile\
\phpsso_server\statics\

2、禁止通过浏览器访问所有文件

通过目录下面放置 .htaccess文件来限制权限。
RewriteEngine on
RewriteRule ^(.*) /index.html

使用场景:
\caches\
\phpsso_server\caches\

3、禁止php跨目录浏览权限配置:

虚拟主机配置样例:
<VirtualHost *:80>
ServerAdmin root@phpip.com
DocumentRoot /data/wwwroot/www
ServerName www.phpip.com
<Directory /data/wwwroot/www>
Options FollowSymLinks
AllowOverride Options FileInfo
Order allow,deny
Allow from all
php_admin_value open_basedir /data/wwwroot/www/:/var/tmp/
DirectoryIndex index.htm index.html index.php
</Directory>
ErrorLog “| /usr/sbin/rotatelogs /data/logs/%m_%d_www.phpip.com-error_log 86400 480”
CustomLog “| /usr/sbin/rotatelogs /data/logs/%m_%d_www.phpip.com-access_log 86400 480” common
</VirtualHost>

4、按天存放apache日志:

参考上面配置文件:
ErrorLog “| /usr/sbin/rotatelogs /data/logs/%m_%d_www.phpip.com-error_log 86400 480”
CustomLog “| /usr/sbin/rotatelogs /data/logs/%m_%d_www.phpip.com-access_log 86400 480” common

140822 phpredis中文手册[转载]

admin阅读(546)

redis中文手册:http://readthedocs.org/docs/redis/en/latest/ 

本文是参考《redis中文手册》,将示例代码用php来实现,注意php-redis与redis_cli的区别(主要是返回值类型和参数用法)。

目录(使用CTRL+F快速查找命令):

Key String Hash List Set
  • 键(Key)
    • DEL
    • KEYS
    • RANDOMKEY
    • TTL
    • EXISTS
    • MOVE
    • RENAME
    • RENAMENX
    • TYPE
    • EXPIRE
    • EXPIREAT
    • OBJECT
    • PERSIST
    • SORT
  • 字符串(String)
    • SET
    • SETNX
    • SETEX
    • SETRANGE
    • MSET
    • MSETNX
    • APPEND
    • GET
    • MGET
    • GETRANGE
    • GETSET
    • STRLEN
    • INCR
    • INCRBY
    • DECR
    • DECRBY
    • SETBIT
    • GETBIT
  • 哈希表(Hash)
    • HSET
    • HSETNX
    • HMSET
    • HGET
    • HMGET
    • HGETALL
    • HDEL
    • HLEN
    • HEXISTS
    • HINCRBY
    • HKEYS
    • HVALS
  • 表(List)
    • LPUSH
    • LPUSHX
    • RPUSH
    • RPUSHX
    • LPOP
    • RPOP
    • BLPOP
    • BRPOP
    • LLEN
    • LRANGE
    • LREM
    • LSET
    • LTRIM
    • LINDEX
    • LINSERT
    • RPOPLPUSH
    • BRPOPLPUSH
  • 集合(Set)
    • SADD
    • SREM
    • SMEMBERS
    • SISMEMBER
    • SCARD
    • SMOVE
    • SPOP
    • SRANDMEMBER
    • SINTER
    • SINTERSTORE
    • SUNION
    • SUNIONSTORE
    • SDIFF
    • SDIFFSTORE
Sorted Set Pub/Sub Transaction Connection Server
  • 有序集(Sorted Set)
    • ZADD
    • ZREM
    • ZCARD
    • ZCOUNT
    • ZSCORE
    • ZINCRBY
    • ZRANGE
    • ZREVRANGE
    • ZRANGEBYSCORE
    • ZREVRANGEBYSCORE
    • ZRANK
    • ZREVRANK
    • ZREMRANGEBYRANK
    • ZREMRANGEBYSCORE
    • ZINTERSTORE
    • ZUNIONSTORE
  • 发布/订阅(Pub/Sub)
    • PUBLISH
    • SUBSCRIBE
    • PSUBSCRIBE
    • UNSUBSCRIBE
    • PUNSUBSCRIBE
  • 事务(Transaction)
    • WATCH
    • UNWATCH
    • MULTI
    • EXEC
    • DISCARD
  • 连接(Connection)
    • AUTH
    • PING
    • SELECT
    • ECHO
    • QUIT
  • 服务器(Server)
    • BGREWRITEAOF
    • BGSAVE
    • SAVE
    • LASTSAVE
    • DBSIZE
    • SLAVEOF
    • FLUSHALL
    • FLUSHDB
    • SHUTDOWN
    • SLOWLOG
    • INFO
    • CONFIG GET
    • CONFIG SET
    • CONFIG RESETSTAT
    • DEBUG OBJECT
    • DEBUG SEGFAULT
    • MONITOR
    • SYNC

phpredis是redis的php的一个扩展,效率是相当高有链表排序功能,对创建内存级的模块业务关系

很有用;以下是redis官方提供的命令使用技巧:

下载地址如下:

https://github.com/owlient/phpredis(支持redis 2.0.4)

Redis::__construct构造函数
$redis = new Redis();

connect, open 链接redis服务
参数
host: string,服务地址
port: int,端口号
timeout: float,链接时长 (可选, 默认为 0 ,不限链接时间)
注: 在redis.conf中也有时间,默认为300

pconnect, popen 不会主动关闭的链接
参考上面

setOption 设置redis模式

getOption 查看redis设置的模式

ping 查看连接状态

KEY相关操作

DEL

移除给定的一个或多个key

如果key不存在,则忽略该命令。

时间复杂度:
O(N),N为要移除的key的数量。
移除单个字符串类型的key,时间复杂度为O(1)。
移除单个列表、集合、有序集合或哈希表类型的key,时间复杂度为O(M),M为以上数据结构内的元素数量。
返回值:
被移除key的数量。

//DEL
# 情况1: 删除单个key
$redis->set('myname','ikodota');
echo $redis->get('myname').'<br>'; # 返回:ikodota

$redis->del('myname');# 返回 TRUE(1)
var_dump($redis->get('myname')); # 返回 bool(false)

# 情况2: 删除一个不存在的key
if(!$redis->exists('fake_key')) # 不存在
var_dump($redis->del('fake_key')); # 返回 int(0)

# 情况3: 同时删除多个key
$array_mset=array('first_key'=>'first_val',
          'second_key'=>'second_val',
          'third_key'=>'third_val');
$redis->mset($array_mset); #用MSET一次储存多个值
$array_mget=array('first_key','second_key','third_key');
var_dump($redis->mget($array_mget)); #一次返回多个值 //array(3) { [0]=> string(9) "first_val" [1]=> string(10) "second_val" [2]=> string(9) "third_val" }

$redis->del($array_mget); #同时删除多个key
var_dump($redis->mget($array_mget)); #返回 array(3) { [0]=> bool(false) [1]=> bool(false) [2]=> bool(false) }

 

KEYS
KEYS pattern 
查找符合给定模式的key
KEYS *命中数据库中所有key
KEYS h?llo命中hello, hallo and hxllo等。
KEYS h*llo命中hlloheeeeello等。
KEYS h[ae]llo命中hellohallo,但不命中hillo

特殊符号用"\"隔开

时间复杂度:
O(N),N为数据库中key的数量。
返回值:
符合给定模式的key列表。

警告 :KEYS的速度非常快,但在一个大的数据库中使用它仍然可能造成性能问题,如果你需要从一个数据集中查找特定的key,你最好还是用集合(Set)

//KEYS
#$redis->FLUSHALL();
$array_mset_keys=array('one'=>'1',
          'two'=>'2',
          'three '=>'3',
          'four'=>'4');
$redis->mset($array_mset_keys); #用MSET一次储存多个值
var_dump($redis->keys('*o*')); //array(3) { [0]=> string(4) "four" [1]=> string(3) "two" [2]=> string(3) "one" }
var_dump($redis->keys('t??')); //array(1) { [0]=> string(3) "two" }
var_dump($redis->keys('t[w]*')); //array(1) { [0]=> string(3) "two" }
print_r($redis->keys('*')); //Array ( [0] => four [1] => three [2] => two [3] => one )

 

RANDOMKEY

从当前数据库中随机返回(不删除)一个key

时间复杂度:
O(1)
返回值:
当数据库不为空时,返回一个key
当数据库为空时,返回nil。

//RANDOMKEY
$redis->FLUSHALL();
# 情况1:数据库不为空
$array_mset_randomkey=array('fruit'=>'apple',
                'drink'=>'beer',
                'food'=>'cookis');
$redis->mset($array_mset_randomkey);
echo $redis->randomkey(); 
print_r($redis->keys('*')); # 查看数据库内所有key,证明RANDOMKEY并不删除key//Array ( [0] => food [1] => drink [2] => fruit )

# 情况2:数据库为空
$redis->flushdb();  # 删除当前数据库所有key
var_dump($redis-> randomkey()); //bool(false)

TTL
TTL key

返回给定key的剩余生存时间(time to live)(以秒为单位)。

时间复杂度:
O(1)
返回值:
key的剩余生存时间(以秒为单位)。
key不存在或没有设置生存时间时,返回-1 。

//TTL
# 情况1:带TTL的key
$redis->flushdb();
//$redis->set('name','ikodota'); # 设置一个key
$redis->expire('name',30);  # 设置生存时间为30秒 //return (integer) 1
echo $redis->get('name'); //return ikodota
echo $redis->ttl('name'); //(integer) 25

//echo $redis->ttl('name');  # 30秒过去,name过期 //(integer) -1
var_dump($redis->get('name')); # 过期的key将被删除 //return bool(false);

# 情况2:不带TTL的key
$redis->set('site','wikipedia.org');//OK
var_dump($redis->ttl('site'));//int(-1)

# 情况3:不存在的key
$redis->EXISTS('not_exists_key');//int(0)
var_dump($redis->TTL('not_exists_key'));//int(-1)

 

EXISTS
EXISTS key

检查给定key是否存在。

时间复杂度:
O(1)
返回值:
key存在,返回1,否则返回0
//EXISTS
echo '<br>EXISTS<br>';
$redis->set('db',"redis"); //bool(true) 
var_dump($redis->exists('db'));  # key存在 //bool(true) 
$redis->del('db');   # 删除key //int(1)
var_dump($redis->exists('db'))  # key不存在 //bool(false)

 

MOVE
MOVE key db

将当前数据库(默认为0)的key移动到给定的数据库db当中。

如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定key,或者key不存在于当前数据库,那么MOVE没有任何效果。

因此,也可以利用这一特性,将MOVE当作锁(locking)原语。

时间复杂度:
O(1)
返回值:
移动成功返回1,失败则返回0
//MOVE
echo '<br><br>MOVE<br>';
# 情况1: key存在于当前数据库
$redis->SELECT(0);  # redis默认使用数据库0,为了清晰起见,这里再显式指定一次。//OK
$redis->SET('song',"secret base - Zone"); //OK
var_dump ($redis->MOVE('song',1));  # 将song移动到数据库1 //bool(true)

# 情况2:当key不存在的时候
$redis->SELECT(1);
var_dump ($redis->EXISTS('fake_key'));//bool(false);
var_dump($redis->MOVE('fake_key', 0));  # 试图从数据库1移动一个不存在的key到数据库0,失败) //bool(false)

$redis->SELECT(0); # 使用数据库0
var_dump($redis->EXISTS('fake_key'));  # 证实fake_key不存在 //bool(false)

# 情况3:当源数据库和目标数据库有相同的key时

$redis->SELECT(0);  # 使用数据库0
$redis->SET('favorite_fruit',"banana");

$redis->SELECT(1);  # 使用数据库1
$redis->SET('favorite_fruit',"apple");

$redis->SELECT(0);  # 使用数据库0,并试图将favorite_fruit移动到数据库1
var_dump($redis->MOVE('favorite_fruit',1));  # 因为两个数据库有相同的key,MOVE失败 //return bool(false)
echo $redis->GET('favorite_fruit');  # 数据库0的favorite_fruit没变 //return banana

$redis->SELECT(1);
echo $redis->GET('favorite_fruit');   # 数据库1的favorite_fruit也是 //return apple

 

RENAME

RENAME key newkey

key改名为newkey

keynewkey相同或者key不存在时,返回一个错误。

newkey已经存在时,RENAME命令将覆盖旧值。

时间复杂度:
O(1)
返回值:
改名成功时提示OK,失败时候返回一个错误。
//RENAME
echo '<br><br>RENAME<br>';
# 情况1:key存在且newkey不存在
$redis->SET('message',"hello world");
var_dump($redis->RENAME('message','greeting'));  //bool(true)
var_dump($redis->EXISTS('message'));  # message不复存在 //bool(false)
var_dump($redis->EXISTS('greeting'));   # greeting取而代之 //bool(true)

# 情况2:当key不存在时,返回错误 ,php返回false;
var_dump($redis->RENAME('fake_key','never_exists'));  //bool(false)

# 情况3:newkey已存在时,RENAME会覆盖旧newkey
$redis->SET('pc',"lenovo");
$redis->SET('personal_computer',"dell"); 
var_dump($redis->RENAME('pc','personal_computer')); //bool(true)
var_dump($redis->GET('pc')); //(nil)   bool(false)
var_dump($redis->GET('personal_computer'));  # dell“没有”了 //string(6) "lenovo"

RENAMENX 
RENAMENX key newkey

当且仅当newkey不存在时,将key改为newkey

出错的情况和RENAME一样(key不存在时报错)。

时间复杂度:
O(1)
返回值:
修改成功时,返回1
如果newkey已经存在,返回0
//RENAMENX
echo '<br><br>RENAMENX<br>';

# 情况1:newkey不存在,成功
$redis->SET('player',"MPlyaer");
$redis->EXISTS('best_player'); //int(0)
var_dump($redis->RENAMENX('player','best_player')); // bool(true) 

# 情况2:newkey存在时,失败
$redis->SET('animal',"bear");
$redis->SET('favorite_animal', "butterfly");

var_dump($redis->RENAMENX('animal', 'favorite_animal'));// bool(false)

var_dump($redis->get('animal')); //string(4) "bear"
var_dump($redis->get('favorite_animal')); //string(9) "butterfly"
TYPE
TYPE key

返回key所储存的值的类型。

时间复杂度:
O(1)
返回值:
none(key不存在) int(0)
string(字符串) int(1)
list(列表) int(3)
set(集合) int(2)
zset(有序集) int(4)
hash(哈希表) int(5)

//TYPE
$redis->flushALL();
echo '<br><br>TYPE<br>';

var_dump($redis->TYPE('fake_key')); //none /int(0)

$redis->SET('weather',"sunny");  # 构建一个字符串
var_dump($redis->TYPE('weather'));//string / int(1)

$redis->SADD('pat',"dog");  # 构建一个集合
var_dump($redis->TYPE('pat')); //set /int(2)

$redis->LPUSH('book_list',"programming in scala");  # 构建一个列表
var_dump($redis->TYPE('book_list'));//list / int(3) 

$redis->ZADD('pats',1,'cat');  # 构建一个zset (sorted set) // int(1)
$redis->ZADD('pats',2,'dog');
$redis->ZADD('pats',3,'pig');
var_dump($redis->zRange('pats',0,-1)); // array(3) { [0]=> string(3) "cat" [1]=> string(3) "dog" [2]=> string(3) "pig" }
var_dump($redis->TYPE('pats')); //zset / int(4)

$redis->HSET('website','google','www.g.cn');   # 一个新域
var_dump($redis->HGET('website','google')); //string(8) "www.g.cn"
var_dump($redis->TYPE('website')); //hash /int(5)

EXPIRE

EXPIRE key seconds

为给定key设置生存时间。

key过期时,它会被自动删除。

在Redis中,带有生存时间的key被称作“易失的”(volatile)。

在低于2.1.3版本的Redis中,已存在的生存时间不可覆盖。
从2.1.3版本开始,key的生存时间可以被更新,也可以被PERSIST命令移除。(详情参见 http://redis.io/topics/expire)。

时间复杂度:
O(1)
返回值:
设置成功返回1
key不存在或者不能为key设置生存时间时(比如在低于2.1.3中你尝试更新key的生存时间),返回0
//EXPIRE
$redis->select(7);
//$redis->flushdb();

echo '<br><br>EXPIRE<br>';
$redis->SET('cache_page',"www.cnblogs.com/ikodota");
$redis->EXPIRE('cache_page', 30);  # 设置30秒后过期
sleep(6);
echo $redis->TTL('cache_page').'<br>';   # 查看给定key的剩余生存时间 //(integer) 24

$redis->EXPIRE('cache_page', 3000);  # 更新生存时间,3000秒
sleep(4);
echo $redis->TTL('cache_page').'<br>';   //(integer) 2996

 

EXPIREAT 
EXPIREAT key timestamp

EXPIREAT的作用和EXPIRE一样,都用于为key设置生存时间。

不同在于EXPIREAT命令接受的时间参数是UNIX时间戳(unix timestamp)。

时间复杂度:
O(1)
返回值:
如果生存时间设置成功,返回1
key不存在或没办法设置生存时间,返回0

//EXPIREAT
echo '<br><br>EXPIREAT<br>';
$redis->SET('cache','www.google.com');
echo $redis->EXPIREAT('cache','1355292000'); # 这个key将在2012.12.12过期

echo ($redis->TTL('cache')); //return 124345085

 

OBJECT 
OBJECT subcommand [arguments [arguments]]

OBJECT命令允许从内部察看给定key的Redis对象。

它通常用在除错(debugging)或者了解为了节省空间而对key使用特殊编码的情况。
当将Redis用作缓存程序时,你也可以通过OBJECT命令中的信息,决定key的驱逐策略(eviction policies)。

OBJECT命令有多个子命令:

  • OBJECT REFCOUNT <key>返回给定key引用所储存的值的次数。此命令主要用于除错。
  • OBJECT ENCODING <key>返回给定key锁储存的值所使用的内部表示(representation)。
  • OBJECT IDLETIME <key>返回给定key自储存以来的空转时间(idle, 没有被读取也没有被写入),以秒为单位。
对象可以以多种方式编码:
  • 字符串可以被编码为raw(一般字符串)或int(用字符串表示64位数字是为了节约空间)。
  • 列表可以被编码为ziplistlinkedlistziplist是为节约大小较小的列表空间而作的特殊表示。
  • 集合可以被编码为intset或者hashtableintset是只储存数字的小集合的特殊表示。
  • 哈希表可以编码为zipmap或者hashtablezipmap是小哈希表的特殊表示。
  • 有序集合可以被编码为ziplist或者skiplist格式。ziplist用于表示小的有序集合,而skiplist则用于表示任何大小的有序集合。
假如你做了什么让Redis没办法再使用节省空间的编码时(比如将一个只有1个元素的集合扩展为一个有100万个元素的集合),特殊编码类型(specially encoded types)会自动转换成通用类型(general type)。
时间复杂度:
O(1)
返回值:
REFCOUNTIDLETIME返回数字。
ENCODING返回相应的编码类型。
//OBJECT
$redis->select(8);
echo '<br><br>OBJECT<br>';
$redis->SET('game',"WOW");  # 设置一个字符串
$redis->OBJECT('REFCOUNT','game');  # 只有一个引用

//sleep(5);
echo $redis->OBJECT('IDLETIME','game');  # 等待一阵。。。然后查看空转时间 //(integer) 10
//echo $redis->GET('game');  # 提取game, 让它处于活跃(active)状态  //return WOW
//echo $redis->OBJECT('IDLETIME','game');  # 不再处于空转 //(integer) 0
var_dump($redis->OBJECT('ENCODING','game'));  # 字符串的编码方式 //string(3) "raw"
$redis->SET('phone',15820123123);  # 大的数字也被编码为字符串
var_dump($redis->OBJECT('ENCODING','phone')); //string(3) "raw"
$redis->SET('age',20);  # 短数字被编码为int
var_dump($redis->OBJECT('ENCODING','age')); //string(3) "int"

 

PERSIST 
PERSIST key

移除给定key的生存时间。

时间复杂度:
O(1)
返回值:
当生存时间移除成功时,返回1.
如果key不存在或key没有设置生存时间,返回0

//PERSIST
echo '<br><br>PERSIST<br>';
$redis->SET('time_to_say_goodbye',"886...");
$redis->EXPIRE('time_to_say_goodbye', 300);
sleep(3);
echo $redis->TTL('time_to_say_goodbye'); # (int) 297
echo '<br>';

$redis->PERSIST('time_to_say_goodbye');  # 移除生存时间
echo $redis->TTL('time_to_say_goodbye');  # 移除成功  //int(-1)


SORT

SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC | DESC] [ALPHA] [STORE destination]

排序,分页等
参数

array(
‘by’ => ‘some_pattern_*’,
‘limit’ => array(0, 1),
‘get’ => ‘some_other_pattern_*’ or an array of patterns,
‘sort’ => ‘asc’ or ‘desc’,
‘alpha’ => TRUE,
‘store’ => ‘external-key’
)

返回或保存给定列表、集合、有序集合key中经过排序的元素。

排序默认以数字作为对象,值被解释为双精度浮点数,然后进行比较。

一般SORT用法

最简单的SORT使用方法是SORT key

假设today_cost是一个保存数字的列表,SORT命令默认会返回该列表值的递增(从小到大)排序结果。

# 将数据一一加入到列表中
$redis->LPUSH('today_cost', 30);
$redis->LPUSH('today_cost', 1.5);
$redis->LPUSH('today_cost', 10);
$redis->LPUSH('today_cost', 8);
# 排序
var_dump($redis->SORT('today_cost')); //array(4) { [0]=> string(3) "1.5" [1]=> string(1) "8" [2]=> string(2) "10" [3]=> string(2) "30" }

当数据集中保存的是字符串值时,你可以用ALPHA修饰符(modifier)进行排序。

# 将数据一一加入到列表中
$redis->LPUSH('website', "www.reddit.com");
$redis->LPUSH('website', "www.slashdot.com");
$redis->LPUSH('website', "www.infoq.com");
# 默认排序
var_dump($redis->SORT('website'));//array(3) { [0]=> string(13) "www.infoq.com" [1]=> string(16) "www.slashdot.com" [2]=> string(14) "www.reddit.com" }

# 按字符排序 ALPHA=true
var_dump($redis->SORT('website', array('ALPHA'=>TRUE))); //array(3) { [0]=> string(13) "www.infoq.com" [1]=> string(14) "www.reddit.com" [2]=> string(16) "www.slashdot.com" }

如果你正确设置了!LC_COLLATE环境变量的话,Redis能识别UTF-8编码。

排序之后返回的元素数量可以通过LIMIT修饰符进行限制。
LIMIT修饰符接受两个参数:offsetcount
offset指定要跳过的元素数量,count指定跳过offset个指定的元素之后,要返回多少个对象。

以下例子返回排序结果的前5个对象(offset0表示没有元素被跳过)。

# 将数据一一加入到列表中
$redis->LPUSH('rank', 30); //(integer) 1
$redis->LPUSH('rank', 56); //(integer) 2
$redis->LPUSH('rank', 42); //(integer) 3
$redis->LPUSH('rank', 22); //(integer) 4
$redis->LPUSH('rank', 0);  //(integer) 5
$redis->LPUSH('rank', 11); //(integer) 6
$redis->LPUSH('rank', 32); //(integer) 7
$redis->LPUSH('rank', 67); //(integer) 8
$redis->LPUSH('rank', 50); //(integer) 9
$redis->LPUSH('rank', 44); //(integer) 10
$redis->LPUSH('rank', 55); //(integer) 11

# 排序
$redis_sort_option=array('LIMIT'=>array(0,5));
var_dump($redis->SORT('rank',$redis_sort_option));   # 返回排名前五的元素 // array(5) { [0]=> string(1) "0" [1]=> string(2) "11" [2]=> string(2) "22" [3]=> string(2) "30" [4]=> string(2) "32" }

修饰符可以组合使用。以下例子返回降序(从大到小)的前5个对象。

$redis_sort_option=array(
            'LIMIT'=>array(0,5),
            'SORT'=>'DESC'
            );
var_dump($redis->SORT('rank',$redis_sort_option)); //array(5) { [0]=> string(2) "67" [1]=> string(2) "56" [2]=> string(2) "55" [3]=> string(2) "50" [4]=> string(2) "44" }

使用外部key进行排序

有时候你会希望使用外部的key作为权重来比较元素,代替默认的对比方法。

假设现在有用户(user)数据如下:

id     name    level
——————————-
1    admin     9999
2    huangz   10
59230  jack        3
222    hacker      9999

id数据保存在key名为user_id的列表中。
name数据保存在key名为user_name_{id}的列表中
level数据保存在user_level_{id}的key中。

# 先将要使用的数据加入到数据库中

# admin
$redis->LPUSH('user_id', 1);//(integer) 1
$redis->SET('user_name_1', 'admin');
$redis->SET('user_level_1',9999);

# huangz
$redis->LPUSH('user_id', 2);//(integer) 2
$redis->SET('user_name_2', 'huangz');
$redis->SET('user_level_2', 10);

# jack
$redis->LPUSH('user_id', 59230);//(integer) 3
$redis->SET('user_name_59230','jack');
$redis->SET('user_level_59230', 3);

# hacker
$redis->LPUSH('user_id', 222);  //(integer) 4
$redis->SET('user_name_222', 'hacker');
$redis->SET('user_level_222', 9999);

如果希望按level从大到小排序user_id,可以使用以下命令:

$redis_sort_option=array('BY'=>'user_level_*',
            'SORT'=>'DESC'
            );
var_dump($redis->SORT('user_id',$redis_sort_option)); //array(4) { [0]=> string(3) "222" [1]=> string(1) "1" [2]=> string(1) "2" [3]=> string(5) "59230" }

#---------------------------
#1) "222"    # hacker
#2) "1"      # admin
#3) "2"      # huangz
#4) "59230"  # jack

但是有时候只是返回相应的id没有什么用,你可能更希望排序后返回id对应的用户名,这样更友好一点,使用GET选项可以做到这一点:

$redis_sort_option=array('BY'=>'user_level_*',
            'SORT'=>'DESC',
            'GET'=>'user_name_*'
            );
var_dump($redis->SORT('user_id', $redis_sort_option)); //array(4) { [0]=> string(6) "hacker" [1]=> string(5) "admin" [2]=> string(6) "huangz" [3]=> string(4) "jack" }
#1) "hacker"
#2) "admin"
#3) "huangz"
#4) "jack"

可以多次地、有序地使用GET操作来获取更多外部key

比如你不但希望获取用户名,还希望连用户的密码也一并列出,可以使用以下命令:

# 先添加一些测试数据
$redis->SET('user_password_222', "hey,im in");
$redis->SET('user_password_1', "a_long_long_password");
$redis->SET('user_password_2', "nobodyknows");
$redis->SET('user_password_59230', "jack201022");

# 获取name和password
$redis_sort_option=array('BY'=>'user_level_*',
            'SORT'=>'DESC',
            'GET'=>array('user_name_*','user_password_*')
            );
var_dump($redis->SORT('user_id',$redis_sort_option));//array(8) { [0]=> string(6) "hacker" [1]=> string(9) "hey,im in" [2]=> string(5) "admin" [3]=> string(20) "a_long_long_password" [4]=> string(6) "huangz" [5]=> string(11) "nobodyknows" [6]=> string(4) "jack" [7]=> string(10) "jack201022" }

#------------------------------------
#1) "hacker"       # 用户名
#2) "hey,im in"    # 密码
#3) "jack"
#4) "jack201022"
#5) "huangz"
#6) "nobodyknows"
#7) "admin"
#8) "a_long_long_password"
# 注意GET操作是有序的,GET user_name_* GET user_password_* 和 GET user_password_* GET user_name_*返回的结果位置不同

# 获取name和password 注意GET操作是有序的
$redis_sort_option=array('BY'=>'user_level_*',
            'SORT'=>'DESC',
            'GET'=>array('user_password_*','user_name_*')
            );
var_dump($redis->SORT('user_id',$redis_sort_option));// array(8) { [0]=> string(9) "hey,im in" [1]=> string(6) "hacker" [2]=> string(20) "a_long_long_password" [3]=> string(5) "admin" [4]=> string(11) "nobodyknows" [5]=> string(6) "huangz" [6]=> string(10) "jack201022" [7]=> string(4) "jack" }

GET还有一个特殊的规则——"GET #",用于获取被排序对象(我们这里的例子是user_id)的当前元素。

比如你希望user_idlevel排序,还要列出idnamepassword,可以使用以下命令:

$redis_sort_option=array('BY'=>'user_level_*',
            'SORT'=>'DESC',
            'GET'=>array('#','user_password_*','user_name_*')
            );
var_dump($redis->SORT('user_id',$redis_sort_option));//array(12) { [0]=> string(3) "222" [1]=> string(9) "hey,im in" [2]=> string(6) "hacker" [3]=> string(1) "1" [4]=> string(20) "a_long_long_password" [5]=> string(5) "admin" [6]=> string(1) "2" [7]=> string(11) "nobodyknows" [8]=> string(6) "huangz" [9]=> string(5) "59230" [10]=> string(10) "jack201022" [11]=> string(4) "jack" }

#--------------------------------------------------------------
#1) "222"          # id
#2) "hacker"       # name
#3) "hey,im in"    # password
#4) "1"
#5) "admin"
#6) "a_long_long_password"
#7) "2"
#8) "huangz"
#9) "nobodyknows"
#10) "59230"
#11) "jack"
#12) "jack201022"

只获取对象而不排序

BY修饰符可以将一个不存在的key当作权重,让SORT跳过排序操作。

该方法用于你希望获取外部对象而又不希望引起排序开销时使用。

# 确保fake_key不存在
$redis->EXISTS('fake_key');//(integer) 0

# 以fake_key作BY参数,不排序,只GET name 和 GET password
$redis_sort_option=array('BY'=>'fake_key',
            'SORT'=>'DESC',
            'GET'=>array('#','user_name_*','user_password_*')
            );
var_dump($redis->SORT('user_id',$redis_sort_option));//array(12) { [0]=> string(3) "222" [1]=> string(6) "hacker" [2]=> string(9) "hey,im in" [3]=> string(5) "59230" [4]=> string(4) "jack" [5]=> string(10) "jack201022" [6]=> string(1) "2" [7]=> string(6) "huangz" [8]=> string(11) "nobodyknows" [9]=> string(1) "1" [10]=> string(5) "admin" [11]=> string(20) "a_long_long_password" }

#----------------------------------------------
#1) "222"        # id
#2) "hacker"     # user_name
#3) "hey,im in"  # password
#4) "59230"
#5) "jack"
#6) "jack201022"
#7) "2"
#8) "huangz"
#9) "nobodyknows"
#10) "1"
#11) "admin"
#12) "a_long_long_password"

保存排序结果

默认情况下,SORT操作只是简单地返回排序结果,如果你希望保存排序结果,可以给STORE选项指定一个key作为参数,排序结果将以列表的形式被保存到这个key上。(若指定key已存在,则覆盖。)

$redis->EXISTS('user_info_sorted_by_level');  # 确保指定key不存在   //(integer) 0
$redis_sort_option=array('BY'=>'user_level_*',
            'GET'=>array('#','user_name_*','user_password_*'),
            'STORE'=>'user_info_sorted_by_level'
            );

var_dump($redis->SORT('user_id',$redis_sort_option)); //int(12)
var_dump($redis->LRANGE('user_info_sorted_by_level', 0 ,11));  # 查看排序结果  //array(12) { [0]=> string(5) "59230" [1]=> string(4) "jack" [2]=> string(10) "jack201022" [3]=> string(1) "2" [4]=> string(6) "huangz" [5]=> string(11) "nobodyknows" [6]=> string(3) "222" [7]=> string(6) "hacker" [8]=> string(9) "hey,im in" [9]=> string(1) "1" [10]=> string(5) "admin" [11]=> string(20) "a_long_long_password" }

#-----------------------------------------------------------------
#1) "59230"
#2) "jack"
#3) "jack201022"
#4) "2"
#5) "huangz"
#6) "nobodyknows"
#7) "222"
#8) "hacker"
#9) "hey,im in"
#10) "1"
#11) "admin"
#12) "a_long_long_password"

一个有趣的用法是将SORT结果保存,用EXPIRE为结果集设置生存时间,这样结果集就成了SORT操作的一个缓存。

这样就不必频繁地调用SORT操作了,只有当结果集过期时,才需要再调用一次SORT操作。

有时候为了正确实现这一用法,你可能需要加锁以避免多个客户端同时进行缓存重建(也就是多个客户端,同一时间进行SORT操作,并保存为结果集),具体参见SETNX命令。

在GET和BY中使用哈希表

可以使用哈希表特有的语法,在SORT命令中进行GET和BY操作。

# 假设现在我们的用户表新增了一个serial项来为作为每个用户的序列号
# 序列号以哈希表的形式保存在serial哈希域内。

$redis_hash_testdata_array=array(1=>'23131283',
                2=>'23810573',
                222=>'502342349',
                59230=>'2435829758'
                );

$redis->HMSET('serial',$redis_hash_testdata_array);

# 我们希望以比较serial中的大小来作为排序user_id的方式
$redis_sort_option=array('BY'=>'*->serial');
var_dump($redis->SORT('user_id', $redis_sort_option)); //array(4) { [0]=> string(3) "222" [1]=> string(5) "59230" [2]=> string(1) "2" [3]=> string(1) "1" }

#----------------------------------------
#1) "222"
#2) "59230"
#3) "2"
#4) "1"

符号"->"用于分割哈希表的关键字(key name)和索引域(hash field),格式为"key->field"

除此之外,哈希表的BYGET操作和上面介绍的其他数据结构(列表、集合、有序集合)没有什么不同。

时间复杂度:
O(N+M*log(M)),N为要排序的列表或集合内的元素数量,M为要返回的元素数量。
如果只是使用SORT命令的GET选项获取数据而没有进行排序,时间复杂度O(N)。
返回值:
没有使用STORE参数,返回列表形式的排序结果。
使用STORE参数,返回排序结果的元素数量。

字符串(String)

SET

SET key value

将字符串值value关联到key

如果key已经持有其他值,SET就覆写旧值,无视类型。

时间复杂度:O(1)返回值:总是返回OK(TRUE),因为SET不可能失败。

# 情况1:对字符串类型的key进行SET
$redis->SET('apple', 'www.apple.com');#OK  //bool(true)
$redis->GET('apple');//"www.apple.com"

# 情况2:对非字符串类型的key进行SET
$redis->LPUSH('greet_list', "hello");  # 建立一个列表 #(integer) 1 //int(1)
$redis->TYPE('greet_list');#list //int(3)

$redis->SET('greet_list', "yooooooooooooooooo");   # 覆盖列表类型 #OK //bool(true)
$redis->TYPE('greet_list');#string //int(1)

SETNX
SETNX key value

key的值设为value,当且仅当key不存在。

若给定的key已经存在,则SETNX不做任何动作。

SETNX是”SET if Not eXists”(如果不存在,则SET)的简写。

时间复杂度:
O(1)
返回值:
设置成功,返回1
设置失败,返回0
//SETNX
echo '<br><br>SETNX<br>';
$redis->EXISTS('job');  # job不存在 //bool(false);
$redis->SETNX('job', "programmer");  # job设置成功 //bool(true)
$redis->SETNX('job', "code-farmer");  # job设置失败 //bool(false)
echo $redis->GET('job');  # 没有被覆盖 //"programmer"

设计模式(Design pattern): 将SETNX用于加锁(locking)

SETNX可以用作加锁原语(locking primitive)。比如说,要对关键字(key)foo加锁,客户端可以尝试以下方式:

SETNX lock.foo <current Unix time + lock timeout + 1>

如果SETNX返回1,说明客户端已经获得了锁,key设置的unix时间则指定了锁失效的时间。之后客户端可以通过DEL lock.foo来释放锁。

如果SETNX返回0,说明key已经被其他客户端上锁了。如果锁是非阻塞(non blocking lock)的,我们可以选择返回调用,或者进入一个重试循环,直到成功获得锁或重试超时(timeout)。

处理死锁(deadlock)

上面的锁算法有一个问题:如果因为客户端失败、崩溃或其他原因导致没有办法释放锁的话,怎么办?

这种状况可以通过检测发现——因为上锁的key保存的是unix时间戳,假如key值的时间戳小于当前的时间戳,表示锁已经不再有效。

但是,当有多个客户端同时检测一个锁是否过期并尝试释放它的时候,我们不能简单粗暴地删除死锁的key,再用SETNX上锁,因为这时竞争条件(race condition)已经形成了:

  • C1和C2读取lock.foo并检查时间戳,SETNX都返回0,因为它已经被C3锁上了,但C3在上锁之后就崩溃(crashed)了。
  • C1向lock.foo发送DEL命令。
  • C1向lock.foo发送SETNX并成功。
  • C2向lock.foo发送DEL命令。
  • C2向lock.foo发送SETNX并成功。
  • 出错:因为竞争条件的关系,C1和C2两个都获得了锁。

幸好,以下算法可以避免以上问题。来看看我们聪明的C4客户端怎么办:

  • C4向lock.foo发送SETNX命令。
  • 因为崩溃掉的C3还锁着lock.foo,所以Redis向C4返回0
  • C4向lock.foo发送GET命令,查看lock.foo的锁是否过期。如果不,则休眠(sleep)一段时间,并在之后重试。
  • 另一方面,如果lock.foo内的unix时间戳比当前时间戳老,C4执行以下命令:

GETSET lock.foo <current Unix timestamp + lock timeout + 1>

  • 因为GETSET的作用,C4可以检查看GETSET的返回值,确定lock.foo之前储存的旧值仍是那个过期时间戳,如果是的话,那么C4获得锁。
  • 如果其他客户端,比如C5,比C4更快地执行了GETSET操作并获得锁,那么C4的GETSET操作返回的就是一个未过期的时间戳(C5设置的时间戳)。C4只好从第一步开始重试。
注意,即便C4的GETSET操作对key进行了修改,这对未来也没什么影响。
(这里是不是有点问题?C4的确是可以重试,但C5怎么办?它的锁的过期被C4修改了。——译注)

 警告

为了让这个加锁算法更健壮,获得锁的客户端应该常常检查过期时间以免锁因诸如DEL等命令的执行而被意外解开,因为客户端失败的情况非常复杂,不仅仅是崩溃这么简单,还可能是客户端因为某些操作被阻塞了相当长时间,紧接着DEL命令被尝试执行(但这时锁却在另外的客户端手上)。

SETEX
SETEX key seconds value

将值value关联到key,并将key的生存时间设为seconds(以秒为单位)。

如果key 已经存在,SETEX命令将覆写旧值。

这个命令类似于以下两个命令:

$redis->SET('key', 'value');
$redis->EXPIRE('key','seconds');  # 设置生存时间

不同之处是,SETEX是一个原子性(atomic)操作,关联值和设置生存时间两个动作会在同一时间内完成,该命令在Redis用作缓存时,非常实用。

时间复杂度:
O(1)
返回值:
设置成功时返回OK
seconds参数不合法时,返回一个错误。
# 情况1:key不存在
$redis->SETEX('cache_user_id', 60,10086);//bool(true)
echo $redis->GET('cache_user_id');  # 值 //"10086"

sleep(4);
echo $redis->TTL('cache_user_id');  # 剩余生存时间 //int(56)

# 情况2:key已经存在,key被覆写
$redis->SET('cd', "timeless"); //bool(true);
$redis->SETEX('cd', 3000,"goodbye my love"); //bool(true);
echo $redis->GET('cd');//"goodbye my love"

SETRANGE

SETRANGE key offset value

value参数覆写(Overwrite)给定key所储存的字符串值,从偏移量offset开始。

不存在的key当作空白字符串处理。

SETRANGE命令会确保字符串足够长以便将value设置在指定的偏移量上,如果给定key原来储存的字符串长度比偏移量小(比如字符串只有5个字符长,但你设置的offset10),那么原字符和偏移量之间的空白将用零比特(zerobytes,"\x00")来填充。

注意你能使用的最大偏移量是2^29-1(536870911),因为Redis的字符串被限制在512兆(megabytes)内。如果你需要使用比这更大的空间,你得使用多个key

时间复杂度:
对小(small)的字符串,平摊复杂度O(1)。(关于什么字符串是”小”的,请参考APPEND命令)
否则为O(M),M为value参数的长度。
返回值:
被SETRANGE修改之后,字符串的长度。

 警告

 当生成一个很长的字符串时,Redis需要分配内存空间,该操作有时候可能会造成服务器阻塞(block)。在2010年的Macbook Pro上,设置偏移量为536870911(512MB内存分配),耗费约300毫秒, 设置偏移量为134217728(128MB内存分配),耗费约80毫秒,设置偏移量33554432(32MB内存分配),耗费约30毫秒,设置偏移量为8388608(8MB内存分配),耗费约8毫秒。 注意若首次内存分配成功之后,再对同一个key调用SETRANGE操作,无须再重新内存。

模式

因为有了SETRANGE和GETRANGE命令,你可以将Redis字符串用作具有O(1)随机访问时间的线性数组。这在很多真实用例中都是非常快速且高效的储存方式。

# 情况1:对非空字符串进行SETRANGE
$redis->SET('greeting', "hello world");
$redis->SETRANGE('greeting', 6, "Redis"); //int(11)
$redis->GET('greeting');//"hello Redis"

# 情况2:对空字符串/不存在的key进行SETRANGE
$redis->EXISTS('empty_string');//bool(false)
$redis->SETRANGE('empty_string', 5 ,"Redis!");  # 对不存在的key使用SETRANGE //int(11)
var_dump($redis->GET('empty_string'));  # 空白处被"\x00"填充  #"\x00\x00\x00\x00\x00Redis!"   //return string(11) "Redis!"

MSET 
MSET key value [key value …]

同时设置一个或多个key-value对。

当发现同名的key存在时,MSET会用新值覆盖旧值,如果你不希望覆盖同名key,请使用MSETNX命令。

MSET是一个原子性(atomic)操作,所有给定key都在同一时间内被设置,某些给定key被更新而另一些给定key没有改变的情况,不可能发生。

时间复杂度:
O(N),N为要设置的key数量。
返回值:
总是返回OK(因为MSET不可能失败)
#MSET
echo '<br><br>MSET<br>';
$redis->select(0);
$redis->flushdb();
$array_mset=array('date'=>'2012.3.5',
        'time'=>'9.09a.m.',
        'weather'=>'sunny'
        );
$redis->MSET($array_mset); //bool(true)

var_dump($redis->KEYS('*'));   # 确保指定的三个key-value对被插入 //array(3) { [0]=> string(4) "time" [1]=> string(7) "weather" [2]=> string(4) "date" }

# MSET覆盖旧值的例子 但是经过测试覆盖不了
var_dump($redis->SET('google', "google.cn"));   //bool(true)
var_dump($redis->MSET('google',"google.hk")); //bool(false)
echo $redis->GET('google'); //google.cn  与redis手册的示例结果不符

MSETNX

MSETNX key value [key value ...]

同时设置一个或多个key-value对,当且仅当key不存在。

即使只有一个key已存在,MSETNX也会拒绝所有传入key的设置操作

MSETNX是原子性的,因此它可以用作设置多个不同key表示不同字段(field)的唯一性逻辑对象(unique logic object),所有字段要么全被设置,要么全不被设置。

时间复杂度:
O(N),N为要设置的key的数量。
返回值:
当所有key都成功设置,返回1
如果所有key都设置失败(最少有一个key已经存在),那么返回0
# 情况1:对不存在的key进行MSETNX
$array_mset=array('rmdbs'=>'MySQL',
        'nosql'=>'MongoDB',
        'key-value-store'=>'redis'
        );
$redis->MSETNX($array_mset);//bool(true)


# 情况2:对已存在的key进行MSETNX
$array_mset=array('rmdbs'=>'Sqlite',
        'language'=>'python'
        );
var_dump($redis->MSETNX($array_mset));  # rmdbs键已经存在,操作失败 //bool(false)
var_dump($redis->EXISTS('language'));  # 因为操作是原子性的,language没有被设置  bool(false)

echo $redis->GET('rmdbs');  # rmdbs没有被修改 //"MySQL"

$array_mset_keys=array( 'rmdbs', 'nosql', 'key-value-store');
print_r($redis->MGET($array_mset_keys)); //Array ( [0] => MySQL [1] => MongoDB [2] => redis )

APPEND

APPEND key value

如果key已经存在并且是一个字符串,APPEND命令将value追加到key原来的值之后。

如果key不存在,APPEND就简单地将给定key设为value,就像执行SET key value一样。

时间复杂度:
平摊复杂度O(1)
返回值:
追加value之后,key中字符串的长度。
# 情况1:对不存在的key执行APPEND

$redis->EXISTS('myphone');  # 确保myphone不存在 //bool(false)
$redis->APPEND('myphone',"nokia");  # 对不存在的key进行APPEND,等同于SET myphone "nokia" //int(5) # 字符长度

# 情况2:对字符串进行APPEND
$redis->APPEND('myphone', " - 1110");# 长度从5个字符增加到12个字符 //int(12)

echo $redis->GET('myphone');  # 查看整个字符串 //"nokia - 1110"

GET

GET key

返回key所关联的字符串值。

如果key不存在则返回特殊值nil

假如key储存的值不是字符串类型,返回一个错误,因为GET只能用于处理字符串值。

时间复杂度:
O(1)
返回值:
key的值。
如果key不存在,返回nil

//GET
var_dump($redis->GET('fake_key')); #(nil) //return bool(false)
$redis->SET('animate', "anohana"); //return bool(true)
var_dump($redis->GET('animate')); //return string(7) "anohana"

MGET

MGET key [key ...]

返回所有(一个或多个)给定key的值。

如果某个指定key不存在,那么返回特殊值nil。因此,该命令永不失败。

时间复杂度:
O(1)
返回值:
一个包含所有给定key的值的列表。
//MGET
echo '<br><br>MGET<br>';
$redis_mget_data_array=array('name'=>'ikodota','blog'=>'cnblogs.com/ikodota');
$redis->MSET($redis_mget_data_array);#用MSET一次储存多个值 

$redis_mget_key_array=array('name','blog');
var_dump($redis->MGET($redis_mget_key_array)); //array(2) { [0]=> string(7) "ikodota" [1]=> string(19) "cnblogs.com/ikodota" }

$redis->EXISTS('fake_key'); //bool(false)

$redis_mget_key_array=array('name','fake_key');
var_dump($redis->MGET($redis_mget_key_array));  # 当MGET中有不存在key的情况   //array(2) { [0]=> string(7) "ikodota" [1]=> bool(false) }

GETRANGE

GETRANGE key start end

返回key中字符串值的子字符串,字符串的截取范围由startend两个偏移量决定(包括startend在内)。

负数偏移量表示从字符串最后开始计数,-1表示最后一个字符,-2表示倒数第二个,以此类推。

GETRANGE通过保证子字符串的值域(range)不超过实际字符串的值域来处理超出范围的值域请求。

时间复杂度:
O(N),N为要返回的字符串的长度。
复杂度最终由返回值长度决定,但因为从已有字符串中建立子字符串的操作非常廉价(cheap),所以对于长度不大的字符串,该操作的复杂度也可看作O(1)。
返回值:
截取得出的子字符串。

注解:在<=2.0的版本里,GETRANGE被叫作SUBSTR。

//GETRANGE
echo '<br><br>GETRANGE<br>';
$redis->SET('greeting', "hello, my friend");
echo $redis->GETRANGE('greeting', 0, 4).'<br>';  # 返回索引0-4的字符,包括4。 //"hello"
echo $redis->GETRANGE('greeting', -1 ,-5).'<br>';  # 不支持回绕操作  //""
echo $redis->GETRANGE('greeting', -3 ,-1).'<br>';  # 负数索引 //"end"
echo $redis->GETRANGE('greeting', 0, -1).'<br>';  # 从第一个到最后一个 //"hello, my friend"
echo $redis->GETRANGE('greeting', 0, 1008611).'<br>';  # 值域范围不超过实际字符串,超过部分自动被符略 //"hello, my friend"

GETSET

GETSET key value

将给定key的值设为value,并返回key的旧值。

key存在但不是字符串类型时,返回一个错误。

时间复杂度:
O(1)
返回值:
返回给定key的旧值(old value)。
key没有旧值时,返回nil
//GETSET
echo '<br><br>GETSET<br>';
var_dump($redis->EXISTS('mail'));//return bool(false);
var_dump($redis->GETSET('mail','xxx@google.com'));  # 因为mail之前不存在,没有旧值,返回nil ,#(nil)   //bool(false)

var_dump($redis->GETSET('mail','xxx@yahoo.com'));  # mail被更新,旧值被返回 //string(14) "xxx@google.com"

设计模式

GETSET可以和INCR组合使用,实现一个有原子性(atomic)复位操作的计数器(counter)。

举例来说,每次当某个事件发生时,进程可能对一个名为mycountkey调用INCR操作,通常我们还要在一个原子时间内同时完成获得计数器的值和将计数器值复位为0两个操作。

可以用命令GETSET mycounter 0来实现这一目标。

$redis->SELECT(2);
echo $redis->INCR('mycount').'<br>'; #(integer) 11

if($redis->GET('mycount')>19){
    echo $redis->GETSET('mycount', 0).'<br>';  # 一个原子内完成GET mycount和SET mycount 0操作 #"11" 
}
echo $redis->GET('mycount'); #"0"

STRLEN

STRLEN key

返回key所储存的字符串值的长度。

key储存的不是字符串值时,返回一个错误。

复杂度:
O(1)
返回值:
字符串值的长度。
当 key不存在时,返回0
$redis->SET('mykey', "Hello world");
echo $redis->STRLEN('mykey'); //int(11)
echo $redis->STRLEN('nonexisting'); # 不存在的key长度视为0  //int(0)

INCR

INCR key

key中储存的数字值增一。

如果key不存在,以0key的初始值,然后执行INCR操作。

如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。

本操作的值限制在64位(bit)有符号数字表示之内。

时间复杂度:
O(1)
返回值:
执行INCR命令之后key的值。

注解:这是一个针对字符串的操作,因为Redis没有专用的整数类型,所以key内储存的字符串被解释为十进制64位有符号整数来执行INCR操作。

$redis->SET('page_view', 20);
var_dump($redis->INCR('page_view')); //int(21) 
var_dump($redis->GET('page_view'));    # 数字值在Redis中以字符串的形式保存 //string(2) "21

INCRBY

INCRBY key increment

key所储存的值加上增量increment

如果key不存在,以0key的初始值,然后执行INCRBY命令。

如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。

本操作的值限制在64位(bit)有符号数字表示之内。

关于更多递增(increment)/递减(decrement)操作信息,参见INCR命令。

时间复杂度:
O(1)
返回值:
加上increment之后,key的值。
//INCRBY
echo '<br><br>INCRBY<br>';
# 情况1:key存在且是数字值
$redis->SET('rank', 50);  # 设置rank为50
$redis->INCRBY('rank', 20);  # 给rank加上20
var_dump($redis->GET('rank')); #"70"   //string(2) "70"

# 情况2:key不存在
$redis->EXISTS('counter'); //bool(false)
$redis->INCRBY('counter'); #int 30  //bool(false)
var_dump($redis->GET('counter')); #30 //经测试 与手册上结果不一样,不能直接从bool型转为int型。 return bool(false) 

# 情况3:key不是数字值
$redis->SET('book', "long long ago...");
var_dump($redis->INCRBY('book', 200)); #(error) ERR value is not an integer or out of range   // bool(false)

DECR

DECR key

key中储存的数字值减一。

如果key不存在,以0key的初始值,然后执行DECR操作。

如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。

本操作的值限制在64位(bit)有符号数字表示之内。

关于更多递增(increment)/递减(decrement)操作信息,参见INCR命令。

时间复杂度:
O(1)
返回值:
执行DECR命令之后key的值。
//DECR
$redis->SELECT(3);
$redis->flushdb();
echo '<br><br>DECR<br>';
# 情况1:对存在的数字值key进行DECR
$redis->SET('failure_times', 10);
$redis->DECR('failure_times'); //int(9)
echo $redis->GET('failure_times').'<br>';  //string(1) "9"

# 情况2:对不存在的key值进行DECR
$redis->EXISTS('count'); #(integer) 0 //bool(false)
$redis->DECR('count');  //int(-1) 
echo $redis->GET('count').'<br>'; //string(2) "-1"

# 情况3:对存在但不是数值的key进行DECR
$redis->SET('company', 'YOUR_CODE_SUCKS.LLC');
var_dump($redis->DECR('company')); #(error) ERR value is not an integer or out of range   //bool(false)
echo $redis->GET('company').'<br>'; //YOUR_CODE_SUCKS.LLC

DECRBY

DECRBY key decrement

key所储存的值减去减量decrement

如果key不存在,以0key的初始值,然后执行DECRBY操作。

如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。

本操作的值限制在64位(bit)有符号数字表示之内。

关于更多递增(increment)/递减(decrement)操作信息,参见INCR命令。

时间复杂度:
O(1)
返回值:
减去decrement之后,key的值。
# 情况1:对存在的数值key进行DECRBY
$redis->SET('count', 100);
var_dump($redis->DECRBY('count', 20)); //int(80)
var_dump($redis->GET('count'));  //string(2) "80"

# 情况2:对不存在的key进行DECRBY
$redis->EXISTS('pages');#(integer) 0  //bool(false)
var_dump($redis->DECRBY('pages', 10));  //int(-10)
var_dump($redis->GET('pages')); //string(3) "-10"

SETBIT

SETBIT key offset value

key所储存的字符串值,设置或清除指定偏移量上的位(bit)。

位的设置或清除取决于value参数,可以是0也可以是1

key不存在时,自动生成一个新的字符串值。

字符串会增长(grown)以确保它可以将value保存在指定的偏移量上。当字符串值增长时,空白位置以0填充。

offset参数必须大于或等于0,小于2^32(bit映射被限制在512MB内)。

时间复杂度:
O(1)
返回值:
指定偏移量原来储存的位(”0″或”1″).

警告:对使用大的offset的SETBIT操作来说,内存分配可能造成Redis服务器被阻塞。具体参考SETRANGE命令,warning(警告)部分。

//SETBIT
echo '<br><br>SETBIT<br>';
$bit_val=67;
echo decbin($bit_val).'<br>'; //1000011
var_dump($redis->SETBIT('bit',1,1));//int(0)  空位上都是0
var_dump($redis->SETBIT('bit',2,0));//int(0)
var_dump($redis->SETBIT('bit',3,0));//int(0)
var_dump($redis->SETBIT('bit',4,0));//int(0)
var_dump($redis->SETBIT('bit',5,0));//int(0)
var_dump($redis->SETBIT('bit',6,1));//int(0)
var_dump($redis->SETBIT('bit',7,1));//int(0)

var_dump($redis->GET('bit')); //string(1) "C" ,二进制为:1000011 ,ASCII:67

var_dump($redis->GETBIT('bit', 6 )); //int(1)  取出第6位(从左到右)为“1”

var_dump($redis->SETBIT('bit',5,1));//int(0)  把第5位的0改为1
var_dump($redis->SETBIT('bit',6,0));//int(1)  把第6位的1改为0

var_dump($redis->GET('bit')); //string(1) "E ,二进制为:1000101,ASCII:69l

GETBIT

GETBIT key offset

key所储存的字符串值,获取指定偏移量上的位(bit)。

offset比字符串值的长度大,或者key不存在时,返回0

时间复杂度:
O(1)
返回值:
字符串值指定偏移量上的位(bit)。
#参见SETBIT的示例

哈希表(Hash)

HSET

HSET key field value

将哈希表key中的域field的值设为value

如果key不存在,一个新的哈希表被创建并进行HSET操作。

如果域field已经存在于哈希表中,旧值将被覆盖。

时间复杂度:
O(1)
返回值:
如果field是哈希表中的一个新建域,并且值设置成功,返回1
如果哈希表中域field已经存在且旧值已被新值覆盖,返回0

HSETNX

HSETNX key field value

将哈希表key中的域field的值设置为value,当且仅当域field不存在。

若域field已经存在,该操作无效。

如果key不存在,一个新哈希表被创建并执行HSETNX命令。

时间复杂度:
O(1)
返回值:
设置成功,返回1
如果给定域已经存在且没有操作被执行,返回0

HMSET

HMSET key field value [field value ...]

同时将多个field - value(域-值)对设置到哈希表key中。

此命令会覆盖哈希表中已存在的域。

如果key不存在,一个空哈希表被创建并执行HMSET操作。

时间复杂度:
O(N),Nfield - value对的数量。
返回值:
如果命令执行成功,返回OK
key不是哈希表(hash)类型时,返回一个错误。

HGET

HGET key field

返回哈希表key中给定域field的值。

时间复杂度:
O(1)
返回值:
给定域的值。
当给定域不存在或是给定key不存在时,返回nil

HMGET

HMGET key field [field ...]

返回哈希表key中,一个或多个给定域的值。

如果给定的域不存在于哈希表,那么返回一个nil值。

因为不存在的key被当作一个空哈希表来处理,所以对一个不存在的key进行HMGET操作将返回一个只带有nil值的表。

时间复杂度:
O(N),N为给定域的数量。
返回值:
一个包含多个给定域的关联值的表,表值的排列顺序和给定域参数的请求顺序一样。

HGETALL

HGETALL key

返回哈希表key中,所有的域和值。

在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。

时间复杂度:
O(N),N为哈希表的大小。
返回值:
以列表形式返回哈希表的域和域的值。 若key不存在,返回空列表。

HDEL

HDEL key field [field ...]

删除哈希表key中的一个或多个指定域,不存在的域将被忽略。

时间复杂度:
O(N),N为要删除的域的数量。
返回值:
被成功移除的域的数量,不包括被忽略的域。

注解:在Redis2.4以下的版本里,HDEL每次只能删除单个域,如果你需要在一个原子时间内删除多个域,请将命令包含在MULTIEXEC块内。

HLEN

HLEN key

返回哈希表key中域的数量。

时间复杂度:
O(1)
返回值:
哈希表中域的数量。
key不存在时,返回0

HEXISTS

HEXISTS key field

查看哈希表key中,给定域field是否存在。

时间复杂度:
O(1)
返回值:
如果哈希表含有给定域,返回1
如果哈希表不含有给定域,或key不存在,返回0

HINCRBY

HINCRBY key field increment

为哈希表key中的域field的值加上增量increment

增量也可以为负数,相当于对给定域进行减法操作。

如果key不存在,一个新的哈希表被创建并执行HINCRBY命令。

如果域field不存在,那么在执行命令前,域的值被初始化为0

对一个储存字符串值的域field执行HINCRBY命令将造成一个错误。

本操作的值限制在64位(bit)有符号数字表示之内。

时间复杂度:
O(1)
返回值:
执行HINCRBY命令之后,哈希表key中域field的值。

HKEYS

HKEYS key

返回哈希表key中的所有域。

时间复杂度:
O(N),N为哈希表的大小。
返回值:
一个包含哈希表中所有域的表。
key不存在时,返回一个空表。

HVALS

HVALS key

返回哈希表key中的所有值。

时间复杂度:
O(N),N为哈希表的大小。
返回值:
一个包含哈希表中所有值的表。
key不存在时,返回一个空表。

表(List)

头元素和尾元素

头元素指的是列表左端/前端第一个元素,尾元素指的是列表右端/后端第一个元素。

举个例子,列表list包含三个元素:x, y, z,其中x是头元素,而z则是尾元素。

空列表

指不包含任何元素的列表,Redis将不存在的key也视为空列表。

LPUSH

LPUSH key value [value ...]

将一个或多个值value插入到列表key表头

如果有多个value值,那么各个value值按从左到右的顺序依次插入到表头:比如对一个空列表(mylist)执行LPUSH mylist a b c,则结果列表为c b a,等同于执行执行命令LPUSH mylist aLPUSH mylist bLPUSH mylist c

如果key不存在,一个空列表会被创建并执行LPUSH操作。

key存在但不是列表类型时,返回一个错误。

时间复杂度:
O(1)
返回值:
执行LPUSH命令后,列表的长度。

注解:在Redis 2.4版本以前的LPUSH命令,都只接受单个value值。

LPUSHX

LPUSHX key value

将值value插入到列表key的表头,当且仅当key存在并且是一个列表。

和LPUSH命令相反,当key不存在时,LPUSHX命令什么也不做。

时间复杂度:
O(1)
返回值:
LPUSHX命令执行之后,表的长度。

RPUSH

RPUSH key value [value ...]

将一个或多个值value插入到列表key表尾

如果有多个value值,那么各个value值按从左到右的顺序依次插入到表尾:比如对一个空列表(mylist)执行RPUSH mylist a b c,则结果列表为a b c,等同于执行命令RPUSHmylist aRPUSH mylist bRPUSH mylist c

如果key不存在,一个空列表会被创建并执行RPUSH操作。

key存在但不是列表类型时,返回一个错误。

时间复杂度:
O(1)
返回值:
执行RPUSH操作后,表的长度。

注解:在Redis 2.4版本以前的RPUSH命令,都只接受单个value值。

RPUSHX

RPUSHX key value

将值value插入到列表key的表尾,当且仅当key存在并且是一个列表。

和RPUSH命令相反,当key不存在时,RPUSHX命令什么也不做。

时间复杂度:
O(1)
返回值:
RPUSHX命令执行之后,表的长度。

LPOP

LPOP key

移除并返回列表key的头元素。

时间复杂度:
O(1)
返回值:
列表的头元素。
key不存在时,返回nil

RPOP

RPOP key

移除并返回列表key的尾元素。

时间复杂度:
O(1)
返回值:
列表的尾元素。
key不存在时,返回nil

BLPOP

BLPOP key [key ...] timeout

BLPOP是列表的阻塞式(blocking)弹出原语。

它是LPOP命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被BLPOP命令阻塞,直到等待超时或发现可弹出元素为止。

当给定多个key参数时,按参数key的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。

非阻塞行为

当BLPOP被调用时,如果给定key内至少有一个非空列表,那么弹出遇到的第一个非空列表的头元素,并和被弹出元素所属的列表的名字一起,组成结果返回给调用者。

当存在多个给定key时,BLPOP按给定key参数排列的先后顺序,依次检查各个列表。

假设现在有job、 commandrequest三个列表,其中job不存在,commandrequest都持有非空列表。考虑以下命令:

BLPOP job command request 0

BLPOP保证返回的元素来自command,因为它是按”查找job -> 查找command -> 查找request“这样的顺序,第一个找到的非空列表。

阻塞行为

如果所有给定key都不存在或包含空列表,那么BLPOP命令将阻塞连接,直到等待超时,或有另一个客户端对给定key的任意一个执行LPUSH或RPUSH命令为止。

超时参数timeout接受一个以秒为单位的数字作为值。超时参数设为0表示阻塞时间可以无限期延长(block indefinitely) 。

相同的key被多个客户端同时阻塞

相同的key可以被多个客户端同时阻塞。
不同的客户端被放进一个队列中,按”先阻塞先服务”(first-BLPOP,first-served)的顺序为key执行BLPOP命令。

在MULTI/EXEC事务中的BLPOP

BLPOP可以用于流水线(pipline,批量地发送多个命令并读入多个回复),但把它用在MULTI/EXEC块当中没有意义。因为这要求整个服务器被阻塞以保证块执行时的原子性,该行为阻止了其他客户端执行LPUSH或RPUSH命令。

因此,一个被包裹在MULTI/EXEC块内的BLPOP命令,行为表现得就像LPOP一样,对空列表返回nil,对非空列表弹出列表元素,不进行任何阻塞操作。

时间复杂度:O(1)返回值:

如果列表为空,返回一个nil
反之,返回一个含有两个元素的列表,第一个元素是被弹出元素所属的key,第二个元素是被弹出元素的值。

BRPOP

BRPOP key [key ...] timeout

BRPOP是列表的阻塞式(blocking)弹出原语。

它是RPOP命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被BRPOP命令阻塞,直到等待超时或发现可弹出元素为止。

当给定多个key参数时,按参数key的先后顺序依次检查各个列表,弹出第一个非空列表的尾部元素。

关于阻塞操作的更多信息,请查看BLPOP命令,BRPOP除了弹出元素的位置和BLPOP不同之外,其他表现一致。

时间复杂度:
O(1)
返回值:
假如在指定时间内没有任何元素被弹出,则返回一个nil和等待时长。
反之,返回一个含有两个元素的列表,第一个元素是被弹出元素所属的key,第二个元素是被弹出元素的值。

LLEN

LLEN key

返回列表key的长度。

如果key不存在,则key被解释为一个空列表,返回0.

如果key不是列表类型,返回一个错误。

时间复杂度:
O(1)
返回值:
列表key的长度。

LRANGE

LRANGE key start stop

返回列表key中指定区间内的元素,区间以偏移量startstop指定。

下标(index)参数startstop都以0为底,也就是说,以0表示列表的第一个元素,以1表示列表的第二个元素,以此类推。

你也可以使用负数下标,以-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推。

注意LRANGE命令和编程语言区间函数的区别

假如你有一个包含一百个元素的列表,对该列表执行LRANGE list 0 10,结果是一个包含11个元素的列表,这表明stop下标也在LRANGE命令的取值范围之内(闭区间),这和某些语言的区间函数可能不一致,比如Ruby的Range.newArray#slice和Python的range()函数。

超出范围的下标

超出范围的下标值不会引起错误。

如果start下标比列表的最大下标end(LLEN list减去1)还要大,或者start > stop,LRANGE返回一个空列表。

如果stop下标比end下标还要大,Redis将stop的值设置为end

时间复杂度:
O(S+N),S为偏移量startN为指定区间内元素的数量。
返回值:
一个列表,包含指定区间内的元素。

LREM

LREM key count value

根据参数count的值,移除列表中与参数value相等的元素。

count的值可以是以下几种:
  • count > 0: 从表头开始向表尾搜索,移除与value相等的元素,数量为count
  • count < 0: 从表尾开始向表头搜索,移除与value相等的元素,数量为count的绝对值。
  • count = 0: 移除表中所有与value相等的值。
时间复杂度:
O(N),N为列表的长度。
返回值:
被移除元素的数量。
因为不存在的key被视作空表(empty list),所以当key不存在时,LREM命令总是返回0

LSET

LSET key index value

将列表key下标为index的元素的值甚至为value

更多信息请参考LINDEX操作。

index参数超出范围,或对一个空列表(key不存在)进行LSET时,返回一个错误。

时间复杂度:
对头元素或尾元素进行LSET操作,复杂度为O(1)。
其他情况下,为O(N),N为列表的长度。
返回值:
操作成功返回ok,否则返回错误信息

LTRIM

LTRIM key start stop

对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。

举个例子,执行命令LTRIM list 0 2,表示只保留列表list的前三个元素,其余元素全部删除。

下标(index)参数startstop都以0为底,也就是说,以0表示列表的第一个元素,以1表示列表的第二个元素,以此类推。

你也可以使用负数下标,以-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推。

key不是列表类型时,返回一个错误。

LTRIM命令通常和LPUSH命令或RPUSH命令配合使用,举个例子:

这个例子模拟了一个日志程序,每次将最新日志newest_log放到log列表中,并且只保留最新的100项。注意当这样使用LTRIM命令时,时间复杂度是O(1),因为平均情况下,每次只有一个元素被移除。

注意LTRIM命令和编程语言区间函数的区别

假如你有一个包含一百个元素的列表list,对该列表执行LTRIM list 0 10,结果是一个包含11个元素的列表,这表明stop下标也在LTRIM命令的取值范围之内(闭区间),这和某些语言的区间函数可能不一致,比如Ruby的Range.newArray#slice和Python的range()函数。

超出范围的下标

超出范围的下标值不会引起错误。

如果start下标比列表的最大下标end(LLEN list减去1)还要大,或者start > stop,LTRIM返回一个空列表(因为LTRIM已经将整个列表清空)。

如果stop下标比end下标还要大,Redis将stop的值设置为end

时间复杂度:
O(N),N为被移除的元素的数量。
返回值:
命令执行成功时,返回ok

LINDEX

LINDEX key index

返回列表key中,下标为index的元素。

下标(index)参数startstop都以0为底,也就是说,以0表示列表的第一个元素,以1表示列表的第二个元素,以此类推。

你也可以使用负数下标,以-1表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推。

如果key不是列表类型,返回一个错误。

时间复杂度:
O(N),N为到达下标index过程中经过的元素数量。
因此,对列表的头元素和尾元素执行LINDEX命令,复杂度为O(1)。
返回值:
列表中下标为index的元素。
如果index参数的值不在列表的区间范围内(out of range),返回nil

LINSERT

LINSERT key BEFORE|AFTER pivot value

将值value插入到列表key当中,位于值pivot之前或之后。

pivot不存在于列表key时,不执行任何操作。

key不存在时,key被视为空列表,不执行任何操作。

如果key不是列表类型,返回一个错误。

时间复杂度:
O(N),N为寻找pivot过程中经过的元素数量。
返回值:
如果命令执行成功,返回插入操作完成之后,列表的长度。
如果没有找到pivot,返回-1
如果key不存在或为空列表,返回0

RPOPLPUSH

RPOPLPUSH source destination

命令RPOPLPUSH在一个原子时间内,执行以下两个动作:

  • 将列表source中的最后一个元素(尾元素)弹出,并返回给客户端。
  • source弹出的元素插入到列表destination,作为destination列表的的头元素。

举个例子,你有两个列表sourcedestinationsource列表有元素a, b, cdestination列表有元素x, y, z,执行RPOPLPUSH source destination之后,source列表包含元素a, bdestination列表包含元素c, x, y, z ,并且元素c被返回。

如果source不存在,值nil被返回,并且不执行其他动作。

如果sourcedestination相同,则列表中的表尾元素被移动到表头,并返回该元素,可以把这种特殊情况视作列表的旋转(rotation)操作。

时间复杂度:
O(1)
返回值:
被弹出的元素。

设计模式: 一个安全的队列

Redis的列表经常被用作队列(queue),用于在不同程序之间有序地交换消息(message)。一个程序(称之为生产者,producer)通过LPUSH命令将消息放入队列中,而另一个程序(称之为消费者,consumer)通过RPOP命令取出队列中等待时间最长的消息。

不幸的是,在这个过程中,一个消费者可能在获得一个消息之后崩溃,而未执行完成的消息也因此丢失。

使用RPOPLPUSH命令可以解决这个问题,因为它在返回一个消息之余,还将该消息添加到另一个列表当中,另外的这个列表可以用作消息的备份表:假如一切正常,当消费者完成该消息的处理之后,可以用LREM命令将该消息从备份表删除。

另一方面,助手(helper)程序可以通过监视备份表,将超过一定处理时限的消息重新放入队列中去(负责处理该消息的消费者可能已经崩溃),这样就不会丢失任何消息了。

BRPOPLPUSH

BRPOPLPUSH source destination timeout

BRPOPLPUSH是RPOPLPUSH的阻塞版本,当给定列表source不为空时,BRPOPLPUSH的表现和RPOPLPUSH一样。

当列表source为空时,BRPOPLPUSH命令将阻塞连接,直到等待超时,或有另一个客户端对source执行LPUSH或RPUSH命令为止。

超时参数timeout接受一个以秒为单位的数字作为值。超时参数设为0表示阻塞时间可以无限期延长(block indefinitely) 。

更多相关信息,请参考RPOPLPUSH命令。

时间复杂度:
O(1)
返回值:
假如在指定时间内没有任何元素被弹出,则返回一个nil和等待时长。
反之,返回一个含有两个元素的列表,第一个元素是被弹出元素的值,第二个元素是等待时长。

集合(Set)

 附录,常用集合运算:

   A = {‘a’, ‘b’, ‘c’}
B = {‘a’, ‘e’, ‘i’, ‘o’, ‘u’}

inter(x, y): 交集,在集合x和集合y中都存在的元素。
inter(A, B) = {'a'}

union(x, y): 并集,在集合x中或集合y中的元素,如果一个元素在x和y中都出现,那只记录一次即可。
union(A,B) = {'a', 'b', 'c', 'e', 'i', 'o', 'u'}

diff(x, y): 差集,在集合x中而不在集合y中的元素。
diff(A,B) = {'b', 'c'}

card(x): 基数,一个集合中元素的数量。
card(A) = 3

空集: 基数为0的集合。

SADD

SADD key member [member ...]

将一个或多个member元素加入到集合key当中,已经存在于集合的member元素将被忽略。

假如key不存在,则创建一个只包含member元素作成员的集合。

key不是集合类型时,返回一个错误。

时间复杂度:
O(N),N是被添加的元素的数量。
返回值:
被添加到集合中的元素的数量,不包括被忽略的元素。

注解:在Redis2.4版本以前,SADD只接受单个member值。

SREM

SREM key member [member ...]

移除集合key中的一个或多个member元素,不存在的member元素会被忽略。

key不是集合类型,返回一个错误。

时间复杂度:
O(N),N为给定member元素的数量。
返回值:
被成功移除的元素的数量,不包括被忽略的元素。

注解:在Redis2.4版本以前,SREM只接受单个member值。

SMEMBERS

SMEMBERS key

返回集合key中的所有成员。

时间复杂度:
O(N),N为集合的基数。
返回值:
集合中的所有成员。

SISMEMBER

SISMEMBER key member

判断member元素是否是集合key的成员。

时间复杂度:
O(1)
返回值:
如果member元素是集合的成员,返回1
如果member元素不是集合的成员,或key不存在,返回0

SCARD

SCARD key

返回集合key基数(集合中元素的数量)。

时间复杂度:
O(1)
返回值:
集合的基数。
key不存在时,返回0

SMOVE

SMOVE source destination member

member元素从source集合移动到destination集合。

SMOVE是原子性操作。

如果source集合不存在或不包含指定的member元素,则SMOVE命令不执行任何操作,仅返回0。否则,member元素从source集合中被移除,并添加到destination集合中去。

destination集合已经包含member元素时,SMOVE命令只是简单地将source集合中的member元素删除。

sourcedestination不是集合类型时,返回一个错误。

时间复杂度:
O(1)
返回值:
如果member元素被成功移除,返回1
如果member元素不是source集合的成员,并且没有任何操作对destination集合执行,那么返回0

SPOP

SPOP key

移除并返回集合中的一个随机元素。

时间复杂度:
O(1)
返回值:
被移除的随机元素。
key不存在或key是空集时,返回nil

也可以参考:如果只想获取一个随机元素,但不想该元素从集合中被移除的话,可以使用SRANDMEMBER命令。

SRANDMEMBER

SRANDMEMBER key

返回集合中的一个随机元素。

该操作和SPOP相似,但SPOP将随机元素从集合中移除并返回,而SRANDMEMBER则仅仅返回随机元素,而不对集合进行任何改动。

时间复杂度:
O(1)
返回值:
被选中的随机元素。 当key不存在或key是空集时,返回nil

SINTER

SINTER key [key ...]

返回一个集合的全部成员,该集合是所有给定集合的交集

不存在的key被视为空集。

当给定集合当中有一个空集时,结果也为空集(根据集合运算定律)。

时间复杂度:
O(N * M),N为给定集合当中基数最小的集合,M为给定集合的个数。
返回值:
交集成员的列表。

SINTERSTORE

SINTERSTORE destination key [key ...]

此命令等同于SINTER,但它将结果保存到destination集合,而不是简单地返回结果集。

如果destination集合已经存在,则将其覆盖。

destination可以是key本身。

时间复杂度:
O(N * M),N为给定集合当中基数最小的集合,M为给定集合的个数。
返回值:
结果集中的成员数量。

SUNION

SUNION key [key ...]

返回一个集合的全部成员,该集合是所有给定集合的并集

不存在的key被视为空集。

时间复杂度:
O(N),N是所有给定集合的成员数量之和。
返回值:
并集成员的列表。

SUNIONSTORE

SUNIONSTORE destination key [key ...]

此命令等同于SUNION,但它将结果保存到destination集合,而不是简单地返回结果集。

如果destination已经存在,则将其覆盖。

destination可以是key本身。

时间复杂度:
O(N),N是所有给定集合的成员数量之和。
返回值:
结果集中的元素数量。

SDIFF

SDIFF key [key ...]

返回一个集合的全部成员,该集合是所有给定集合的差集 。

不存在的key被视为空集。

时间复杂度:
O(N),N是所有给定集合的成员数量之和。
返回值:
交集成员的列表。

SDIFFSTORE

SDIFFSTORE destination key [key ...]

此命令等同于SDIFF,但它将结果保存到destination集合,而不是简单地返回结果集。

如果destination集合已经存在,则将其覆盖。

destination可以是key本身。

时间复杂度:
O(N),N是所有给定集合的成员数量之和。
返回值:
结果集中的元素数量。

有序集(Sorted Set)

ZADD

ZADD key score member [[score member] [score member] ...]

将一个或多个member元素及其score值加入到有序集key当中。

如果某个member已经是有序集的成员,那么更新这个memberscore值,并通过重新插入这个member元素,来保证该member在正确的位置上。

score值可以是整数值或双精度浮点数。

如果key不存在,则创建一个空的有序集并执行ZADD操作。

key存在但不是有序集类型时,返回一个错误。

对有序集的更多介绍请参见sorted set。

时间复杂度:
O(M*log(N)),N是有序集的基数,M为成功添加的新成员的数量。
返回值:
被成功添加的成员的数量,不包括那些被更新的、已经存在的成员。

注解:在Redis2.4版本以前,ZADD每次只能添加一个元素。

ZREM

ZREM key member [member ...]

移除有序集key中的一个或多个成员,不存在的成员将被忽略。

key存在但不是有序集类型时,返回一个错误。

时间复杂度:
O(M*log(N)),N为有序集的基数,M为被成功移除的成员的数量。
返回值:
被成功移除的成员的数量,不包括被忽略的成员。

注解:在Redis2.4版本以前,ZREM每次只能删除一个元素。

ZCARD

ZCARD key

返回有序集key的基数。

时间复杂度:
O(1)
返回值:
key存在且是有序集类型时,返回有序集的基数。
key不存在时,返回0

ZCOUNT

ZCOUNT key min max

返回有序集key中,score值在minmax之间(默认包括score值等于minmax)的成员。

关于参数minmax的详细使用方法,请参考ZRANGEBYSCORE命令。

时间复杂度:
O(log(N)+M),N为有序集的基数,M为值在minmax之间的元素的数量。
返回值:
score值在minmax之间的成员的数量。

ZSCORE

ZSCORE key member

返回有序集key中,成员memberscore值。

如果member元素不是有序集key的成员,或key不存在,返回nil

时间复杂度:
O(1)
返回值:
member成员的score值,以字符串形式表示。

ZINCRBY

ZINCRBY key increment member

为有序集key的成员memberscore值加上增量increment

你也可以通过传递一个负数值increment,让score减去相应的值,比如ZINCRBY key -5 member,就是让memberscore值减去5

key不存在,或member不是key的成员时,ZINCRBY key increment member等同于ZADD key increment member

key不是有序集类型时,返回一个错误。

score值可以是整数值或双精度浮点数。

时间复杂度:
O(log(N))
返回值:
member成员的新score值,以字符串形式表示。

ZRANGE

ZRANGE key start stop [WITHSCORES]

返回有序集key中,指定区间内的成员。

其中成员的位置按score值递增(从小到大)来排序。

具有相同score值的成员按字典序(lexicographical order)来排列。

如果你需要成员按score值递减(从大到小)来排列,请使用ZREVRANGE命令。

下标参数startstop都以0为底,也就是说,以0表示有序集第一个成员,以1表示有序集第二个成员,以此类推。
你也可以使用负数下标,以-1表示最后一个成员,-2表示倒数第二个成员,以此类推。
超出范围的下标并不会引起错误。
比如说,当start的值比有序集的最大下标还要大,或是start > stop时,ZRANGE命令只是简单地返回一个空列表。
另一方面,假如stop参数的值比有序集的最大下标还要大,那么Redis将stop当作最大下标来处理。
可以通过使用WITHSCORES选项,来让成员和它的score值一并返回,返回列表以value1,score1, ..., valueN,scoreN的格式表示。
客户端库可能会返回一些更复杂的数据类型,比如数组、元组等。
时间复杂度:
O(log(N)+M),N为有序集的基数,而M为结果集的基数。
返回值:
指定区间内,带有score值(可选)的有序集成员的列表。

ZREVRANGE

ZREVRANGE key start stop [WITHSCORES]

返回有序集key中,指定区间内的成员。

其中成员的位置按score值递减(从大到小)来排列。
具有相同score值的成员按字典序的反序(reverse lexicographical order)排列。

除了成员按score值递减的次序排列这一点外,ZREVRANGE命令的其他方面和ZRANGE命令一样。

时间复杂度:
O(log(N)+M),N为有序集的基数,而M为结果集的基数。
返回值:
指定区间内,带有score值(可选)的有序集成员的列表。

ZRANGEBYSCORE

ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

返回有序集key中,所有score值介于minmax之间(包括等于minmax)的成员。有序集成员按score值递增(从小到大)次序排列。

具有相同score值的成员按字典序(lexicographical order)来排列(该属性是有序集提供的,不需要额外的计算)。

可选的LIMIT参数指定返回结果的数量及区间(就像SQL中的SELECT LIMIT offset, count),注意当offset很大时,定位offset的操作可能需要遍历整个有序集,此过程最坏复杂度为O(N)时间。

可选的WITHSCORES参数决定结果集是单单返回有序集的成员,还是将有序集成员及其score值一起返回。
该选项自Redis 2.0版本起可用。

区间及无限

minmax可以是-inf+inf,这样一来,你就可以在不知道有序集的最低和最高score值的情况下,使用ZRANGEBYSCORE这类命令。

默认情况下,区间的取值使用闭区间(小于等于或大于等于),你也可以通过给参数前增加(符号来使用可选的开区间(小于或大于)。

举个例子:

返回所有符合条件1 < score <= 5的成员;

返回所有符合条件5 < score < 10的成员。

时间复杂度:
O(log(N)+M),N为有序集的基数,M为被结果集的基数。
返回值:
指定区间内,带有score值(可选)的有序集成员的列表。

ZREVRANGEBYSCORE

ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]

返回有序集key中,score值介于maxmin之间(默认包括等于maxmin)的所有的成员。有序集成员按score值递减(从大到小)的次序排列。

具有相同score值的成员按字典序的反序(reverse lexicographical order)排列。

除了成员按score值递减的次序排列这一点外,ZREVRANGEBYSCORE命令的其他方面和ZRANGEBYSCORE命令一样。

时间复杂度:
O(log(N)+M),N为有序集的基数,M为结果集的基数。
返回值:
指定区间内,带有score值(可选)的有序集成员的列表。

ZRANK

ZRANK key member

返回有序集key中成员member的排名。其中有序集成员按score值递增(从小到大)顺序排列。

排名以0为底,也就是说,score值最小的成员排名为0

使用ZREVRANK命令可以获得成员按score值递减(从大到小)排列的排名。

时间复杂度:
O(log(N))
返回值:
如果member是有序集key的成员,返回member的排名。
如果member不是有序集key的成员,返回nil

ZREVRANK

ZREVRANK key member

返回有序集key中成员member的排名。其中有序集成员按score值递减(从大到小)排序。

排名以0为底,也就是说,score值最大的成员排名为0

使用ZRANK命令可以获得成员按score值递增(从小到大)排列的排名。

时间复杂度:
O(log(N))
返回值:
如果member是有序集key的成员,返回member的排名。
如果member不是有序集key的成员,返回nil

ZREMRANGEBYRANK

ZREMRANGEBYRANK key start stop

移除有序集key中,指定排名(rank)区间内的所有成员。

区间分别以下标参数startstop指出,包含startstop在内。

下标参数startstop都以0为底,也就是说,以0表示有序集第一个成员,以1表示有序集第二个成员,以此类推。
你也可以使用负数下标,以-1表示最后一个成员,-2表示倒数第二个成员,以此类推。
时间复杂度:
O(log(N)+M),N为有序集的基数,而M为被移除成员的数量。
返回值:
被移除成员的数量。

ZREMRANGEBYSCORE

ZREMRANGEBYSCORE key min max

移除有序集key中,所有score值介于minmax之间(包括等于minmax)的成员。

自版本2.1.6开始,score值等于minmax的成员也可以不包括在内,详情请参见ZRANGEBYSCORE命令。

时间复杂度:
O(log(N)+M),N为有序集的基数,而M为被移除成员的数量。
返回值:
被移除成员的数量。

ZINTERSTORE

ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]

计算给定的一个或多个有序集的交集,其中给定key的数量必须以numkeys参数指定,并将该交集(结果集)储存到destination

默认情况下,结果集中某个成员的score值是所有给定集下该成员score值之

关于WEIGHTSAGGREGATE选项的描述,参见ZUNIONSTORE命令。

时间复杂度:
O(N*K)+O(M*log(M)),N为给定key中基数最小的有序集,K为给定有序集的数量,M为结果集的基数。
返回值:
保存到destination的结果集的基数。

ZUNIONSTORE

ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]

计算给定的一个或多个有序集的并集,其中给定key的数量必须以numkeys参数指定,并将该并集(结果集)储存到destination

默认情况下,结果集中某个成员的score值是所有给定集下该成员score值之

WEIGHTS

使用WEIGHTS选项,你可以为每个给定有序集分别指定一个乘法因子(multiplication factor),每个给定有序集的所有成员的score值在传递给聚合函数(aggregation function)之前都要先乘以该有序集的因子。

如果没有指定WEIGHTS选项,乘法因子默认设置为1

AGGREGATE

使用AGGREGATE选项,你可以指定并集的结果集的聚合方式。

默认使用的参数SUM,可以将所有集合中某个成员的score值之作为结果集中该成员的score值;使用参数MIN,可以将所有集合中某个成员的最小score值作为结果集中该成员的score值;而参数MAX则是将所有集合中某个成员的最大score值作为结果集中该成员的score值。

时间复杂度:
O(N)+O(M log(M)),N为给定有序集基数的总和,M为结果集的基数。
返回值:
保存到destination的结果集的基数。

发布/订阅(Pub/Sub)

PUBLISH

PUBLISH channel message

将信息 message 发送到指定的频道 channel 。

时间复杂度:
O(N+M),其中 N 是频道 channel 的订阅者数量,而 M 则是使用模式订阅(subscribed patterns)的客户端的数量。
返回值:
接收到信息 message 的订阅者数量。

SUBSCRIBE

SUBSCRIBE channel [channel ...]

订阅给定频道的信息。

时间复杂度:
O(N),其中 N 是订阅的频道的数量。
返回值:
接收到的信息(请参见下面的代码说明)。

PSUBSCRIBE

PSUBSCRIBE pattern [pattern ...]

订阅符合给定模式的频道。

每个模式以 * 作为匹配符,比如 huangz* 匹配所有以 huangz 开头的频道( huangzmsg 、 huangz-blog 、 huangz.tweets 等等), news.* 匹配所有以 news. 开头的频道(news.it 、 news.global.today 等等),诸如此类。

时间复杂度:
O(N), N 是订阅的模式的数量。
返回值:
接收到的信息(请参见下面的代码说明)。

UNSUBSCRIBE

警告:此命令在新版 Redis 中似乎已经被废弃?

PUNSUBSCRIBE

警告:此命令在新版 Redis 中似乎已经被废弃?

事务(Transaction)

WATCH

ATCH key [key ...]

监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

时间复杂度:
O(1)。
返回值:
总是返回 OK 。

UNWATCH

UNWATCH

取消 WATCH 命令对所有 key 的监视。

如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。

因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。

时间复杂度:
O(1)
返回值:
总是 OK 。

MULTI

MULTI

标记一个事务块的开始。

事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 EXEC 命令在一个原子时间内执行。

时间复杂度:
O(1)。
返回值:
总是返回 OK 。

EXEC

EXEC

执行所有事务块内的命令。

假如某个(或某些) key 正处于 WATCH 命令的监视之下,且事务块中有和这个(或这些) key 相关的命令,那么 EXEC 命令只在这个(或这些) key 没有被其他命令所改动的情况下执行并生效,否则该事务被打断(abort)。

时间复杂度:
事务块内所有命令的时间复杂度的总和。
返回值:
事务块内所有命令的返回值,按命令执行的先后顺序排列。
当操作被打断时,返回空值 nil 。

DISCARD

DISCARD

取消事务,放弃执行事务块内的所有命令。

如果正在使用 WATCH 命令监视某个(或某些) key ,那么取消所有监视,等同于执行命令 UNWATCH 。

时间复杂度:
O(1)。
返回值:
总是返回 OK 。

连接(Connection)

AUTH

AUTH password

通过设置配置文件中 requirepass 项的值(使用命令 CONFIG SET requirepass password ),可以使用密码来保护 Redis 服务器。

如果开启了密码保护的话,在每次连接 Redis 服务器之后,就要使用 AUTH 命令解锁,解锁之后才能使用其他 Redis 命令。

如果 AUTH 命令给定的密码 password 和配置文件中的密码相符的话,服务器会返回 OK 并开始接受命令输入。

反之,如果密码不匹配的话,服务器将返回一个错误,并要求客户端需重新输入密码。

警告:因为 Redis 高性能的特点,在很短时间内尝试猜测非常多个密码是有可能的,因此请确保使用的密码足够复杂和足够长,以免遭受密码猜测攻击。

时间复杂度:
O(1)
返回值:
密码匹配时返回 OK ,否则返回一个错误。

PING

PING

客户端向服务器发送一个 PING ,然后服务器返回客户端一个 PONG 。

通常用于测试与服务器的连接是否仍然生效,或者用于测量延迟值。

时间复杂度:
O(1)
返回值:
PONG

SELECT

SELECT index

切换到指定的数据库,数据库索引号用数字值指定,以 0 作为起始索引值。

新的链接总是使用 0 号数据库。

时间复杂度:
O(1)
返回值:
OK

ECHO

ECHO message

打印一个特定的信息 message ,测试时使用。

时间复杂度:
O(1)
返回值:
message 自身。

QUIT

QUIT

请求服务器关闭与当前客户端的连接。

一旦所有等待中的回复(如果有的话)顺利写入到客户端,连接就会被关闭。

时间复杂度:
O(1)
返回值:
总是返回 OK (但是不会被打印显示,因为当时 Redis-cli 已经退出)。

服务器(Server)

BGREWRITEAOF

BGREWRITEAOF

异步(Asynchronously)重写 AOF 文件以反应当前数据库的状态。

即使 BGREWRITEAOF 命令执行失败,旧 AOF 文件中的数据也不会因此丢失或改变。

时间复杂度:
O(N), N 为要追加到 AOF 文件中的数据数量。
返回值:
反馈信息。

BGSAVE

在后台异步保存当前数据库的数据到磁盘。

BGSAVE 命令执行之后立即返回 OK ,然后 Redis fork出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。

客户端可以通过 LASTSAVE 命令查看相关信息,判断 BGSAVE 命令是否执行成功。

时间复杂度:
O(N), N 为要保存到数据库中的 key 的数量。
返回值:
反馈信息。

SAVE

SAVE

同步保存当前数据库的数据到磁盘。

时间复杂度:
O(N), N 为要保存到数据库中的 key 的数量。
返回值:
总是返回 OK 。

LASTSAVE

LASTSAVE

返回最近一次 Redis 成功执行保存操作的时间点( SAVE 、 BGSAVE 等),以 UNIX 时间戳格式表示。

时间复杂度:
O(1)
返回值:
一个 UNIX 时间戳。

DBSIZE

DBSIZE

返回当前数据库的 key 的数量。

时间复杂度:
O(1)
返回值:
当前数据库的 key 的数量。

SLAVEOF

SLAVEOF host port

SLAVEOF 命令用于在 Redis 运行时动态地修改复制(replication)功能的行为。

通过执行 SLAVEOF host port 命令,可以将当前服务器转变为指定服务器的从属服务器(slave server)。

如果当前服务器已经是某个主服务器(master server)的从属服务器,那么执行 SLAVEOF host port 将使当前服务器停止对旧主服务器的同步,丢弃旧数据集,转而开始对新主服务器进行同步。

另外,对一个从属服务器执行命令 SLAVEOF NO ONE 将使得这个从属服务器关闭复制功能,并从从属服务器转变回主服务器,原来同步所得的数据集不会被丢弃。

利用“ SLAVEOF NO ONE 不会丢弃同步所得数据集”这个特性,可以在主服务器失败的时候,将从属服务器用作新的主服务器,从而实现无间断运行。

时间复杂度:
SLAVEOF host port ,O(N), N 为要同步的数据数量。
SLAVEOF NO ONE , O(1) 。
返回值:
总是返回 OK 。

FLUSHALL

FLUSHALL

清空整个 Redis 服务器的数据(删除所有数据库的所有 key)。

此命令从不失败。

时间复杂度:
尚未明确
返回值:
总是返回 OK 。

FLUSHDB

FLUSHDB

清空当前数据库中的所有 key 。

此命令从不失败。

时间复杂度:
O(1)
返回值:
总是返回 OK 。

SHUTDOWN

SHUTDOWN

SHUTDOWN 命令执行以下操作:

  • 停止所有客户端
  • 如果有最少一个保存点在等待,执行 SAVE 命令
  • 如果 AOF 选项被打开,更新 AOF 文件
  • 服务器关闭

如果持久化被打开的话, SHUTDOWN 命令会保证服务器正常关闭而丢失任何数据。

假如只是单纯地执行 SAVE 命令,然后再执行 QUIT 命令,则没有这一保证 —— 因为在执行 SAVE 之后、执行 QUIT 之前的这段时间中间,其他客户端可能正在和服务器进行通讯,这时如果执行 QUIT 就会造成数据丢失。

时间复杂度:
不明确
返回值:
执行失败时返回错误。
执行成功时不返回任何信息,服务器和客户端的连接断开,客户端自动退出。

SLOWLOG

SLOWLOG subcommand [argument]

什么是 SLOWLOG

Slow log 是 Redis 用来记录查询执行时间的日志系统。

查询执行时间指的是不包括像客户端响应(talking)、发送回复等 IO 操作,而单单是执行一个查询命令所耗费的时间。

另外,slow log 保存在内存里面,读写速度非常快,因此你可以放心地使用它,不必担心因为开启 slow log 而损害 Redis 的速度。

设置 SLOWLOG

Slow log 的行为由两个配置参数(configuration parameter)指定,可以通过改写 redis.conf 文件或者用 CONFIG GET 和 CONFIG SET 命令对它们动态地进行修改。

第一个选项是 slowlog-log-slower-then ,它决定要对执行时间大于多少微秒(microsecond,1秒 = 1,000,000 微秒)的查询进行记录。

比如执行以下命令将让 slow log 记录所有查询时间大于等于 100 微秒的查询:

CONFIG SET slowlog-log-slower-then 100 ,

而以下命令记录所有查询时间大于 1000 微秒的查询:

CONFIG SET slowlog-log-slower-then 1000 。

另一个选项是 slowlog-max-len ,它决定 slow log 最多能保存多少条日志, slow log 本身是一个 LIFO 队列,当队列大小超过 slowlog-max-len 时,最旧的一条日志将被删除,而最新的一条日志加入到 slow log ,以此类推。

以下命令让 slow log 最多保存 1000 条日志:

CONFIG SET slowlog-max-len 1000 。

使用 CONFIG GET 命令可以查询两个选项的当前值:

查看 slow log

要查看 slow log ,可以使用 SLOWLOG GET 或者 SLOWLOG GET number 命令,前者打印所有 slow log ,最大长度取决于 slowlog-max-len 选项的值,而 SLOWLOG GET number 则只打印指定数量的日志。

最新的日志会最先被打印:

日志的唯一 id 只有在 Redis 服务器重启的时候才会重置,这样可以避免对日志的重复处理(比如你可能会想在每次发现新的慢查询时发邮件通知你)。

查看当前日志的数量

使用命令 SLOWLOG LEN 可以查看当前日志的数量。

请注意这个值和 slower-max-len 的区别,它们一个是当前日志的数量,一个是允许记录的最大日志的数量。

清空日志

使用命令 SLOWLOG RESET 可以清空 slow log 。

时间复杂度:O(1)

返回值:取决于不同命令,返回不同的值。

INFO

INFO

返回关于 Redis 服务器的各种信息和统计值。

时间复杂度:
O(1)
返回值:
具体请参见下面的测试代码。

CONFIG GET

CONFIG GET parameter

CONFIG GET 命令用于取得运行中的 Redis 服务器的配置参数(configuration parameters),不过并非所有配置参数都被 CONFIG GET 命令所支持。

CONFIG GET 接受单个参数 parameter 作为搜索关键字,查找所有匹配的配置参数,其中参数和值以“键-值对”(key-value pairs)的方式排列。

比如执行 CONFIG GET s* 命令,服务器就会返回所有以 s 开头的配置参数及参数的值:

如果你只是寻找特定的某个参数的话,你当然也可以直接指定参数的名字:

使用命令 CONFIG GET * ,可以列出 CONFIG GET 命令支持的所有参数:

所有被 CONFIG SET 所支持的配置参数都可以在配置文件 redis.conf 中找到,不过 CONFIG GET 和 CONFIG SET 使用的格式和 redis.conf 文件所使用的格式有以下两点不同:

  • 10kb 、 2gb 这些在配置文件中所使用的储存单位缩写,不可以用在 CONFIG 命令中, CONFIG SET 的值只能通过数字值显式地设定。
    像 CONFIG SET xxx 1k 这样的命令是错误的,正确的格式是 CONFIG SET xxx 1000 。
  • save 选项在 redis.conf 中是用多行文字储存的,但在 CONFIG GET 命令中,它只打印一行文字。
    以下是 save 选项在 redis.conf 文件中的表示:
    save 900 1
    save 300 10
    save 60 10000
    但是 CONFIG GET 命令的输出只有一行:
    redis> CONFIG GET save
    1) "save"
    2) "900 1 300 10 60 10000"
    上面 save 参数的三个值表示:在 900 秒内最少有 1 个 key 被改动,或者 300 秒内最少有 10 个 key 被改动,又或者 60 秒内最少有 1000 个 key 被改动,以上三个条件随便满足一个,就触发一次保存操作。
时间复杂度:
不明确
返回值:
给定配置参数的值。

CONFIG SET

CONFIG SET parameter value

CONFIG SET 命令可以动态地调整 Redis 服务器的配置(configuration)而无须重启。

你可以使用它修改配置参数,或者改变 Redis 的持久化(Persistence)方式。

CONFIG SET 可以修改的配置参数可以使用命令 CONFIG GET * 来列出,所有被 CONFIG SET 修改的配置参数都会立即生效。

关于 CONFIG SET 命令的更多消息,请参见命令 CONFIG GET 的说明。

关于如何使用 CONFIG SET 命令修改 Redis 持久化方式,请参见 Redis Persistence 。

时间复杂度:
不明确
返回值:
当设置成功时返回 OK ,否则返回一个错误。

CONFIG RESETSTAT

CONFIG RESETSTAT

重置 INFO 命令中的某些统计数据,包括:

  • Keyspace hits (键空间命中次数)
  • Keyspace misses (键空间不命中次数)
  • Number of commands processed (执行命令的次数)
  • Number of connections received (连接服务器的次数)
  • Number of expired keys (过期key的数量)
时间复杂度:
O(1)
返回值:
总是返回 OK 。

DEBUG OBJECT

DEBUG OBJECT key

返回给定 key 的调试信息。

时间复杂度:
O(1)
返回值:
当 key 存在时,返回有关信息。
当 key 不存在时,返回一个错误。

DEBUG SEGFAULT

DEBUG SEGFAULT

令 Redis 服务器崩溃,调试用。

时间复杂度:
不明确
返回值:

MONITOR

MONITOR

实时打印出 Redis 服务器接收到的命令,调试用。

时间复杂度:
不明确
返回值:
总是返回 OK 。

SYNC

YNC

用于复制功能(replication)的内部命令。

时间复杂度:
不明确
返回值:
不明确

(结束)

140822 redis哈希类型 的 hset & hmset[转载]

admin阅读(541)

redis 的哈希类型是每一个 key 都对应一个 HashTable.比较适合存储对象或者数组。

hset

描述:将哈希表key中的域field的值设为value。如果key不存在,一个新的哈希表被创建并进行HSET操作。如果域field已经存在于哈希表中,旧值将被覆盖。
参数:key field value
返回值:如果field是哈希表中的一个新建域,并且值设置成功,返回1。如果哈希表中域field已经存在且旧值已被新值覆盖,返回0。

hmset

描述:同时将多个field – value(域-值)对设置到哈希表key中。此命令会覆盖哈希表中已存在的域。如果key不存在,一个空哈希表被创建并执行HMSET操作。
参数:key field value [field value …]
返回值:如果命令执行成功,返回OK。当key不是哈希表(hash)类型时,返回一个错误。

hmget

描述:返回哈希表key中,一个或多个给定域的值。如果给定的域不存在于哈希表,那么返回一个nil值。因为不存在的key被当作一个空哈希表来处理,所以对一个不存在的key进行HMGET操作将返回一个只带有nil值的表。
参数:key field [field …]
返回值:一个包含多个给定域的关联值的表,表值的排列顺序和给定域参数的请求顺序一样。

hgetall

描述:返回哈希表key中,所有的域和值。在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。
参数:key
返回值:以列表形式返回哈希表的域和域的值。 若key不存在,返回空列表。

用于存储用户信息的:

<?php

$redis = new Redis();

$redis -> connect(‘127.0.0.1’, 6379);

$redis -> set(‘my_php_redis’, ‘install phpredis’);

echo $redis -> get(‘my_php_redis’);

$redis -> hset(‘zhlinfo’, ‘name’, ‘zhl’);

$redis -> hset(‘zhlinfo’, ‘age’, 26);

$redis -> hset(‘zhlinfo’, ‘address’, ‘China Beijing’);

var_dump($redis -> hgetall(‘zhlinfo’));

echo $redis -> hget(‘zhlinfo’, ‘name’);

echo $redis -> hget(‘zhlinfo’, ‘age’);

echo $redis -> hget(‘zhlinfo’, ‘address’);

$arr = array(‘name’ => ‘cjq’, ‘age’=> ’23’, ‘address’ => ‘China Beijing’, ‘hobby’=>’travelling’);

$redis -> hmset(‘cjqinfo’, $arr);

var_dump($redis->hmget(‘cjqinfo’));

echo $redis->hget(‘cjqinfo’,’hobby’);

$redis -> setTimeout(‘cjqinfo’, 5); //设置过期时间

sleep(5);

echo 5;

var_dump($redis->hmget(‘cjqinfo’));

出处http://dreameng.blog.51cto.com/1187899/1335461

140820 discuz@功能的代码[转载]

admin阅读(554)

        $atlist = $atlist_tmp = $ateduids = array();
        preg_match_all("/@([^\r\n]*?)\s/i", $message.' ', $atlist_tmp);//解析内容里的被@到的名字。
        $atlist_tmp = array_slice(array_unique($atlist_tmp[1]), 0, $_G['group']['allowat']);//应该是提取用户名吧。。
        $atnum = $maxselect = 0;
        foreach(C::t('home_notification')->fetch_all_by_authorid_fromid($_G['uid'], $_G['tid'], 'at') as $row) {
            $atnum ++;
            $ateduids[$row[uid]] = $row['uid'];
        }
        $maxselect = $_G['group']['allowat'] - $atnum;
        if($maxselect > 0 && !empty($atlist_tmp)) {
        if(empty($_G['setting']['at_anyone'])) {//判断是否只能@好友,如果只能@好友的话,那就从下面的home_follow表中找出在好友列表里的人的姓名。其他的就@不到了。
                foreach(C::t('home_follow')->fetch_all_by_uid_fusername($_G['uid'], $atlist_tmp) as $row) {
                    if(!in_array($row['followuid'], $ateduids)) {
                        $atlist[$row[followuid]] = $row['fusername'];
                    }
                    if(count($atlist) == $maxselect) {
                        break;
                    }
                }
                if(count($atlist) < $maxselect) {//判断@上限。
                    $query = C::t('home_friend')->fetch_all_by_uid_username($_G['uid'], $atlist_tmp);//不用说了吧
                    foreach($query as $row) {
                        if(!in_array($row['followuid'], $ateduids)) {
                            $atlist[$row[fuid]] = $row['fusername'];
                        }
                    }
                }
            } else {
                foreach(C::t('common_member')->fetch_all_by_username($atlist_tmp) as $row) {
                    if(in_array($row['uid'], $ateduids)) {
                        $atlist[$row[uid]] = $row['username'];
                    }
                    if(count($atlist) == $maxselect) {
                        break;
                    }
                }
            }
        }
        if($atlist) {//提醒和加链接。
            foreach($atlist as $atuid => $atusername) {
                $notic = array('subject' => '铂金小猪我爱你', 'message' => '铂金小猪你好,红薯在评论回复中说他爱你', 'from_id' => 0, 'from_idtype' => 'sendnotice');
                notification_add($atuid,'system','system_notice',$notic,1);//发送提醒给被@到的用户。$notic是提醒的内容。
                $atsearch[] = "/@$atusername /i";
                $atreplace[] = "[url=home.php?mod=space&uid=$atuid]@{$atusername}[/url] ";//加链接,但不是a标签。
            }
            $message = preg_replace($atsearch, $atreplace, $message.' ', 1);
        }
require_once libfile('function/followcode');
require_once libfile('function/discuzcode');
$cont=followcode($message);//格式化代码中的标签

140818 Discuz论坛标题字数突破80的限制

admin阅读(545)

这个帖子Discuz! X2的时候写过,升级Discuz! X2.5之后那个帖子基础上再做了下修改。

当一些用户发布帖子的时候 标题要是超过了80个字符超出的部分被剪切掉了,特别是一些用户发送一些英文或其他其语言的文章的时候标题说甚至会超过180个字符,又特别论坛编码是UTF-8格式,因为一个字占3个字节,所以标题最长也就26个汉字,很多用户想修改这个80个字符的限制。

想去掉这个字数限制,要从下面五个部分来修改:

一、数据库修改;

二、修改JS验证字符数文件;

三、修改模板中写死的字符限制数;

四,修改函数验证文件;

五,修改语言包文件。

现以把标题字符限制80修改为120为例子,描述一下修改方法:

一、数据库修改,修改数据库标题字段的长度为120字符:运行下面的sql语句:

(注意修改你的表的前缀)

ALTER TABLE `pre_forum_post` CHANGE `subject` `subject` VARCHAR(120) NOT NULL;
ALTER TABLE `pre_forum_rsscache` CHANGE `subject` `subject` char(120) NOT NULL;
ALTER TABLE `pre_forum_thread` CHANGE `subject` `subject` char(120) NOT NULL;

 

二、修改JS验证字符数:1、找到文件static/js/forum_post.js的74-80行

if(($('postsubmit').name != 'replysubmit' && !($('postsubmit').name == 'editsubmit' && !isfirstpost) && theform.subject.value == "") || !sortid && !special && trim(message) == "") {
showError('抱歉,您尚未输入标题或内容');
return false;
} else if(mb_strlen(theform.subject.value) > 80) {
showError('您的标题超过 80 个字符的限制');
return false;
}

 

修改为:

if(($('postsubmit').name != 'replysubmit' && !($('postsubmit').name == 'editsubmit' && !isfirstpost) && theform.subject.value == "") || !sortid && !special && trim(message) == "") {
showError('抱歉,您尚未输入标题或内容');
return false;
} else if(mb_strlen(theform.subject.value) > 120) {
showError('您的标题超过 120 个字符的限制');
return false;
}

 

2、找到文件sitatic/js/forum.js的209到215行代码:

if(theform.message.value == '' && theform.subject.value == '') {
s = '抱歉,您尚未输入标题或内容';
theform.message.focus();
} else if(mb_strlen(theform.subject.value) > 80) {
s = '您的标题超过 80 个字符的限制';
theform.subject.focus();
}

 

修改为:

if(theform.message.value == '' && theform.subject.value == '') {
s = '抱歉,您尚未输入标题或内容';
theform.message.focus();
} else if(mb_strlen(theform.subject.value) > 120) {
s = '您的标题超过 120 个字符的限制';
theform.subject.focus();
}

 

三、修改模板中写死的字符限制数:

1、找到文件\template\default\forum\post_editor_extra.htm的25到31行:

<!--{if $_G[gp_action] != 'reply'}-->
<span><input type="text" name="subject" id="subject" class="px" value="$postinfo[subject]" {if $_G[gp_action] == 'newthread'}onblur="if($('tags')){relatekw('-1','-1'{if $_G['group']['allowposttag']},function(){extraCheck(4)}{/if});doane();}"{/if} tabindex="1" /></span>
<!--{else}-->
<span id="subjecthide" class="z">RE: $thread[subject] [<a href="javascript:;">{lang modify}</a>]</span>
<span id="subjectbox"><input type="text" name="subject" id="subject" class="px" value="" /></span>
<!--{/if}-->
<span id="subjectchk"{if $_G[gp_action] == 'reply'}{/if}>{lang comment_message1} <strong id="checklen">80</strong> {lang comment_message2}</span>

修改为下面代码:

<!--{if $_G[gp_action] != 'reply'}-->
<span><input type="text" name="subject" id="subject" class="px" value="$postinfo[subject]" {if $_G[gp_action] == 'newthread'}onblur="if($('tags')){relatekw('-1','-1'{if $_G['group']['allowposttag']},function(){extraCheck(4)}{/if});doane();}"{/if} tabindex="1" /></span>
<!--{else}-->
<span id="subjecthide" class="z">RE: $thread[subject] [<a href="javascript:;">{lang modify}</a>]</span>
<span id="subjectbox"><input type="text" name="subject" id="subject" class="px" value="" /></span>
<!--{/if}-->
<span id="subjectchk"{if $_G[gp_action] == 'reply'}{/if}>{lang comment_message1} <strong id="checklen">120</strong> {lang comment_message2}</span>

 

2、找到文件\template\default\forum\forumdisplay_fastpost.htm31-32行:

<input type="text" id="subject" name="subject" class="px" value="" tabindex="11" />
<span>{lang comment_message1} <strong id="checklen">80</strong> {lang comment_message2}</span>

 

修改为:

<input type="text" id="subject" name="subject" class="px" value="" tabindex="11" />
<span>{lang comment_message1} <strong id="checklen">120</strong> {lang comment_message2}</span>

 

四,修改函数验证提示:

找到文件source/function/function_post.php的346-348行:

if(dstrlen($subject) > 80) {
return 'post_subject_toolong';
}

 

修改为:

if(dstrlen($subject) > 120) {
return 'post_subject_toolong';
}

 

五、找到语言包提示文字,打开 source/language/lang_messege.php 并找到985行改为:

  'post_subject_toolong' => '抱歉,您的标题超过 120 个字符修改标题长度',

 

OK,你再发表帖子标题就可以是120个字符数了!!!

 

140815 discuz密码输入错误过多被锁定解决

admin阅读(556)

discuz解决用户被锁,ip被封的数据库修改

用户登录次数过多被锁修改表”pre_common_failedlogin”
用户ip被封 修改表“pre_common_banned”

管理员后台登陆次数过多“你的管理面板已经锁定”  删除这个表的记录pre_common_admincp_session

140808 PHP闭包(Closure)[转载]

admin阅读(372)

不知不觉发现PHP已经出到了5.5版本,而自己一直在用PHP5.2,让我看起来像深山出来的小伙子一样,又土又落后。在我习惯在javascript中使用闭包之后,忽然间对PHP的闭包打起了兴趣。

于是乎在网上下了个WAMP集成开发环境,是PHP5.3版本的(PHP5.3开始引入了闭包的特性),不得不说WAMP安装使用真的很方便。简单配置了一下,开始动手。

 

匿名函数

提到闭包就不得不想起匿名函数,也叫闭包函数(closures),貌似PHP闭包实现主要就是靠它。声明一个匿名函数是这样:

1
2
3
$func = function() {
    
}; //带结束符

可以看到,匿名函数因为没有名字,如果要使用它,需要将其返回给一个变量。匿名函数也像普通函数一样可以声明参数,调用方法也相同:

1
2
3
4
5
6
7
8
$func = function( $param ) {
    echo $param;
};
$func( 'some string' );
//输出:
//some string

顺便提一下,PHP在引入闭包之前,也有一个可以创建匿名函数的函数:create function,但是代码逻辑只能写成字符串,这样看起来很晦涩并且不好维护,所以很少有人用。

 

实现闭包

将匿名函数在普通函数中当做参数传入,也可以被返回。这就实现了一个简单的闭包。

下边有三个例子

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
//例一
//在函数里定义一个匿名函数,并且调用它
function printStr() {
    $func = function( $str ) {
        echo $str;
    };
    $func( 'some string' );
}
printStr();
//例二
//在函数中把匿名函数返回,并且调用它
function getPrintStrFunc() {
    $func = function( $str ) {
        echo $str;
    };
    return $func;
}
$printStrFunc = getPrintStrFunc();
$printStrFunc( 'some string' );
//例三
//把匿名函数当做参数传递,并且调用它
function callFunc( $func ) {
    $func( 'some string' );
}
$printStrFunc = function( $str ) {
    echo $str;
};
callFunc( $printStrFunc );
//也可以直接将匿名函数进行传递。如果你了解js,这种写法可能会很熟悉
callFunc( function( $str ) {
    echo $str;
} );

 

连接闭包和外界变量的关键字:USE

闭包可以保存所在代码块上下文的一些变量和值。PHP在默认情况下,匿名函数不能调用所在代码块的上下文变量,而需要通过使用use关键字。

换一个例子看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getMoney() {
    $rmb = 1;
    $dollar = 6;
    $func = function() use ( $rmb ) {
        echo $rmb;
        echo $dollar;
    };
    $func();
}
getMoney();
//输出:
//1
//报错,找不到dorllar变量

可以看到,dollar没有在use关键字中声明,在这个匿名函数里也就不能获取到它,所以开发中要注意这个问题。

有人可能会想到,是否可以在匿名函数中改变上下文的变量,但我发现是不可以的:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getMoney() {
    $rmb = 1;
    $func = function() use ( $rmb ) {
        echo $rmb;
        //把$rmb的值加1
        $rmb++;
    };
    $func();
    echo $rmb;
}
getMoney();
//输出:
//1
//1

啊,原来use所引用的也只不过是变量的一个副本而已。但是我想要完全引用变量,而不是复制。

要达到这种效果,其实在变量前加一个 & 符号就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getMoney() {
    $rmb = 1;
    $func = function() use ( &$rmb ) {
        echo $rmb;
        //把$rmb的值加1
        $rmb++;
    };
    $func();
    echo $rmb;
}
getMoney();
//输出:
//1
//2

好,这样匿名函数就可以引用上下文的变量了。如果将匿名函数返回给外界,匿名函数会保存use所引用的变量,而外界则不能得到这些变量,这样形成‘闭包’这个概念可能会更清晰一些。

根据描述改变一下上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function getMoneyFunc() {
    $rmb = 1;
    $func = function() use ( &$rmb ) {
        echo $rmb;
        //把$rmb的值加1
        $rmb++;
    };
    return $func;
}
$getMoney = getMoneyFunc();
$getMoney();
$getMoney();
$getMoney();
//输出:
//1
//2
//3

 

总结

PHP闭包的特性并没有太大惊喜,其实用CLASS就可以实现类似甚至强大得多的功能,更不能和js的闭包相提并论,只能期待PHP以后对闭包支持的改进。不过匿名函数还是挺有用的,比如在使用preg_replace_callback等之类的函数可以不用在外部声明回调函数了。

140808 PHP命名空间(Namespace)[转载]

admin阅读(431)

对于命名空间,官方文档已经说得很详细[查看],我在这里做了一下实践和总结。

命名空间一个最明确的目的就是解决重名问题,PHP中不允许两个函数或者类出现相同的名字,否则会产生一个致命的错误。这种情况下只要避免命名重复就可以解决,最常见的一种做法是约定一个前缀。

例:项目中有两个模块:articlemessage board,它们各自有一个处理用户留言的类Comment。之后我可能想要增加对所有用户留言的一些信息统计功能,比如说我想得到所有留言的数量。这时候调用它们Comment提供的方法是很好的做法,但是同时引入各自的Comment类显然是不行的,代码会出错,在另一个地方重写任何一个Comment也会降低维护性。那这时只能重构类名,我约定了一个命名规则,在类名前面加上模块名,像这样:Article_CommentMessageBoard_Comment

可以看到,名字变得很长,那意味着以后使用Comment的时候会写上更多的代码(至少字符多了)。并且,以后如果要对各个模块增加更多的一些整合功能,或者是互相调用,发生重名的时候就需要重构名字。当然在项目开始的时候就注意到这个问题,并规定命名规则就能很好的避免这个问题。另一个解决方法可以考虑使用命名空间。

 

注明:

本文提到的常量:PHP5.3开始const关键字可以用在类的外部。constdefine都是用来声明常量的(它们的区别不详述),但是在命名空间里,define的作用是全局的,而const则作用于当前空间。我在文中提到的常量是指使用const声明的常量。

 

 

基础

命名空间将代码划分出不同的空间(区域),每个空间的常量、函数、类(为了偷懒,我下边都将它们称为元素)的名字互不影响, 这个有点类似我们常常提到的‘封装’的概念。

创建一个命名空间需要使用namespace关键字,这样:

<?php

//创建一个名为'Article'的命名空间
namespace Article;

?>

 

要注意的是,当前脚本文件的第一个命名空间前面不能有任何代码,下面的写法都是错误的:

//例一
//在脚本前面写了一些逻辑代码

<?php

$path = "/";

class Comment { }

namespace Article;

?>



//例二
//在脚本前面输出了一些字符

<html></html>
<?php

namespace Article;

?>

 

为什么要说第一个命名空间呢?因为同一脚本文件中可以创建多个命名空间。

下面我创建了两个命名空间,顺便为这两个空间各自添加了一个Comment类元素:

<?php

//创建一个名为'Article'的命名空间
namespace Article;

//此Comment属于Article空间的元素
class Comment { }



//创建一个名为'MessageBoard'的命名空间
namespace MessageBoard;

//此Comment属于MessageBoard空间的元素
class Comment { }
?>

 

在不同空间之间不可以直接调用其它元素,需要使用命名空间的语法:

<?php

namespace Article;

class Comment { }



namespace MessageBoard;

class Comment { }

//调用当前空间(MessageBoard)的Comment类
$comment = new Comment();

//调用Article空间的Comment类
$article_comment = new \Article\Comment();

?>

 

可以看到,在MessageBoard空间中调用article空间里的Comment类时,使用了一种像文件路径的语法: \空间名\元素名

除了类之外,对函数和常量的用法是一样的,下面我为两个空间创建了新的元素,并在MessageBoard空间中输出了它们的值。

<?php

namespace Article;

const PATH = '/article';

function getCommentTotal() {
    return 100;
}

class Comment { }




namespace MessageBoard;

const PATH = '/message_board';

function getCommentTotal() {
    return 300;
}

class Comment { }

//调用当前空间的常量、函数和类
echo PATH; ///message_board
echo getCommentTotal(); //300
$comment = new Comment();

//调用Article空间的常量、函数和类
echo \Article\PATH; ///article
echo \Article\getCommentTotal(); //100
$article_comment = new \Article\Comment();

?>

 

然后我的确得到了Article空间的元素数据。

 

 

子空间

命名空间的调用语法像文件路径一样是有道理的,它允许我们自定义子空间来描述各个空间之间的关系。

抱歉我忘了说,articlemessage board这两个模块其实都是处于同一个blog项目内。如果用命名空间来表达它们的关系,是这样:

 

<?php

//我用这样的命名空间表示处于blog下的article模块
namespace Blog\Article;

class Comment { }



//我用这样的命名空间表示处于blog下的message board模块
namespace Blog\MessageBoard;

class Comment { }

//调用当前空间的类
$comment = new Comment();

//调用Blog\Article空间的类
$article_comment = new \Blog\Article\Comment();

?>

 

而且,子空间还可以定义很多层次,比如说 Blog\Article\Archives\Date

 

 

公共空间

我有一个common_inc.php脚本文件,里面有一些好用的函数和类:

<?php

function getIP() { }

class FilterXSS { }

?>

 

在一个命名空间里引入这个脚本,脚本里的元素不会归属到这个命名空间。如果这个脚本里没有定义其它命名空间,它的元素就始终处于公共空间中:

<?php

namespace Blog\Article;

//引入脚本文件
include './common_inc.php';

$filter_XSS = new FilterXSS(); //出现致命错误:找不到Blog\Article\FilterXSS类

$filter_XSS = new \FilterXSS(); //正确


?>

 

调用公共空间的方式是直接在元素名称前加 \ 就可以了,否则PHP解析器会认为我想调用当前空间下的元素。除了自定义的元素,还包括PHP自带的元素,都属于公共空间。

要提一下,其实公共空间的函数和常量不用加 \ 也可以正常调用(不明白PHP为什么要这样做),但是为了正确区分元素,还是建议调用函数的时候加上 \

 

 

名称术语

在说别名和导入之前,需要知道关于空间三种名称的术语,以及PHP是怎样解析它们的。官方文档说得非常好,我就直接拿来套了。

  1. 非限定名称,或不包含前缀的类名称,例如 $comment = new Comment();。如果当前命名空间是Blog\ArticleComment将被解析为Blog\Article\Comment。如果使用Comment的代码不包含在任何命名空间中的代码(全局空间中),则Comment会被解析为Comment
  2. 限定名称,或包含前缀的名称,例如 $comment = new Article\Comment();。如果当前的命名空间是Blog,则Comment会被解析为Blog\Article\Comment。如果使用Comment的代码不包含在任何命名空间中的代码(全局空间中),则Comment会被解析为Comment
  3. 完全限定名称,或包含了全局前缀操作符的名称,例如 $comment = new \Article\Comment();。在这种情况下,Comment总是被解析为代码中的文字名(literal name)Article\Comment

 

其实可以把这三种名称类比为文件名(例如 comment.php)、相对路径名(例如 ./article/comment.php)、绝对路径名(例如 /blog/article/comment.php),这样可能会更容易理解。

我用了几个示例来表示它们:

<?php

//创建空间Blog
namespace Blog;

class Comment { }

//非限定名称,表示当前Blog空间
//这个调用将被解析成 Blog\Comment();
$blog_comment = new Comment();

//限定名称,表示相对于Blog空间
//这个调用将被解析成 Blog\Article\Comment();
$article_comment = new Article\Comment(); //类前面没有反斜杆\

//完全限定名称,表示绝对于Blog空间
//这个调用将被解析成 Blog\Comment();
$article_comment = new \Blog\Comment(); //类前面有反斜杆\

//完全限定名称,表示绝对于Blog空间
//这个调用将被解析成 Blog\Article\Comment();
$article_comment = new \Blog\Article\Comment(); //类前面有反斜杆\



//创建Blog的子空间Article
namespace Blog\Article;

class Comment { }


?>

 

其实之前我就一直在使用非限定名称和完全限定名称,现在它们终于可以叫出它们的名称了。

 

 

别名和导入

别名和导入可以看作是调用命名空间元素的一种快捷方式。PHP并不支持导入函数或常量。

它们都是通过使用use操作符来实现:

<?php

namespace Blog\Article;

class Comment { }



//创建一个BBS空间(我有打算开个论坛)
namespace BBS;

//导入一个命名空间
use Blog\Article;
//导入命名空间后可使用限定名称调用元素
$article_comment = new Article\Comment();

//为命名空间使用别名
use Blog\Article as Arte;
//使用别名代替空间名
$article_comment = new Arte\Comment();

//导入一个类
use Blog\Article\Comment;
//导入类后可使用非限定名称调用元素
$article_comment = new Comment();

//为类使用别名
use Blog\Article\Comment as Comt;
//使用别名代替空间名
$article_comment = new Comt();

?>

 

我注意到,如果导入元素的时候,当前空间有相同的名字元素将会怎样?显然结果会发生致命错误。

例:

<?php

namespace Blog\Article;

class Comment { }



namespace BBS;

class Comment { }

Class Comt { }


//导入一个类
use Blog\Article\Comment;
$article_comment = new Comment(); //与当前空间的Comment发生冲突,程序产生致命错误

//为类使用别名
use Blog\Article\Comment as Comt;
$article_comment = new Comt(); //与当前空间的Comt发生冲突,程序产生致命错误

?>

 

 

动态调用

PHP提供了namespace关键字和__NAMESPACE__魔法常量动态的访问元素,__NAMESPACE__可以通过组合字符串的形式来动态访问:

<?php

namespace Blog\Article;

const PATH = '/Blog/article';

class Comment { }


//namespace关键字表示当前空间
echo namespace\PATH; ///Blog/article
$comment = new namespace\Comment();

//魔法常量__NAMESPACE__的值是当前空间名称
echo __NAMESPACE__; //Blog\Article
//可以组合成字符串并调用
$comment_class_name = __NAMESPACE__ . '\Comment';
$comment = new $comment_class_name();

?>

 

字符串形式调用问题

上面的动态调用的例子中,我们看到了字符串形式的动态调用方式,如果要使用这种方式要注意两个问题。

1. 使用双引号的时候特殊字符可能被转义

<?php

namespace Blog\Article;

class name { }

//我是想调用Blog\Article\name
$class_name = __NAMESPACE__ . "\name"; //但是\n将被转义为换行符

$name = new $class_name(); //发生致命错误

?>

 

2. 不会认为是限定名称

PHP在编译脚本的时候就确定了元素所在的空间,以及导入的情况。而在解析脚本时字符串形式调用只能认为是非限定名称和完全限定名称,而永远不可能是限定名称。

<?php

namespace Blog;

//导入Common类
use Blog\Article\Common;
//我想使用非限定名称调用Blog\Article\Common
$common_class_name = 'Common';
//实际会被当作非限定名称,也就表示当前空间的Common类,但我当前类没有创建Common类
$common = new $common_class_name(); //发生致命错误:Common类不存在

//我想使用限定名称调用Blog\Article\Common
$common_class_name = 'Article\Common';
//实际会被当作完全限定名称,也就表示Article空间下的Common类,但我下面只定义了Blog\Article空间而不是Article空间
$common = new $common_class_name(); //发生致命错误:Article\Common类不存在


namespace Blog\Article;

class Common { }

?>

 

 

总结

我对PHP的命名空间刚刚接触,也不能随便给一些没有实践的建议。我个人认为命名空间的作用和功能都很强大,如果要写插件或者通用库的时候再也不用担心重名问题。不过如果项目进行到一定程度,要通过增加命名空间去解决重名问题,我觉得工作量不会比重构名字少。也不得不承认它的语法会对项目增加一定的复杂度,因此从项目一开始的时候就应该很好的规划它,并制定一个命名规范。

 

 

140801 wordpress自定义上传路径和生成文件地址[转载]

admin阅读(513)

WordPress 3.5 一个最大的改变就是更加简洁化,把一些用户不常使用的设置去掉或者隐藏了,比如后台的媒体(Media)设置页面隐藏上传路径(upload_path)和文件 URL 地址(upload_url_path)的设定就被隐藏了。如果你还要进行设置这两个选项,现在只能在 options.php 中进行设置,或者使用 UPLOADS 常量,也可以使用 upload_dir 这个 filter。下面分别介绍下这三种方法:

1. options.php 中设置

直接访问:http://site_url/wp-admin/options.php,然后找到下面选项,将其设置为你所需的参数。

2. 定义 UPLOADS 常量

wp-config.php 文件中定义 UPLOADS 这个常量:

define('UPLOADS','my-uploads');

这个方法有个不好的地方:只能指定上传的相对目录,生成文件的 URL 地址不能使用子域名,这样在做静态文件 CDN 加速的时候就不是很方便。

3. 使用 upload_dir filter

这个方法,效果其实和第一个方法基本一致:

add_filter( 'upload_dir', 'wpjam_custom_upload_dir' );
function wpjam_custom_upload_dir( $uploads ) {
	$upload_path = '';
	$upload_url_path = '';

	if ( empty( $upload_path ) || 'wp-content/uploads' == $upload_path ) {
		$uploads['basedir']  = WP_CONTENT_DIR . '/uploads';
	} elseif ( 0 !== strpos( $upload_path, ABSPATH ) ) {
		$uploads['basedir'] = path_join( ABSPATH, $upload_path );
	} else {
		$uploads['basedir'] = $upload_path;
	}

	$uploads['path'] = $uploads['basedir'].$uploads['subdir'];

	if ( $upload_url_path ) {
		$uploads['baseurl'] = $upload_url_path;
		$uploads['url'] = $uploads['baseurl'].$uploads['subdir'];
	}
	return $uploads;
}

将上面代码中的 $upload_path$upload_url_path 改成你要的值,然后上传到当前主题的 fucntions.php 文件即可。

另外这三种方法的优先级是:使用 upload_dir filter > 定义 UPLOADS 常量 > options.php 中设置

140801 RESTful API 设计最佳实践[转载]

admin阅读(486)

背景

目前互联网上充斥着大量的关于RESTful API(为方便,下文中“RESTful API ”简写为“API”)如何设计的文章,然而却没有一个”万能“的设计标准:如何鉴权?API 格式如何?你的API是否应该加入版本信息?当你开始写一个app的时候,特别是后端模型部分已经写完的时候,你不得不殚精竭虑的设计和实现自己app的public API部分。因为一旦发布,对外发布的API将会很难改变。

在给SupportedFu设计API的时候,我试图以实用的角度来解决上面提到的问题。我希望可以设计出容易使用,容易部署,并且足够灵活的API,本文因此而生。

API设计的基本要求

网上的很多关于API设计的观点都十分”学院派“,它们也许更有理论基础,但是有时却和现实世界脱轨(因此我是自由派)。所以我这篇文章的目标是从实践的角度出发,给出当前网络应用的API设计最佳实践(当然,是我认为的最佳了~),如果觉得不合适,我不会遵从标准。当然作为设计的基础,几个必须的原则还是要遵守的:

  1. 当标准合理的时候遵守标准。
  2. API应该对程序员友好,并且在浏览器地址栏容易输入。
  3. API应该简单,直观,容易使用的同时优雅。
  4. API应该具有足够的灵活性来支持上层ui。
  5. API设计权衡上述几个原则。

需要强调的是:API的就是程序员的UI,和其他UI一样,你必须仔细考虑它的用户体验!

使用RESTful URLs 和action.

虽然前面我说没有一个万能的API设计标准。但确实有一个被普遍承认和遵守:RESTfu设计原则。它被Roy Felding提出(在他的”基于网络的软件架构“论文中第五章)。而REST的核心原则是将你的API拆分为逻辑上的资源。这些资源通过http被操作(GET ,POST,PUT,DELETE)。

那么我应该如何拆分出这些资源呢?

显然从API用户的角度来看,”资源“应该是个名词。即使你的内部数据模型和资源已经有了很好的对应,API设计的时候你仍然不需要把它们一对一的都暴露出来。这里的关键是隐藏内部资源,暴露必需的外部资源。

在SupportFu里,资源是 ticket、user、group。

一旦定义好了要暴露的资源,你可以定义资源上允许的操作,以及这些操作和你的API的对应关系:

  • GET /tickets # 获取ticket列表
  • GET /tickets/12 # 查看某个具体的ticket
  • POST /tickets # 新建一个ticket
  • PUT /tickets/12 # 更新ticket 12.
  • DELETE /tickets/12 #删除ticekt 12

可以看出使用REST的好处在于可以充分利用http的强大实现对资源的CURD功能。而这里你只需要一个endpoint:/tickets,再没有其他什么命名规则和url规则了,cool!

这个endpoint的单数复数

一个可以遵从的规则是:虽然看起来使用复数来描述某一个资源实例看起来别扭,但是统一所有的endpoint,使用复数使得你的URL更加规整。这让API使用者更加容易理解,对开发者来说也更容易实现。

如何处理关联?关于如何处理资源之间的管理REST原则也有相关的描述:

  • GET /tickets/12/messages- Retrieves list of messages for ticket #12
  • GET /tickets/12/messages/5- Retrieves message #5 for ticket #12
  • POST /tickets/12/messages- Creates a new message in ticket #12
  • PUT /tickets/12/messages/5- Updates message #5 for ticket #12
  • PATCH /tickets/12/messages/5- Partially updates message #5 for ticket #12
  • DELETE /tickets/12/messages/5- Deletes message #5 for ticket #12

其中,如果这种关联和资源独立,那么我们可以在资源的输出表示中保存相应资源的endpoint。然后API的使用者就可以通过点击链接找到相关的资源。如果关联和资源联系紧密。资源的输出表示就应该直接保存相应资源信息。(例如这里如果message资源是独立存在的,那么上面 GET /tickets/12/messages就会返回相应message的链接;相反的如果message不独立存在,他和ticket依附存在,则上面的API调用返回直接返回message信息)

不符合CURD的操作

对这个令人困惑的问题,下面是一些解决方法:

  1. 重构你的行为action。当你的行为不需要参数的时候,你可以把active对应到activated这个资源,(更新使用patch).
  2. 以子资源对待。例如:github上,对一个gists加星操作:PUT /gists/:id/star 并且取消星操作:DELETE /gists/:id/star.
  3. 有时候action实在没有难以和某个资源对应上例如search。那就这么办吧。我认为API的使用者对于/search这种url也不会有太大意见的(毕竟他很容易理解)。只要注意在文档中写清楚就可以了。

永远使用SSL

毫无例外,永远都要使用SSL。你的应用不知道要被谁,以及什么情况访问。有些是安全的,有些不是。使用SSL可以减少鉴权的成本:你只需要一个简单的令牌(token)就可以鉴权了,而不是每次让用户对每次请求签名。

值得注意的是:不要让非SSL的url访问重定向到SSL的url。

文档

文档和API本身一样重要。文档应该容易找到,并且公开(把它们藏到pdf里面或者存到需要登录的地方都不太好)。文档应该有展示请求和输出的例子:或者以点击链接的方式或者通过curl的方式(请见openstack的文档)。如果有更新(特别是公开的API),应该及时更新文档。文档中应该有关于何时弃用某个API的时间表以及详情。使用邮件列表或者博客记录是好方法。

版本化

在API上加入版本信息可以有效的防止用户访问已经更新了的API,同时也能让不同主要版本之间平稳过渡。关于是否将版本信息放入url还是放入请求头有过争论:API version should be included in the URL or in a header. 学术界说它应该放到header里面去,但是如果放到url里面我们就可以跨版本的访问资源了。。(参考openstack)。

strip使用的方法就很好:它的url里面有主版本信息,同时请求头俩面有子版本信息。这样在子版本变化过程中url的稳定的。变化有时是不可避免的,关键是如何管理变化。完整的文档和合理的时间表都会使得API使用者使用的更加轻松。

结果过滤,排序,搜索:

url最好越简短越好,和结果过滤,排序,搜索相关的功能都应该通过参数实现(并且也很容易实现)。

过滤:为所有提供过滤功能的接口提供统一的参数。例如:你想限制get /tickets 的返回结果:只返回那些open状态的ticket–get /tickektsstate=open这里的state就是过滤参数。

排序:和过滤一样,一个好的排序参数应该能够描述排序规则,而不业务相关。复杂的排序规则应该通过组合实现:

  • GET /ticketssort=-priority- Retrieves a list of tickets in descending order of priority
  • GET /ticketssort=-priority,created_at- Retrieves a list of tickets in descending order of priority. Within a specific priority, older tickets are ordered first

这里第二条查询中,排序规则有多个rule以逗号间隔组合而成。

搜索:有些时候简单的排序是不够的。我们可以使用搜索技术(ElasticSearch和Lucene)来实现(依旧可以作为url的参数)。

  • GET /ticketsq=return&state=open&sort=-priority,created_at- Retrieve the highest priority open tickets mentioning the word ‘return’

对于经常使用的搜索查询,我们可以为他们设立别名,这样会让API更加优雅。例如:

get /ticketsq=recently_closed -> get /tickets/recently_closed.

限制API返回值的域

有时候API使用者不需要所有的结果,在进行横向限制的时候(例如值返回API结果的前十项)还应该可以进行纵向限制。并且这个功能能有效的提高网络带宽使用率和速度。可以使用fields查询参数来限制返回的域例如:

GET /ticketsfields=id,subject,customer_name,updated_at&state=open&sort=-updated_at

更新和创建操作应该返回资源

PUT、POST、PATCH 操作在对资源进行操作的时候常常有一些副作用:例如created_at,updated_at 时间戳。为了防止用户多次的API调用(为了进行此次的更新操作),我们应该会返回更新的资源(updated representation.)例如:在POST操作以后,返回201 created 状态码,并且包含一个指向新资源的url作为返回头

是否需要 “HATEOAS

网上关于是否允许用户创建新的url有很大的异议(注意不是创建资源产生的url)。为此REST制定了HATEOAS来描述了和endpoint进行交互的时候,行为应该在资源的metadata返回值里面进行定义。

(译注:作者这里认为HATEOAS还不算成熟,我也不怎么理解这段就算了,读者感兴趣可以自己去原文查看)

只提供json作为返回格式

现在开始比较一下XML和json了。XML即冗长,难以阅读,又不适合各种编程语言解析。当然XML有扩展性的优势,但是如果你只是将它来对内部资源串行化,那么他的扩展优势也发挥不出来。很多应用(youtube,twitter,box)都已经开始抛弃XML了,我也不想多费口舌。给了google上的趋势图吧:

当然如果的你使用用户里面企业用户居多,那么可能需要支持XML。如果是这样的话你还有另外一个问题:你的http请求中的media类型是应该和accept 头同步还是和url?为了方便(browser explorability),应该是在url中(用户只要自己拼url就好了)。如果这样的话最好的方法是使用.xml或者.json的后缀。

命名方式?

是蛇形命令(下划线和小写)还是驼峰命名?如果使用json那么最好的应该是遵守JAVASCRIPT的命名方法-也就是说骆驼命名法。如果你正在使用多种语言写一个库,那么最好按照那些语言所推荐的,java,c#使用骆驼,python,ruby使用snake。

个人意见:我总觉得蛇形命令更好使一些,当然这没有什么理论的依据。有人说蛇形命名读起来更快,能达到20%,也不知道真假http://ieeexplore.ieee.org/xpl/articleDetails.jsptp=&arnumber=5521745

默认使用pretty print格式,使用gzip

只是使用空格的返回结果从浏览器上看总是觉得很恶心(一大坨有没有?~)。当然你可以提供url上的参数来控制使用“pretty print”,但是默认开启这个选项还是更加友好。格外的传输上的损失不会太大。相反你如果忘了使用gzip那么传输效率将会大大减少,损失大大增加。想象一个用户正在debug那么默认的输出就是可读的-而不用将结果拷贝到其他什么软件中在格式化-是想起来就很爽的事,不是么?

下面是一个例子:

$ curl https://API.github.com/users/veesahni > with-whitespace.txt
$ ruby -r json -e 'puts JSON JSON.parse(STDIN.read)' < with-whitespace.txt > without-whitespace.txt
$ gzip -c with-whitespace.txt > with-whitespace.txt.gz
$ gzip -c without-whitespace.txt > without-whitespace.txt.gz

输出如下:

  • without-whitespace.txt- 1252 bytes
  • with-whitespace.txt- 1369 bytes
  • without-whitespace.txt.gz- 496 bytes
  • with-whitespace.txt.gz- 509 bytes

在上面的例子中,多余的空格使得结果大小多出了8.5%(没有使用gzip),相反只多出了2.6%。据说:twitter使用gzip之后它的streaming API传输减少了80%(link:https://dev.twitter.com/blog/announcing-gzip-compression-streaming-APIs).

只在需要的时候使用“envelope”

很多API象下面这样返回结果:

1
2
3
4
5
6
{
  "data" : {
    "id" : 123,
    "name" : "John"
  }
}

理由很简单:这样做可以很容易扩展返回结果,你可以加入一些分页信息,一些数据的元信息等-这对于那些不容易访问到返回头的API使用者来说确实有用,但是随着“标准”的发展(cors和http://tools.ietf.org/html/rfc5988#page-6都开始被加入到标准中了),我个人推荐不要那么做。

何时使用envelope?

有两种情况是应该使用envelope的。如果API使用者确实无法访问返回头,或者API需要支持交叉域请求(通过jsonp)。

jsonp请求在请求的url中包含了一个callback函数参数。如果给出了这个参数,那么API应该返回200,并且把真正的状态码放到返回值里面(包装在信封里),例如:

1
2
3
4
5
6
7
callback_function({
  status_code: 200,
  next_page: "https://..",
  response: {
    ... actual JSON response body ...
  }
})

同样为了支持无法方法返回头的API使用者,可以允许envelope=true这样的参数。

在post,put,patch上使用json作为输入

如果你认同我上面说的,那么你应该决定使用json作为所有的API输出格式,那么我们接下来考虑考虑API的输入数据格式。

很多的API使用url编码格式:就像是url查询参数的格式一样:单纯的键值对。这种方法简单有效,但是也有自己的问题:它没有数据类型的概念。这使得程序不得不根据字符串解析出布尔和整数,而且还没有层次结构–虽然有一些关于层次结构信息的约定存在可是和本身就支持层次结构的json比较一下还是不很好用。

当然如果API本身就很简单,那么使用url格式的输入没什么问题。但对于复杂的API你应该使用json。或者干脆统一使用json。

注意使用json传输的时候,要求请求头里面加入:Content-Type:application/json.,否则抛出415异常(unsupported media type)。

分页

分页数据可以放到“信封”里面,但随着标准的改进,现在我推荐将分页信息放到link header里面:http://tools.ietf.org/html/rfc5988#page-6。

使用link header的API应该返回一系列组合好了的url而不是让用户自己再去拼。这点在基于游标的分页中尤为重要。例如下面,来自github的文档

1
2
Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",
<https://api.github.com/user/repos?page=50&per_page=100>; rel="last"

自动加载相关的资源

很多时候,自动加载相关资源非常有用,可以很大的提高效率。但是这却和RESTful的原则相背。为了如此,我们可以在url中添加参数:embed(或者expend)。embed可以是一个逗号分隔的串,例如:

1
GET /ticket/12embed=customer.name,assigned_user

对应的API返回值如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
  "id" : 12,
  "subject" : "I have a question!",
  "summary" : "Hi, ....",
  "customer" : {
    "name" : "Bob"
  },
  assigned_user: {
   "id" : 42,
   "name" : "Jim",
  }
}

值得提醒的是,这个功能有时候会很复杂,并且可能导致N+1 SELECT 问题

重写HTTP方法

有的客户端只能发出简单的GET 和POST请求。为了照顾他们,我们可以重写HTTP请求。这里没有什么标准,但是一个普遍的方式是接受X-HTTP-Method-Override请求头。

速度限制

为了避免请求泛滥,给API设置速度限制很重要。为此 RFC 6585 引入了HTTP状态码429(too many requests)。加入速度设置之后,应该提示用户,至于如何提示标准上没有说明,不过流行的方法是使用HTTP的返回头。

下面是几个必须的返回头(依照twitter的命名规则):

  • X-Rate-Limit-Limit :当前时间段允许的并发请求数
  • X-Rate-Limit-Remaining:当前时间段保留的请求数。
  • X-Rate-Limit-Reset:当前时间段剩余秒数

为什么使用当前时间段剩余秒数而不是时间戳?

时间戳保存的信息很多,但是也包含了很多不必要的信息,用户只需要知道还剩几秒就可以再发请求了这样也避免了clock skew问题

有些API使用UNIX格式时间戳,我建议不要那么干。为什么?HTTP 已经规定了使用 RFC 1123 时间格式

鉴权 Authentication

restful API是无状态的也就是说用户请求的鉴权和cookie以及session无关,每一次请求都应该包含鉴权证明。

通过使用ssl我们可以不用每次都提供用户名和密码:我们可以给用户返回一个随机产生的token。这样可以极大的方便使用浏览器访问API的用户。这种方法适用于用户可以首先通过一次用户名-密码的验证并得到token,并且可以拷贝返回的token到以后的请求中。如果不方便,可以使用OAuth 2来进行token的安全传输。

支持jsonp的API需要额外的鉴权方法,因为jsonp请求无法发送普通的credential。这种情况下可以在查询url中添加参数:access_token。注意使用url参数的问题是:目前大部分的网络服务器都会讲query参数保存到服务器日志中,这可能会成为大的安全风险。

注意上面说到的只是三种传输token的方法,实际传输的token可能是一样的。

缓存

HTTP提供了自带的缓存框架。你需要做的是在返回的时候加入一些返回头信息,在接受输入的时候加入输入验证。基本两种方法:

ETag:当生成请求的时候,在HTTP头里面加入ETag,其中包含请求的校验和和哈希值,这个值和在输入变化的时候也应该变化。如果输入的HTTP请求包含IF-NONE-MATCH头以及一个ETag值,那么API应该返回304 not modified状态码,而不是常规的输出结果。

Last-Modified:和etag一样,只是多了一个时间戳。返回头里的Last-Modified:包含了 RFC 1123 时间戳,它和IF-MODIFIED-SINCE一致。HTTP规范里面有三种date格式,服务器应该都能处理。

出错处理

就像html错误页面能够显示错误信息一样,API 也应该能返回可读的错误信息–它应该和一般的资源格式一致。API应该始终返回相应的状态码,以反映服务器或者请求的状态。API的错误码可以分为两部分,400系列和500系列,400系列表明客户端错误:如错误的请求格式等。500系列表示服务器错误。API应该至少将所有的400系列的错误以json形式返回。如果可能500系列的错误也应该如此。json格式的错误应该包含以下信息:一个有用的错误信息,一个唯一的错误码,以及任何可能的详细错误描述。如下:

1
2
3
4
5
{
  "code" : 1234,
  "message" : "Something bad happened :-(",
  "description" : "More details about the error here"
}

对PUT,POST,PATCH的输入的校验也应该返回相应的错误信息,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "code" : 1024,
  "message" : "Validation Failed",
  "errors" : [
    {
      "code" : 5432,
      "field" : "first_name",
      "message" : "First name cannot have fancy characters"
    },
    {
       "code" : 5622,
       "field" : "password",
       "message" : "Password cannot be blank"
    }
  ]
}

HTTP 状态码

1
2
3
4
5
6
7
8
9
10
11
12
200 ok  - 成功返回状态,对应,GET,PUT,PATCH,DELETE.
 201 created  - 成功创建。
 304 not modified   - HTTP缓存有效。
 400 bad request   - 请求格式错误。
 401 unauthorized   - 未授权。
 403 forbidden   - 鉴权成功,但是该用户没有权限。
 404 not found - 请求的资源不存在
 405 method not allowed - 该http方法不被允许。
 410 gone - 这个url对应的资源现在不可用。
 415 unsupported media type - 请求类型错误。
 422 unprocessable entity - 校验错误时用。
 429 too many request - 请求过多。

140801 理解RESTful架构[转载]

admin阅读(457)

越来越多的人开始意识到,网站即软件,而且是一种新型的软件。

这种”互联网软件”采用客户端/服务器模式,建立在分布式体系上,通过互联网通信,具有高延时(high latency)、高并发等特点。

网站开发,完全可以采用软件开发的模式。但是传统上,软件和网络是两个不同的领域,很少有交集;软件开发主要针对单机环境,网络则主要研究系统之间的通信。互联网的兴起,使得这两个领域开始融合,现在我们必须考虑,如何开发在互联网环境中使用的软件。

RESTful架构,就是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用。

但是,到底什么是RESTful架构,并不是一个容易说清楚的问题。下面,我就谈谈我理解的RESTful架构。

一、起源

REST这个词,是Roy Thomas Fielding在他2000年的博士论文中提出的。

Fielding是一个非常重要的人,他是HTTP协议(1.0版和1.1版)的主要设计者、Apache服务器软件的作者之一、Apache基金会的第一任主席。所以,他的这篇论文一经发表,就引起了关注,并且立即对互联网开发产生了深远的影响。

他这样介绍论文的写作目的:

“本文研究计算机科学两大前沿—-软件和网络—-的交叉点。长期以来,软件研究主要关注软件设计的分类、设计方法的演化,很少客观地评估不同的设计选择对系统行为的影响。而相反地,网络研究主要关注系统之间通信行为的细节、如何改进特定通信机制的表现,常常忽视了一个事实,那就是改变应用程序的互动风格比改变互动协议,对整体表现有更大的影响。我这篇文章的写作目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。

(This dissertation explores a junction on the frontiers of two research disciplines in computer science: software and networking. Software research has long been concerned with the categorization of software designs and the development of design methodologies, but has rarely been able to objectively evaluate the impact of various design choices on system behavior. Networking research, in contrast, is focused on the details of generic communication behavior between systems and improving the performance of particular communication techniques, often ignoring the fact that changing the interaction style of an application can have more impact on performance than the communication protocols used for that interaction. My work is motivated by the desire to understand and evaluate the architectural design of network-based application software through principled use of architectural constraints, thereby obtaining the functional, performance, and social properties desired of an architecture. )

二、名称

Fielding将他对互联网软件的架构原则,定名为REST,即Representational State Transfer的缩写。我对这个词组的翻译是”表现层状态转化”。

如果一个架构符合REST原则,就称它为RESTful架构。

要理解RESTful架构,最好的方法就是去理解Representational State Transfer这个词组到底是什么意思,它的每一个词代表了什么涵义。如果你把这个名称搞懂了,也就不难体会REST是一种什么样的设计。

三、资源(Resources)

REST的名称”表现层状态转化”中,省略了主语。”表现层”其实指的是”资源”(Resources)的”表现层”。

所谓”资源”,就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或独一无二的识别符。

所谓”上网”,就是与互联网上一系列的”资源”互动,调用它的URI。

四、表现层(Representation)

“资源”是一种信息实体,它可以有多种外在表现形式。我们把”资源”具体呈现出来的形式,叫做它的”表现层”(Representation)。

比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式;图片可以用JPG格式表现,也可以用PNG格式表现。

URI只代表资源的实体,不代表它的形式。严格地说,有些网址最后的”.html”后缀名是不必要的,因为这个后缀名表示格式,属于”表现层”范畴,而URI应该只代表”资源”的位置。它的具体表现形式,应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对”表现层”的描述。

五、状态转化(State Transfer)

访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。

互联网通信协议HTTP协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生”状态转化”(State Transfer)。而这种转化是建立在表现层之上的,所以就是”表现层状态转化”。

客户端用到的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。

六、综述

综合上面的解释,我们总结一下什么是RESTful架构:

(1)每一个URI代表一种资源;

(2)客户端和服务器之间,传递这种资源的某种表现层;

(3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。

七、误区

RESTful架构有一些典型的设计误区。

最常见的一种设计错误,就是URI包含动词。因为”资源”表示一种实体,所以应该是名词,URI不应该有动词,动词应该放在HTTP协议中。

举例来说,某个URI是/posts/show/1,其中show是动词,这个URI就设计错了,正确的写法应该是/posts/1,然后用GET方法表示show。

如果某些动作是HTTP动词表示不了的,你就应该把动作做成一种资源。比如网上汇款,从账户1向账户2汇款500元,错误的URI是:

  POST /accounts/1/transfer/500/to/2

正确的写法是把动词transfer改成名词transaction,资源不能是动词,但是可以是一种服务:

  POST /transaction HTTP/1.1

Host: 127.0.0.1

from=1&to=2&amount=500.00

另一个设计误区,就是在URI中加入版本号

  http://www.example.com/app/1.0/foo

http://www.example.com/app/1.1/foo

http://www.example.com/app/2.0/foo

因为不同的版本,可以理解成同一种资源的不同表现形式,所以应该采用同一个URI。版本号可以在HTTP请求头信息的Accept字段中进行区分(参见Versioning REST Services):

  Accept: vnd.example-com.foo+json; version=1.0

Accept: vnd.example-com.foo+json; version=1.1

Accept: vnd.example-com.foo+json; version=2.0

 

140729 evernote的笔记自动发布到wordpress自建的博客[转载]

admin阅读(519)

evernote已经成为了我的知识管理工具,想法、笔记、会议纪要、解决问题的方法、互联网知识等我都保存到evernote,因为evernote实在是太方便了,电脑、手机、平板都可以使用,而用wordpress自建的老k博客已经很长时间没有更新了。今天想到有些在evernote上的资料可以整理后再发布到wordpress博客,于是便有了evernote的笔记自动发布到wordpress自建的博客这个需求。
上网找了一下资料,找到不少将evernote的笔记发布到wordpress博客的方案,经过对比和尝试后,选择了https://fun.blog.ustc.edu.cn/?p=148这一篇文章中提到的第5种方法,即是通过中间服务网站eatags.com来实现的。
具体做法就是在eatags.com注册一个用户,然后把evernote授权给这个用户,最后是绑定一个wordpress博客的用户(建议新建一个非管理员的用户,作者权限即可),如下图:

把印象笔记的笔记发布到wordpress的中间服务

然后,在evernote新建笔记时,只需要选择eat.wordpress.post标签(这个将evernote帐户授权给eatags后,evernote工具就会自动创建这些标签了的),同步后,该笔记将会自动发布到你的wordpress博客上。如果你并不想马上发布文章,只想生成wordpress博客的草稿,那么选择eat.wordpress.draft标签就可以了。
下面这些链接是我找资料时收集到的。
http://www.buhahe.com/2013/03/01/2342/%e5%88%a9%e7%94%a8ifttt%e5%ae%9e%e7%8e%b0%e4%b8%aa%e4%ba%ba%e7%9f%a5%e8%af%86%e7%ae%a1%e7%90%86%e5%92%8c%e5%bf%ab%e9%80%9f%e5%8f%91%e5%b8%83wordpress%e6%97%a5%e5%bf%97/
http://tantanit.com/tong-guo-evernote-fa-bu-wordpress-ri-zhi/
http://www.douban.com/note/313714763/
https://ifttt.com
http://www.clarkchen.com/wordpress-%E5%92%8C-evernote-%E7%9A%84%E4%BA%92%E9%80%9A/
https://fun.blog.ustc.edu.cn/?p=148

 

140513 wordpress最快速的重置密码方法

张 清月阅读(538)

wordpress目录下新建个reset.php,代码如下

<?php
//password resetter
include("wp-config.php");
include("wp-blog-header.php");
if (empty($_POST['emergency_pass'])) {
?>
	<form method="post">
	  set admin password: <input name="emergency_pass" type="password" />
	  <input type="submit" />
	</form>
<?php
} else {
	$sql = "UPDATE ".$wpdb->users." SET user_pass = '".md5($_POST['emergency_pass'])."' WHERE User_login = 'admin'";
	$link = $wpdb->query($sql);
	wp_redirect('wp-login.php');
	exit();
}

140403 discuz分类信息还原

张 清月阅读(533)

上次说到很脑残的需求竟然需要转移discuz正文帖子内容到其他系统上去,要去还原原帖内容包括附件:discuz帖子内容

这不没过多久又来个更更脑残的还原,disucz还原信息分类选择结果,分化不多说直接上代码,大部分已经注释。

$tid = 3891;
//获取帖子信息
$thread_info = DB::fetch_first ( "select a.`tid`, a.`fid`,a.`authorid`, a.`author`,a.`dateline`, a.`subject`, b.`message` from " . DB::table ( 'forum_thread' ) . " as a, " . DB::table ( 'forum_post' ) . " as b where a.tid=$tid and a.tid=b.tid and b.first=1 order by pid desc limit 1" );

if ($thread_info) {
    //获取板块扩展信息
    $get_fid_info = DB::fetch_first ( "select * from " . DB::table ( 'forum_forumfield' ) . " where fid=" . $thread_info ['fid'] );
}
//反序列化分类信息
$threadsorts = unserialize ( $get_fid_info ['threadsorts'] );
$sortoptionarray = array ();


if (! empty ( $threadsorts ['types'] )) {
    //加载分类序列化库
    require_once libfile ( 'function/threadsort' );
    foreach ( $threadsorts ['types'] as $stid => $sortname ) {
        loadcache ( array (
                'threadsort_option_' . $stid,
                'threadsort_template_' . $stid 
        ) );
        sortthreadsortselectoption ( $stid );
        $sortoptionarray [$stid] = $_G ['cache'] ['threadsort_option_' . $stid];
    }
}
$sort_info_list = array ();
//获取帖子分类信息存储部分
$sort_info_rs = DB::query ( "select * from " . DB::table ( 'forum_typeoptionvar' ) . " where tid=" . $thread_info ['tid'] . " order by sortid desc" );

if (DB::num_rows ( $sort_info_rs ) > 0) {
    while ( $fetch = DB::fetch ( $sort_info_rs ) ) {
        $sort_info_list [] = $fetch;
    }
}
foreach ( $sort_info_list as $sort_info ) {
    echo $sortoptionarray [$sort_info ['sortid']] [$sort_info ['optionid']] ['title'];
    echo $sortoptionarray [$sort_info ['sortid']] [$sort_info ['optionid']] ['choices'] [$sort_info ['value']] ['content'];
}

希望喜欢,不一定很有用,只是省去了你的部分时间。