小喵的唠叨话:上一篇博客,我们详细的介绍了Redis 中String数据类型的底层实现,相信大家已经在原理上掌握的相当不错了。这次,我们就介绍Redis的命令行操作。当然,我们实际开发的时候可能不会太经常直接用到Redis的命令,而是通过一些数据库的封装来操控(就像我们虽然会使用SQL,但是实际开发的时候,总是有一些特别好用的库,既安全又方便)。但是,学习最基础的Redis命令,是针对各种语言的以不变应万变的法宝,毕竟库是针对于语言的。同时,学会了Redis的基本命令,将来对各种库的掌握也能更迅速。


Redis基本操作——String(实战篇)

关于Redis的命令,主要参考Redis的官方文档(http://redis.io/commands#string)。小喵这里主要像是翻译一下,并且给出一些实用的例子。排版上,小喵按功能重新组织了一下指令,并将比较常用的指令放在了最前面。同时也增加了锚点,方便大家查阅。小喵这里的Redis版本是3.0.7,应该是目前的最新版。

注:redis的命令(SET,GET等)是不区分大小写的(KEY和VALUE区分的),为了方便所以小喵的操作可能都是小写的。

指令清单:

  • APPEND
  • BITCOUNT
  • BITOP
  • BITPOS
  • DECR
  • DECRBY
  • GET
  • GETBIT
  • GETRANGE
  • GETSET
  • INCR
  • INCRBY
  • INCRBYFLOAT
  • MGET
  • MSET
  • MSETNX
  • PSETEX
  • SET
  • SETBIT
  • SETEX
  • SETNX
  • SETRANGE
  • STRLEN

一,SET的相关操作

1,SET key value [EX seconds] [PX milliseconds] [NX|XX]

Set the string value of a key

将键key设置成指定的字符串value。

我们知道Redis是一种key-value数据库(当然这么说可能不够严谨,因为Redis支持很多高级的数据结构)。因此所有的数据都是通过key来访问,key就是键。

SET指令可以将字符串的value和key绑定在一起。

如果不加额外的参数。当key中已经保存了一个值的话,这个值会被覆盖成新的值,并且忽略掉原始类型(一个key对应的不一定是一个字符串,也可以是List等其他的数据结构,这些结构在后面的文章里面会陆续介绍);如果key不存在,那么则会在数据库中新增一个key,对应的值就是你刚刚设置的。

例子:

redis> get name
(nil)
redis> set name miaoerduo
OK
redis> get name
"miaoerduo"
redis> set name miao
OK
redis> get name
"miao"

GET命令用于查看key对应的值,我们下面会讲到。

第一个get,由于name这个key不存在,因此我们得到的结果是nil,也就是说空。

之后我们设置一次,然后就get到我们的结果了。之后再设置一次,会发现结果已经被覆盖。

这就是set命令的基本用法。

我们看到文档的后面有一些可选的参数。下面依次来介绍:

EX seconds:设置key的过时时间,单位为秒。

PX milliseconds:设置key的过期时间,单位为毫秒。

以ex为例:

redis> set name miaoerduo ex 10
OK
redis> get name
"miaoerduo"
redis> get name
(nil)

我们设置了name的过期时间为10秒。

在设置完name之后,立刻获取name的值,可以得到正确的输出。但是过了10s之后,发现只能得到一个nil。说明这个key已经被清空了。

那么这个设置有什么作用呢?

我们说几个常见的应用场景:

a,验证码

我们经常在登录一些网站或是进行付款等操作的时候,都会收到一些验证码,并且说10min后失效。

实际上就可以通过下面一条指令来实现:

set phone_num code ex 600

用手机号作为key,验证码作为值,超时6min。这样当你输入好验证码,提交的时候,后台就可以了先get phone_num,再比较你的输入和数据库里面存的值,从而完成身份的验证。

b,session

早前,网站通过cookie来保存用户的用户名和密码,之后出现了很多的安全隐患,因此就提出了session的机制。

用户登陆成功之后,本地的cookie会保留一个较长的随机码,而网站后的后台则存储了这个随机码和你的用户id的对应关系。在你第二次登录的时候,cookie会传输到后台,而后台则根据你的随机码,获取你的用户信息,如果符合,则自动登录。这样,即使网站上有不法分子获取了你的cookie,也得不到你的任何信息,因为你的真实的有用的信息都存储在网站的后台。

我们在登录邮箱的时候,通常都会有一个选项,7天内自动登录。这其实就是给后台存的session设置了一个超时。聪明的你是不是已经会自己实现了呢?

NX:(if Not eXist)只有键key不存在的时候才会设置key的值

XX:只有键key存在的时候才会设置key的值

NX通常用于实现锁机制,XX的功能,小喵暂时木有头绪。。。想到应用场景的筒子们可以留言告诉小喵,小喵看到了一定会更新的。

举个例子:

redis> del name
(integer) 0
redis> get name
(nil)
redis> set name miaoerduo nx
OK
redis> set name miaoerduo nx
(nil)
redis> get name
"miaoerduo"
redis> set name miao xx
OK
redis> get name
"miao"
redis> del name
(integer) 1
redis> set name miao xx
(nil)

首先我们使用del指令删除了name(这个指令后面会介绍)。之后使用nx模式设置name,第一次成功了,而第二次就失败了,因为name已经存在了。之后使用xx模式修改,发现确实可以。del name之后,再使用xx模式,就失败了,因为此时的name已经不存在了。

以上,就是set的所有的用法。

2,SETRANGE key offset value

Overwrite part of a string at key starting at the specified offset

这个命令可以覆盖掉key对应的string的一部分。offset表示需要覆盖的字符串的起始位置,之后会用value的值,覆盖掉原始string的对应位置的数据。

这里有一些比较有意思的操作:如果原始key不存在,则默认为一个长度为0的字符串。如果offset超过原始string的长度,那么会在之前的string后面补充0以达到offset。如果value的长度超过了原始string后面可以覆盖的部分,则Redis内部会重新申请内存,完成数据的追加(还记得上一章的内容吗?),这时候数据库由于需要分配内存,可能会出现阻塞(需要分配的内存越大,阻塞时间越长)。

例子:

redis> set str "hello world"
OK
redis> setrange str 6 redis
(integer) 11
redis> get str
"hello redis"
redis> setrange str 15 aha
(integer) 18
redis> get str
"hello redis\x00\x00\x00\x00aha"
redis> del str
(integer) 1
redis> setrange str 5 "hello miao"
(integer) 15
redis> get str
"\x00\x00\x00\x00\x00hello miao"

在这个实验中,我们先新建了一个str,内容是"hello world",之后,从位置6开始写入字符串"redis",则得到了"hello redis"。之后我们在15的位置,写入"aha",这是offset已经比字符串的长度要大了,则Redis会默认填充0(\x00是0的16进制表达),之后再追加字符串。最后,我们给一个不存在的key使用setrange设置了一个值,结果表现得和空字符串一样。

3,MSET key value [key value ...]

Set multiple keys to multiple values

一次性设置多个key-value。如果key的值已存在,则会直接覆盖。

相当与同时调用多次SET命令。不过要注意的是,这个操作是原子的。也就是说,所有的keys都一次性设置的。如果你同时运行两个MSET来设置相同的keys,那么操作的结果也只会是两次MSET中的后一次的结果,而不会是混杂的结果。

例子:

redis> mset name1 miaoerduo name2 miao name3 love
OK
redis> get name1
"miaoerduo"
redis> get name2
"miao"
redis> get name3
"love"

4,MSETNX key value [key value ...]

Set multiple keys to multiple values, only if none of the keys exist

一次性设置多个key-value。如果存在任何一个key已经存在,那么这个操作都不会执行。所以,当使用MSETNX的时候,要么全部key都被修改,要么全部都不被修改。

当然这个操作也是原子的。

redis> get name3
"love"
redis> msetnx name3 miaoerduo name4 miao
(integer) 0
redis> get name4
(nil)
redis> msetnx name4 miaoerduo name5 miao
(integer) 1
redis> get name4
"miaoerduo"
redis> get name5
"miao"

当name3已经存在的时候,我们设置name3和name4,发现,连name4都没有创建。当设置name4和name5的时候,由于这两个key之前都不存在,因此设置成功了。

5,SETEX key seconds value

Set the value and expiration of a key

设置key的值和超时。和前面的set key value ex seconds一样。

6,PSETEX key milliseconds value

Set the value and expiration in milliseconds of a key

设置key的值和超时。和前面的set key value px milliseconds一样。

7,SETNX key value

Set the value of a key, only if the key does not exist

当key不存在的时候,设置key的值。和set key value nx一样。

二、GET的相关操作

1,GET key

Get the value of a key

根据给定的key,获取value值。这个操作,我们之前已经使用了很多次。

如果key不存在,会返回nil。如果key对应的值不是string(List,Set等),则会报错,因为GET只能处理string类型的value。

演示如下:

redis> get name
(nil)
redis> set name "miaoerduo"
OK
redis> get name
"miaoerduo"
redis> lpush arr 1 2 3 4 5
(integer) 5
redis> get arr
(error) WRONGTYPE Operation against a key holding the wrong kind of value

注意,lpush是List部分的内容,arr的值是一个List的结构。这里只需要知道key不是string类型的时候,get的时候就会报错。

2,MGET key [key ...]

Get the values of all the given keys

还记得我们之前可以同时设置多个key-value吗?其实我们也可以一次性获取多个key的值。如果key不存在,则对应的地方返回nil。

那么一次性获取多个值和单独一次一次GET有什么区别呢?

小喵认为,主要有两点,原子操作和查询效率。

比如博客上会统计评论数和访问数。如果我们依次读取这两部分的值,那么可能得到的两个值并不是同一时刻的。而如果使用MGET,则得到的一定是同一时刻的。这就是原子操作的威力(注,官方文档只介绍了SET和GET的操作是原子的,并没有说MGET是不是,这里小喵果断的说MGET是原子操作也是不合理的。但考虑到使用MGET的时候,是把查询指令一次性传输到后台来执行,所以应该原子操作。无论如何,在上面的例子中,使用MGET总是比两次GET要合理的)。

另外,如果数据库的查询,都分为三个过程,传输查询指令,执行指令,输出结果。

如果分多次GET的话,在传输指令和输出结果的这两个部分就要重复很多次,效率大打折扣。


反派汪:分多次查询,需要传输的指令数目,也不会比单次多很多,为什么一定会影响效率呢?

喵太:其实不然,传输数据需要很多的准备工作,不仅仅是数据的具体传输,有时候需要考虑连接的创建和关闭、设置锁等的开销。

反派汪:说的好像有道理,我再回去研究研究。


redis> set key1 a
OK
redis> set key2 b
OK
redis> mget key1 key2 key3
1) "a"
2) "b"
3) (nil)

我们只创建了key1和key2,使用MGET获取的时候,由于没有key3,对应的位置返回了nil。

3,GETRANGE key start end

Get a substring of the string stored at a key

该指令只要用于获取字符串的字串,在Redis2.0版本之前,叫做SUBSTR。strat和end是字串的起始和结束的位置,可以用负数来表示距离string尾部的未知的下标。-1是最后一个字符,-2是底数第二个字符。这个表达方式和Python的获取list的子list非常相似。

需要注意的有两点:

字串包括了start和end这两个位置的字符。在Python中是不包含end的。

当给出的start和end超出了string的范围时,指令只会返回在string内的结果。

redis> set str "hello world"
OK
redis> getrange str 6 -1
"world"
redis> getrange str -5 -1
"world"
redis> getrange str 0 4
"hello"
redis> getrange str 0 100
"hello world"

上述例子是几种情况下的输出。

4,GETSET key value

Set the string value of a key and return its old value

设置key的值,并返回之前的值。如果之前key的值不是string,则会报错。

这个指令相当于先GET,再SET。

这个指令可以用来配合INCR指令一起使用支持重置的技术功能(INCR我们后面会讲到)。先设置count为0,每次INCR使得count加1。等到需要获取计数的时候,使用GETSET count 0,就能获取计数的值,并且把计数器重置了。

redis> set str "hello"
OK
redis> getset str "world"
"hello"
redis> get str
"world"

第一行设置str为”hello”

第二行获取了str原先的值,并把str设置成”world”。

第三行GET的时候,就是修改之后的值了。

三、string的修改操作

1,STRLEN key

Get the length of the value stored in a key

返回key对应的string的长度,如果key对应的不是string,则报错。如果key不存在,则返回0(还是把key对应的看成空字符串)。

redis> set str "hello world"
OK
redis> strlen str
(integer) 11
redis> get nokey
(nil)
redis> strlen nokey
(integer) 0

2,APPEND key value

Append a value to a key

如果key已经存在,且值为string,则将value追加到值的后面,如果key不存在,则会创建一个空的字符串的key,然后执行追加操作。

redis> set str "hello"
OK
redis> append str " world"
(integer) 11
redis> get str
"hello world"
redis> del str
(integer) 1
redis> append str "hello"
(integer) 5
redis> get str
"hello"

这个例子中,我们先向已有值的str中append了一个字符串。然后向不存在的key,也添加了字符串。

3,INCR key

Increment the integer value of a key by one

对存储在key的整数值进行原子的加1操作。

如果key不存在,则会设置默认值0,再加1。

如果key存在,但是存储的值不是字符串,或者存储的字符串不能表示整数,则执行该操作时,会报错。

这个操作仅限于64位的有符号的整型数据。

比较有意思的是,虽然这个key存储的值是个字符串,但是该操作的效果和对相应的数字进行操作一样。而且,Redis中,在存储这类字符串的时候,底层上其实存储的就是一个整数,因此不存在存储上的浪费。

redis> set count 0
OK
redis> incr count
(integer) 1
redis> incr count
(integer) 2
redis> del count
(integer) 1
redis> get count
(nil)
redis> incr count
(integer) 1
redis> incr count
(integer) 2

值得注意的是,该操作是原子操作,即使有多个请求传输到Redis,count执行的结果都不会错误,所以我们可以大胆放心的用这个功能实现多线程的计数功能。

4,DECR key

Decrement the integer value of a key by one

对存储在key的整数值进行原子的减1操作。

注意事项和INCR一样。

5,INCRBY key increment

Increment the integer value of a key by the given amount

对存储在key的整数值进行原子的加操作,加increment。

如果key不存在,操作之前,key就会被置为0。如果key的value类型错误或者是个不能表示成数字的字符串,就返回错误。这个操作最多支持64位有符号的整型数字。基本上和INCR一样。

6,DECRBY key decrement

Decrement the integer value of a key by the given number

对存储在key的整数值进行原子的减操作,减increment。

其他和INCR一样。

7,INCRBYFLOAT key increment

Increment the float value of a key by the given amount

对存储造key中的浮点数进行原子的加操作,加increment。

如果key不存在,操作之前,key就会被置为0。如果key的value类型错误或者是个不能表示成浮点数的字符串,就返回错误。

我们并没有DECRBYFLOAT这个操作,因此想要实现减操作,只需要把increment设成负的就可以。

redis> set a 1.5
OK
redis> incrbyfloat a 10.1
"11.6"
redis> incrbyfloat a 10.1
"21.7"
redis> incrbyfloat a -10.1
"11.6"
redis> incrbyfloat a -1.5e2
"-138.39999999999999999"

浮点数可以用一般的小数和科学计数法表示。

四、二进制操作

1,SETBIT key offset value

Sets or clears the bit at offset in the string value stored at key

设置或者清空key的value(字符串)在offset处的bit值。这里将string看成由bit组成的数组。

指定位置的bit可以被设置,或者被清空,这个由value(只能是0或者1)来决定。当key不存在的时候,就创建一个新的字符串value。要确保这个字符串足够长到在offset处有bit值。参数offset需要大于等于0,并且小于2^32(限制bitmap大小为512)。当key对应的字符串增大的时候,新增的部分bit值都是设置为0。

该操作返回value原来的offset位置的bit值。

redis> setbit a 0 1
(integer) 0
redis> setbit a 1 1
(integer) 0
redis> setbit a 2 1
(integer) 0
redis> setbit a 3 1
(integer) 0
redis> get a
"\xf0"

a最开始不存在,使用setbit操作,将a的前4位都设置成1。最终就得到了\xf0,这是16进制表示的结果,前4位都是1,其他都是0。

2,GETBIT key offset

Returns the bit value at offset in the string value stored at key

获取key对应的string在offset处的bit值。当offset超出了字符串长度的时候,这个字符串就被假定为由0比特填充的连续空间。当key不存在的时候,它就认为是一个空字符串,所以offset总是超出范围,然后value也被认为是由0比特填充的连续空间。

redis> setbit a 7 1
(integer) 0
redis> getbit a 0
(integer) 0
redis> getbit a 7
(integer) 1
redis> getbit a 100
(integer) 0

3,BITCOUNT key [start end]

Count set bits in a string

统计key的string的二进制中1的个数。

start和end分别表示string的起始和结束位置,含义和GETRANGE中一样。

redis> setbit mykey 0 1
(integer) 0
redis> setbit mykey 10 1
(integer) 0
redis> setbit mykey 5 1
(integer) 0
redis> bitcount mykey
(integer) 3

4,BITOP operation destkey key [key …]

Perform bitwise operations between strings

对string进行bit级别的操作。具体操作有4种。AND,OR,XOR,NOT。

用法如下:

BITOP AND destkey srckey1 srckey2 srckey3 … srckeyN

BITOP OR destkey srckey1 srckey2 srckey3 … srckeyN

BITOP XOR destkey srckey1 srckey2 srckey3 … srckeyN

BITOP NOT destkey srckey

NOT操作后面只有一个目标key和srckey,是因为NOT操作是一元的。

对于AND,OR和XOR操作,Redis会将srckey1,srckey2,…,srckeyN这些字符串对位进行相关操作,之后将结果存入destkey中。

如果srckey的length不相等的话,Redis内部会将短的字符串补齐,并填充上0。

redis> set key1 "\xf0\xf0"
OK
redis> set key2 "\x0f\x0f"
OK
redis> set key3 "\xff\xff"
OK
redis> bitop and destkey key1 key2 key3
(integer) 2
redis> get destkey
"\x00\x00"
redis> bitop or destkey key1 key2 key3
(integer) 2
redis> get destkey
"\xff\xff"
redis> bitop xor destkey key1 key2 key3
(integer) 2
redis> get destkey
"\x00\x00"
redis> bitop not destkey key1
(integer) 2
redis> get destkey
"\x0f\x0f"

5,BITPOS key bit [start] [end]

Find first bit set or clear in a string

返回string的二进制中第一个0或1的位置。

这里将string看成一个有许多bit组成的数组。其中start和end表示string的一个间隔,如果指定了start和end,则BITPOS只会查询这个区间。注意,start和end表示的字符的位置,不是bit的位置。

即使指定了start和end,BITPOS这个操作也只会这个目标bit的绝对地址。

有几点需要注意:

在没有指定查询区间或只指定start的时候,查询bit位为0的位置时,如果string中没有该位,则会返回string的bit位的总数。比如在\xff\xff\xff直接查询bit为0的位置,Redis默认该字符串后面都是0,因此,返回的结果就是12(下标从0开始数的)。

如果指定了查询区间,无论查询0或是1,在没查询到的时候只会返回-1。

在没有指定查询区间时,查询bit位为1的位置时,如果string中没有该位,则会返回-1,表示未查询到。

redis> set bits "\x00\xff\x00\xff"
OK
redis> strlen bits
(integer) 4
redis> bitpos bits 0
(integer) 0
redis> bitpos bits 1
(integer) 8
redis> bitpos bits 0 1
(integer) 16
redis> bitpos bits 0 1 -1
(integer) 16
redis> bitpos bits 1 1 -1
(integer) 8
redis> bitpos bits 0 3
(integer) 32
redis> bitpos bits 0 3 3
(integer) -1

看上面的例子,bits的初始设置的二进制表示为:

00000000 11111111 00000000 11111111

直接获取0(bitpos bits 0)的位置为0,获取1(bitpos bits 1)的位置为8。

指定开始位置为1(start = 1)的时候,0第一次出现(bitpos bits 1)的位置为16。

指定起始位置为3(start = 3)的时候,0第一次出现(bitpos bits 0 3)的位置为32。这是因为Redis在查询字符串的时候查询到了字符串的末尾,之后默认末尾的后面都是0,因此得到了32这的个位置。

最后,当指定必须在第3个字节(从0开始数)查询0的时候,由于查不到0,因此只返回了-1。


这篇博客的内容,小喵自己也感觉有点多,每个指令小喵都是参考了官方的文档,然后都自己动手做了实验。可能有些地方,小喵没有理解的很好,这时就需要大家多多指教了。