前言
redis是一个内存中的key-value型数据库。
也就是说,该数据库中的每条记录,都是由一个key和一个value构成的,其中value有五种类型:字符串(strings),散列(hashes),列表(list),集合(set),有序集合(sorted set)。
不知道各位小伙伴有没有听过memcache这种技术,这种技术也是内存中的key-value型数据库。
与redis的唯一区别就是,memcache的value没有类型之分,而这也是memcache被redis取代的直接原因。
正是因为redis的value类型有这么多种,并且每种类型的value都有不同的处理方法,可以让一些计算发生在服务端,从而减轻客户端的压力。
那么这篇文章我们就来介绍一下redis中五种value类型的基本操作方法。
不过在介绍value的类型之前,需要介绍一下本文的测试工具——redis cli。
redis cli就是redis中的命令行工具
根据上一篇文章的介绍配置好环境后,在命令行中输入redis-cli即可进入redis的命令行界面,如下图所示:

如果您运行不成功,请参照这篇文章进行环境配置。
value介绍
所谓“授人以鱼不如授人以渔”,本文在介绍不同的value及其常用方法前,先介绍一个无往不利的命令——help。
就像linux系统中的man命令,help也提供了各种命令的讲解。
通过这个命令就可以自行查看各种类型的api,以后遇到问题就可以自行解决啦。

value的type与encoding
在正式介绍不同的value类型之前,先来介绍一个redis中key上的两个属性——type与encoding。
这两个属性用来标记value的类型。
其中,type就是我们接下来要介绍的五种类型,而encoding则是更细致的区分value,在string类型的value中就会看到encoding的不同。
字符串(string)
这种类型的value的type就会被标记成string,但是encoding会明确的反应value的数据类型,看下面这段命令:
> set k1 1
OK
> type k1
string
> object encoding k1
"int"
> set k2 hello
OK
> type k2
string
> object encoding k2
"embstr"
同样都是string类型的value,encoding却一个是int,一个是embstr。
通过上面这段命令我们可以看出,虽然同为string类型的value,具体的类型也会有不同,下面我们就来介绍同为string类型的不同的三种value。
字符串
对于一个字符串类型的数据,我们预期能有什么操作呢?
set/get
这两个操作很简单,就是简单的赋值与获取操作。
> SET k1 hello
OK
> GET k1
"hello"
mset/mget
用法与上面的两个命令相同,但是可以同时设置多个key的值,并且是个原子操作,也就是说,如果一个设置失败了,那么整个命令都不会成功。
append
这个命令也是顾名思义,在某个string后面追加一些字符
> append k1 world
(integer) 10
> get k1
"helloworld"
setrange/getrange
这两个操作看上去就不是那么直观了,这个时候就可以利用help命令来查看这个命令的用法。
> help setrange
SETRANGE key offset value
summary: Overwrite part of a string at key starting at the specified offset
since: 2.2.0
group: string
根据帮助文档,我们就知道,这两个命令是从指定的位置开始重写字符串。
那么我们就能推断出,用法大致如下:
> setrange k1 5 " redis"
(integer) 11
> get k1
"hello redis"
strlen
这个命令也很简单啦,就是得到字符串的长度嘛!
> set k1 "hello world"
OK
> strlen k1
(integer) 11
数值类型
没错,在redis中,数值类型的value也归类在字符串类型中,并且可以对这种类型的数据进行数值运算。
incr、incrby
这两个操作是我们对数值类型的数据经常进行的操作。
其中,incr是把数值增一,而incrby可以指定增加的数值
用法如下:
> set k1 0
OK
> incr k1
(integer) 1
> get k1
"1"
> incrby k1 4
(integer) 5
> get k1
"5"
> incrby k1 -5
(integer) 0
> get k1
"0"
通过这个例子还能看出来,通过incrby命令加上一个负数实现减操作。
位图
也可以称为bitmap,其实就是根据实际应用,人为赋予比特位特殊的含义。
首先,我们需要明确一个概念——二进制安全。
二进制安全就是说,在redis与外界交互的时候,只通过字节流,不用编码方式将字节流读取成字符流,毕竟编码方式有很多种,不同的编码方式会造成乱码问题。
所以类似strlen等命令,其实算的是字节长度。
所谓字节,就是由8个比特位组成的单元,offset如下图所示:

offset也是从零开始,由左到右依次增长。
setbit
这个命令的用法如下:
## setbit key offset value
> setbit k1 7 1
(integer) 0
> strlen k1
(integer) 1
> setbit k2 9 1
(integer) 0
> strlen k2
(integer) 2
这个命令中的offset是bit级别的offset。
所以上面命令的结果就很好理解了。
第7个比特位用一个字节就能表示,而第9个比特位要靠两个字节才能表示,这也是命令strlen显示出的结果。
bitcount
这个命令就可以知道一个字节中有多少个比特位是1.
> setbit k3 1 1
(integer) 0
> setbit k3 5 1
(integer) 0
> setbit k3 7 1
(integer) 0
> bitcount k3
(integer) 3
上面的命令就是讲一个value的三个比特位设为1,然后通过bitcount可以得到为1的比特位有几个。
bitpos
## BITPOS key bit [start] [end]
> bitpos k3 1 0 7
(integer) 1
继续用上面的例子,可以知道k3在位置1的比特位是1,bitpos就能知道某个范围内,第一个数值为查询值的比特位是第几位。
bitop
位运算
位运算的案例分析
如果公司有用户系统,统计用户的登录天数,且窗口随机。
这个问题用位图就能很恰当的解决。
比如一个用户zhangsan分别在一年中的第一天,第100天和第365天登录过系统,我们通过位图就能完美记录,并且查询某个窗口期也会很方便。
来看看解决方法:
> setbit zhangsan 1 1
(integer) 0
> setbit zhangsan 100 1
(integer) 0
> setbit zhangsan 365 1
(integer) 0
> bitcount zhangsan 0 365
(integer) 3
也就是说位图中的每一位代表一天,如果那天该用户登录了,比特位就设为1。
那么统计一年中某个阶段内该用户的登录时间也很好办,直接用bitcount统计该阶段内的比特位为1的个数即可。
比如统计年末一周内用户的登录次数:
> bitcount zhangsan -7 -1
(integer) 1
也就是说,该用户在年末内一周只登录了一天。、
散列(hashes)
散列就可以理解成一个K-V键值对的map。
散列类型的方法有个标志就是都是以h开头的。
有过编程经验的小伙伴对这些map的基本操作都烂熟于心了,我就简单的把这些方法罗列出来。
hset/hget
## HSET key field value [field value ...]
> hset zhangsan age 18 name zhangsan gender male
(integer) 3
> hget zhangsan age
"18"
hkeys
> hkeys zhangsan
1) "age"
2) "name"
3) "gender"
hvals
> hvals zhangsan
1) "18"
2) "zhangsan"
3) "male"
hgetall
> hgetall zhangsan
1) "age"
2) "18"
3) "name"
4) "zhangsan"
5) "gender"
6) "male"
对散列中的value同样可以做数值计算。
hincrebyfloat
> HINCRBY zhangsan age 1
(integer) 19
> hget zhangsan age
"19"
列表(lists)
对于这种容器类型的value,首先要掌握的就是增删改查操作。
首先来看看列表的增删改查操作
增
lpush
在list的左边添加元素。
> lpush listtest 1 2 3
(integer) 3
> lrange listtest 0 10
1) "3"
2) "2"
3) "1"
上面的命令lpush是将1,2,3依次从列表的左边“压入”列表中,lrange就是从列表的最左边开始打印元素。
可以看到最后被压进list的在最左边。
rpush
在list的右边添加元素,其他方面与lpush一样。
删
lpop
将list最左边的元素弹出列表。
## LPOP key [count]
> lpop listtest 1
1) "3"
> lpop listtest 2
1) "2"
2) "1"
rpop
将list最右边的元素弹出列表。
查
lrange
通过上面的例子也可以看出,lrange可以按从左到右的顺序将元素打印出来。
命令的组合
通过不同命令的组合,可以用list实现不同的数据结构,比如栈和队列。
栈
栈是一个先进后出的一种数据结构。
通过同向的命令,比如lpush与lpop或者rpush与rpop,就会实现一个栈。
队列
队列是一种先进后出的数据结构。
通过异向的命令,比如lpush与rpop或者rpush与lpop,就会实现一个队列。
阻塞,单播队列
list有几个B开头的方法,都是阻塞方法。所以list也可以实现阻塞队列。
BLPOP, BLPUSH等等。
并且阻塞队列也存在先进先出的特性,先被阻塞的线程先恢复。
集合(sets)
有过编程经验的小伙伴都知道set这种数据结构,这是一种去重、无序的容器,类似java中的hashset。
集合类型的方法有个特征就是都是s打头。
提起集合,我们最先想到的就是各种集合运算。
集合运算
所谓集合运算,就是求多个集合之间的交、并、差集。
sinter/sinterstore
这两个命令都是求交集的,区别在于,sinterstore可以将结果储存在一个新key中,而sinter只是将结果打印出来。
> sadd set1 1 2 3 4 5
(integer) 5
> sadd set2 3 4 5 6 7
(integer) 5
> sinter set1 set2
1) "3"
2) "4"
3) "5"
> sinterstore interdest set1 set2
(integer) 3
> smembers interdest
1) "3"
2) "4"
3) "5"
sunion/sunionstore
这两个命令是用来计算几个集合的并集的,用法与sinter差不多。
sdiff/sdiffstore
要计算差集,就会涉及到左差或者右差,通过调整参数的顺序就可以得出。
来看下面的例子:
> sadd set1 1 2 3 4 5
(integer) 5
> sadd set2 3 4 5 6 7
(integer) 5
> sdiff set1 set2
1) "1"
2) "2"
> sdiff set2 set1
1) "6"
2) "7"
随机事件
这种操作是指在集合中随机的抽出元素来。
srandmember
这种操作可以指定从集合中随机抽出几个元素。
其中,count可以取正数或者负数。
count是正数,代表取出的元素不能重复。
count是负数,代表取出的结果可以重复。
用法如下:
## SRANDMEMBER key [count]
> sadd set 1 2 3 4 5 6
(integer) 6
> srandmember set 3
1) "4"
2) "3"
3) "6"
> srandmember set 7
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
> srandmember set -7
1) "6"
2) "3"
3) "4"
4) "3"
5) "4"
6) "6"
7) "6"
可以发现,如果count是正数的话,将set取空就会结束,但是count是负数的话,就会有重复的元素。
spop
随机取出元素且不放回,与count为正数的srandmember效果相同。
> spop set 3
1) "5"
2) "4"
3) "2"
有序集合(sorted set)
在集合的基础上,又开发出了有序集合。
这个容器的特点就是去重且有序。
那么问题就来了,是按什么排序的呢?
——这个容器中的每个元素除了本身的值以外,还有个维度代表分值,排序就是基于这个分值,如果分值相同,就按照字典序排序。
所以在有序集合中,一个元素有三个维度——自身的值,分值,索引。
有序集合的操作有个标志就是所有的操作都是z开头。
基本操作
由于涉及到有序的概念,所以排名、打印等功能是一定有的。
zadd
向有序数组添加元素,需要指定score等。
## ZADD key score member [score member ...]
> zadd fruits 1 banana 2 apple 3 orange
(integer) 3
zcount
取得有序集合中,分数在指定范围内的元素个数。
## ZCOUNT key min max
> zcount fruits 1 2
(integer) 2
zrange
用来打印有序集合中索引在某个范围内的元素。
## ZRANGE key min max
> zrange fruits 0 3
1) "banana"
2) "apple"
3) "orange"
zrangebyscore
用来打印有序集合中分数在某个范围内的元素,并且可以通过参数withscores将分数也打印出来。
> zrangebyscore fruits 1 3
1) "banana"
2) "apple"
3) "orange"
> zrangebyscore fruits 1 3 withscores
1) "banana"
2) "1"
3) "apple"
4) "2"
5) "orange"
6) "3"
zscore
查询有序集合中某个元素的分值。
> zscore fruits banana
"1"
zrank
查询有序集合中某个元素的排名。
> zrank fruits banana
(integer) 0
> zrank fruits apple
(integer) 1
> zrank fruits orange
(integer) 2
zincrby
通过这个操作可以改变某个元素的分值。
> zincrby fruits 2 apple
"4"
> zrangebyscore fruits 0 5 withscores
1) "banana"
2) "1"
3) "orange"
4) "3"
5) "apple"
6) "4"
用这个操作将原本分值为2的apple变成了4,从而让apple在这个有序集合中的位置也发生了变化。
集合操作
有序集合除了有序,同样还具备集合的特性,也就是说可以进行集合运算。
但是由于有score的存在,又与普通的集合运算有所不同。
需要注意如何处理结果集合中各个元素的分值的方式。
我们拿交集运算来举例。
zinter/zinterstore
通过help命令可以查到,zinter的命令如下:
ZINTER numkeys key [key …] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX] [WITHSCORES]
summary: Intersect multiple sorted sets
since: 6.2.0
group: sorted_set
需要注意两点:
做运算的每个集合都有个权重
WEIGHT需要指定重合元素的聚合方法
AGGREGATE,有三种,分别是sum, min, max,其中默认是sum。
我们根据上面的命令来演示一下:
> zadd fruit1 1 banana 2 apple 3 orange
(integer) 3
> zrange fruit1 0 -1 withscores
1) "banana"
2) "1"
3) "apple"
4) "2"
5) "orange"
6) "3"
> zadd fruit2 2 banana 4 watermelon 6 apple
(integer) 3
> zrange fruit2 0 -1 withscores
1) "banana"
2) "2"
3) "watermelon"
4) "4"
5) "apple"
6) "6"
> zinterstore out 2 fruit1 fruit2 WEIGHTS 1 1
(integer) 2
> zrange out 0 -1 withscores
1) "banana"
2) "3"
3) "apple"
4) "8"
可以看到在进行交集运算的时候,指定了两个set的权重都是1,默认的聚合方法是sum。
最后的结果也反应了这一点。
排序是怎么实现的
跳表
跳表的原理非常复杂,如果细讲完全可以用一篇文章单独来讲。
这里仅仅介绍一下跳表的简单原理。
跳表就是由好几个层次的list组成。
上一个层次的list都是由下一层list抽取出几个特征元素构成。
换句话说,就是通过牺牲存储空间,换取查询上的性能。
跳表也叫平衡树,总的来看,这个数据结构可以达到增删改查平均值的最优。
总结
本文介绍了Redis中五种不同的value类型,并且对每一种类型的特殊方法都进行了讲解。
其实本文讲的东西完全都可以通过help指令来获取,完全就是抛砖引玉。
redis真正的神奇之处还需要各位小伙伴自己去探索啊!