【redis系列】(二)redis中五种value类型与其常用方法

前言

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上的两个属性——typeencoding

这两个属性用来标记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

需要注意两点:

  1. 做运算的每个集合都有个权重WEIGHT

  2. 需要指定重合元素的聚合方法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真正的神奇之处还需要各位小伙伴自己去探索啊!


版权声明:本文为asd13518662原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。