前言
redis是key value格式的数据库,redis的value提供了五种数据类型,string list hash set sorted_set,这里要特别注意,redis所谓的数据类型针对的是value,而key只有String这种类型。我们很有必要学习通过redis客户端操作这些数据类型,因为后续我们使用高级的API方式调用,也可以从redis基本的五种数据类型操作命令中找到影子。其实不管redis的数据类型是哪种,在redis中**,只存储成字节**,redis服务把字节数组返回给客户端,客户端根据自己的编码转成对应的编码格式。这里就要求redis的客户端需要提前商量好编码格式,不然会出现乱码的现象。而出现乱码的原因就是存储在redis的客户端和读取redis的客户端编码格式不一致。
我们先说下怎么学习这些操作命令,不管学什么技术,不懂就输入help
一、String类型
要学习string 类型的命令,我们可以输入
help @string
这个时候回出现所有string数据类型的相关操作命令。好下面就让我们来学习下string相关的命令。string数据类型的数据类型又可以细分为字符串,数值型,bitmap型。
在进行验证前先把之前redis库中的数据情空,有两种方式清库
FLUSHALL 这个命令是把redis中16库都清空
FLUSHDB 这个命令是把redis客户端登录当前的这个数据库清空
这两个命令在生产环境中慎用,不然数据就丢失了
数据类型推断方法
# 通过help @generic可以找到type命令含义,判断对应key的value的数据类型
# 举例,如果k0 是通过set k0 xx ,那么通过help @string可以推断出string类型
127.0.0.1:6379> type k0
string
127.0.0.1:6379> OBJECT help
1) OBJECT <subcommand> arg arg ... arg. Subcommands are:
2) ENCODING <key> -- Return the kind of internal representation used in order to store the value associated with a key.
3) FREQ <key> -- Return the access frequency index of the key. The returned integer is proportional to the logarithm of the recent access frequency of the key.
4) IDLETIME <key> -- Return the idle time of the key, that is the approximated number of seconds elapsed since the last access to the key.
5) REFCOUNT <key> -- Return the number of references of the value associated with the specified key.
我们可以通过 OBJECT ENCODING key知道对应key的数据类型字符串还是数值型
1.1 字符串类型
增加
# 增加单个key
127.0.0.1:6379> set k0 a
OK
# 往k0的value后面追加 此时k0 为aaa
127.0.0.1:6379> APPEND k0 aa
(integer) 3
# 批量新增多个key
127.0.0.1:6379> mset k1 b k2 c k3 d
OK
# 如果该key不存在,则新增一个key
127.0.0.1:6379> SETNX k4 e
(integer) 1
#批量创建key,如果有一个存在,则新增不成功
127.0.0.1:6379> MSETNX k5 f k6 g
(integer) 1
# 设置key 超时时间 时间单位为妙
127.0.0.1:6379> SETEX k5 100 f
OK
#设置key 超时时间,时间单位毫秒
127.0.0.1:6379> PSETEX k5 100000 ff
OK
查询
# 查询单个key
127.0.0.1:6379> get k0
"a"
# 批量查询key
127.0.0.1:6379> MGET k0 k1 k2
1) "a"
2) "b"
3) "c"
# 推断数据类型
127.0.0.1:6379> type k0
string
# 字符串类型
127.0.0.1:6379> OBJECT ENCODING k1
"embstr"
# 返回key 对应value字节的长度
127.0.0.1:6379> STRLEN k0
(integer) 3
# 因为我的客户端是utf8编码所以在redis中就存储3个字节本来表示
127.0.0.1:6379> set k0 中
OK
2.2 数值类型
和string类型差不多一样的命令,只是数值类型是存储进redis的数据为 整数字时,redis会默认设置他的value类型为string 中数值类型
127.0.0.1:6379> set k0 111
OK
127.0.0.1:6379> TYPE k0
string
127.0.0.1:6379> OBJECT encoding k0
"int"
数值类型的数据可以进行递增或者递减操作
# 增加1
127.0.0.1:6379> INCR k0
(integer) 112
#指定增长幅度
127.0.0.1:6379> INCRBY k0 3
(integer) 115
# 增长浮点数据,此时的数据就变成字符串了,就不能进行数值运算了
127.0.0.1:6379> INCRBYFLOAT k0 0.5
"115.5"
#将数据还原
127.0.0.1:6379> set k0 111
OK
# 减少1
127.0.0.1:6379> DECR k0
(integer) 110
# 指定减少幅度减少
127.0.0.1:6379> DECRBY k0 5
(integer) 105
像incr decr 这些原子性的操作可以应用在秒杀,点赞数量等这种需要事务控制数据增加或者减少的地方。
2.3 bitmap
位图,这是一个在redis中非常有用的好东西。在讲他之前先来聊下这两个场景,大家会怎么去设计。
第一,统计每个用户一年当中登录访问了我们的系统多少次
第二,一段时间内,统计有多少用户登录
首先什么是位图?
举个例子,一个字节有8个比特位
0000 0000 我们通过redis的命令可以改变比特位从0变1或者1变0.这就是位图
新增
# 0100 0000 对应ASCII中的@符号
127.0.0.1:6379> SETBIT k0 1 1
(integer) 0
127.0.0.1:6379> get k0
"@"
# 0100 0001 对应ASCII中的A符号
127.0.0.1:6379> SETBIT k0 7 1
(integer) 0
127.0.0.1:6379> get k0
"A"
统计有少比特位是1
127.0.0.1:6379> BITCOUNT k0
(integer) 2
查看比特位在区间内,哪个位置先出现
# 0100 0001
#这里有个正反向索引的概念,从左向右去算位置是0,1,2...递增
#从右向左算位置则为-1,-2,-3....
# 所以下面命令的意思是在【0,-1】从头到尾统计比特位第一次出现1的位置
127.0.0.1:6379> BITPOS k0 1 0 -1
(integer) 1
127.0.0.1:6379> get k0
"A"
# 下面命令的意思是在【2,-1】从第二个索引到尾统计比特位第一次出现1的位置
127.0.0.1:6379> BITPOS k0 1 2 -1
(integer) -1
127.0.0.1:6379>
比特位与或运算
127.0.0.1:6379> SETBIT k0 1 1
(integer) 0
127.0.0.1:6379> SETBIT k1 7 1
(integer) 0
# or或运算
127.0.0.1:6379> BITOP or dk k0 k1
(integer) 1
127.0.0.1:6379> get dk
"A"
#与运算
127.0.0.1:6379> BITOP and dk2 k0 k1
(integer) 1
127.0.0.1:6379> get dk2
"\x00"
基础知识说完,现在回到最上面说的两个问题。如果用redis的bitmap将可以结局
首选第一个问题,统计每个用户一年当中哪几天登录访问了我们的系统
我们可以这样设计,redis中的key是userId至于value我们可以给365个bit。我们只需要45个字节就可以处理。比如用户10.1号登陆,我们就可以在redis中对应10.1号的bit位上设置为1。后面只需要bitcount就可以得出这个用户一年登陆我们系统多少次。
第二个问题,一段时间内,统计有多少用户登录,这里我们需要使用redis给我们提供bitmap的比特位或运算。
我们可以这样设计,redis的key为日期如20200920这种,然后根据给我们系统每个用户一个编号从0开始计数,这样每天,当有用户登录的时候,我们在redis对应日期key的value中找到对应用户所表示的位,给他设置为1,这样就表示了这个用今天登录了。
当我们要统计一段时间内总共有多少用户登录访问量的时候,只需要把这几天的key拿出来进行BITOP or或运算,然后将得到的值再进行bitcount就可以得出一段时间内登录的总用户数。
二、list
redis的list数据类型,可以模拟出很多种数据结构如:栈,队列,数组,单播(FIFO)。将在下面学习list操作命令的时候结合起来
首先list是有顺序的,这个顺序是指存入list先后的顺序,我们可以脑补下list在内存中相当于是一个双向链表结构。redis的key中有两个指针,一个head指向前一个节点,一个tail指向后一个节点。
通过help @list可以看出list的命令大体可以分为两大类,L开头表示left 左边的意思,R开头表示right右边的意思。
#从左边一次一个压进去 比如下面的在内存中的表示如下
# g f e d c b a
127.0.0.1:6379> LPUSH k0 a b c d e f g
(integer) 7
# 根据下面取出的结果,我们得到一个结论
# 同向命令 相当于 栈
127.0.0.1:6379> LRANGE k0 0 -1
1) "g"
2) "f"
3) "e"
4) "d"
5) "c"
6) "b"
7) "a"
#从右边一次一个压进去 比如下面的在内存中的表示如下
# a b c d e f
127.0.0.1:6379> RPUSH k1 a b c d e f g
(integer) 7
# 根据下面取出的结果,我们得到一个结论
# 反向命令 相当于 队列(FIFO)
127.0.0.1:6379> LRANGE k1 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
6) "f"
7) "g"
# 根据下标获取数据,这里相当于 “数组“ 数据结构
127.0.0.1:6379> LINDEX k0 0
"g"
# 只有当key存在 下面命令才执行成功 值才会添加进对应key的list中
127.0.0.1:6379> LPUSHX k0 aaaa
(integer) 8
# 删除指定key中数据,从左边开始删除指定个数。
127.0.0.1:6379> LREM k0 1 aaaa
(integer) 1
# lpop移除并拿到第一个值
127.0.0.1:6379> LPUSH k0 a b c d e f g
(integer) 7
127.0.0.1:6379> LPOP k0
"g"
# 查看list长度
127.0.0.1:6379> LLEN k0
(integer) 6
list中的命令还包括阻塞式的命令
# 当我们k0中还有数据的时候,就能得道k0弹出的值是f
127.0.0.1:6379> BLPOP k0 0
1) "k0"
2) "f"
# 当我们要获取一个不存在的key中值的时候,如果不存在并且我们设置超时时间是0(表示永远不超时),redis中超时单位是秒
127.0.0.1:6379> BLPOP k1 0
# 我们另外开个redis客户端给k1设置值后,他才能弹出值
# 为了展示单播(FIFO)的数据,我们多开几个客户端阻塞等待k1,当我们设置k1的值后
#第一个阻塞等待k1的客户端将获取到值打印在屏幕(其实我一直觉得单播和队列挺像的)
[root@localhost ~]# redis-cli
127.0.0.1:6379> LPUSH k1 aaa
(integer) 1
27.0.0.1:6379> BLPOP k1 0
1) "k1"
2) "aaa"
(190.59s)
三、hash
hash这种数据结构,相当于java中的hashmap,就是一个kv键值对的集合。主要用途是保存一些对象属性的值。
比如我们设计需要存储用户信息则可惜这么设计redis key为userId value为hash,里面存储name=zs ,age=18等。
其实hash的使用方式和string类型的使用方式基本一样,只是hash类型的命令前面多了h。
# 新增
127.0.0.1:6379> HSET k0 name zs
(integer) 1
# 获取
127.0.0.1:6379> HGET k0 name
"zs"
#批量设置key值
127.0.0.1:6379> HMSET k0 name zs age 18
OK
# 批量获取值
127.0.0.1:6379> HMGET k0 name age
1) "zs"
2) "18"
# 获取key所有的kv
127.0.0.1:6379> HGETALL k0
1) "name"
2) "zs"
3) "age"
4) "18"
# 获取对应key 的所有的key(有点绕,就是获取redis key对应的hash中所有的key值)
127.0.0.1:6379> HKEYS k0
1) "name"
2) "age"
# 获取key 所有values值(有点绕,就是获取redis key对应的hash中所有的value值)
127.0.0.1:6379> HVALS k0
1) "zs"
2) "18"
四、set
set类型和list类型很像,但是set与list相比,set是无序且唯一,要求去重的话只能用set 类型
# 增加
127.0.0.1:6379> sadd k0 a b c d a
(integer) 4
# 查看到a去重,只留下一个,无序
127.0.0.1:6379> SMEMBERS k0
1) "c"
2) "b"
3) "a"
4) "d"
# 统计set集合数量
127.0.0.1:6379> SCARD k0
(integer) 4
# 判断给定的值是否在set 集合中已经出现过 返回1 表示存在 0 表示不存在
127.0.0.1:6379> SISMEMBER k0 a
(integer) 1
# 删除集合某个成员
127.0.0.1:6379> SREM k0 d
(integer) 1
127.0.0.1:6379> SADD k0 d e f g
(integer) 4
127.0.0.1:6379> SMEMBERS k0
1) "f"
2) "e"
3) "a"
4) "b"
5) "c"
6) "d"
7) "g"
# 随机删除集合中三个成员,并返回删除的三个成员。
127.0.0.1:6379> SPOP k0 3
1) "b"
2) "f"
3) "d"
127.0.0.1:6379> SMEMBERS k0
1) "e"
2) "a"
3) "c"
4) "g"
set 类型还可以进行集合的运算,交集并集,差集
先FLUSHALL清除数据下
127.0.0.1:6379> SADD k0 1 2 3 4 5
(integer) 5
127.0.0.1:6379> SADD k1 4 5 6 7 8
(integer) 5
#交集,并把结果打印出来
127.0.0.1:6379> SINTER k0 k1
1) "4"
2) "5"
# 交集,并把数据存储在另一个key中,下面的差集和并集我就不演示对应带又store的命令,
# 因为带又store的命,是把数据存储成另一个key
127.0.0.1:6379> SINTERSTORE ik k0 k1
(integer) 2
127.0.0.1:6379> SMEMBERS ik
1) "4"
2) "5"
# 差集 = k0的数据减去k0和k1的交集
127.0.0.1:6379> SDIFF k0 k1
1) "1"
2) "2"
3) "3"
# 差集=k1 的数据减去k0和k1的交集
127.0.0.1:6379> SDIFF k1 k0
1) "6"
2) "7"
3) "8"
# 并集
127.0.0.1:6379> SUNION k0 k1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
set 类型的常见用途
主要利用他的随机事件。可以用来做抽奖,下面我们的例子都是建立在抽奖人数远远大于奖品数量的前提下。(因为如果奖品远大于抽奖人,我们要实现单次抽奖不重复将实现不了,将会有奖品剩余)
抽奖分成两种,第一种是当你抽中后,就再也不能参加下面的抽奖。第二种是可以重复抽奖
第一种,我们可以使用set 中命令
127.0.0.1:6379> SADD k0 a b c d e f g
(integer) 7
127.0.0.1:6379> SPOP k0
"c"
127.0.0.1:6379> SMEMBERS k0
1) "f"
2) "a"
3) "b"
4) "e"
5) "d"
6) "g"
第二种,可重复抽奖。这里又分为两种,因为我们单次的抽奖可以分为可重复和不可重复
我们需要了解set当中一个命令
其中count 有三种情况 正数 0 负数
正数表示数据不可重复
0 返回数据为空
负数表示数据可以重复
所以对应我们的单次抽奖重复的情况
# 随机抽出3名,那么有可能重复,也有可能不重复
SRANDMEMBER k0 -3
单次抽奖不重复
127.0.0.1:6379> SRANDMEMBER k0 3
五、sorted_set
理论上sorted_set也是set的一种,但是 sorted_set实现了排序的功能,这里的排序是集合内的数据进行排序,和list是存入的顺序排序不一样。这里很多命令,其实和上面的很像。
因为sorted_set要进行排序,那就引申出一个问题,根据什么维度进行排序呢?所以redis出现score分值这个概念,redis根据用户给各个成员的分值情况进行排序,如果分值一样,则按照字典序排序。
#新增
127.0.0.1:6379> ZADD k0 1 a 2 b 3 c 4 d
(integer) 4
# 根据分值,查询出所有数据,从低到高排序
127.0.0.1:6379> ZRANGE k0 0 -1 withscores
1) "a"
2) "1"
3) "b"
4) "2"
5) "c"
6) "3"
7) "d"
8) "4"
# 根据分值,查询出所有数据,从高到低排序
127.0.0.1:6379> ZREVRANGE k0 0 -1 withscores
1) "d"
2) "4"
3) "c"
4) "3"
5) "b"
6) "2"
7) "a"
8) "1"
# 删除
127.0.0.1:6379> ZREM k0 a
(integer) 1
sorted_set也支持集合操作。并集和交集
交集
# 可以看出 主要麻烦的是sort_set有分值的概念,这里需要如何处理
# 从图片中灰色的描述看出,要么求和,最小,最大。默认是求和的
127.0.0.1:6379> help ZINTERSTORE
127.0.0.1:6379> ZADD k0 80 jack 90 tom 100 kang
(integer) 3
127.0.0.1:6379> ZADD k1 70 hong 80 jack 90 kang
(integer) 3
# 求交集,最大和最小我就不演示,把sum 换成max 或者min就行了
127.0.0.1:6379> ZINTERSTORE ik 2 k0 k1 aggregate sum
(integer) 2
127.0.0.1:6379> ZRANGE ik 0 -1 withscores
1) "jack"
2) "160"
3) "kang"
4) "190"
并集
127.0.0.1:6379> help ZUNIONSTORE
127.0.0.1:6379> ZUNIONSTORE uk 2 k0 k1 aggregate max
(integer) 4
127.0.0.1:6379> ZRANGE uk 0 -1 withscores
1) "hong"
2) "70"
3) "jack"
4) "80"
5) "tom"
6) "90"
7) "kang"
8) "100"
这里在说下,redis的sorted_set为什么排序这么快? 在面试中经常会被问到。
理由是redis使用的是skip list跳跃表的数据结构,所以速度就快。如果不用跳跃表,那么只能存一个数据就挨个和redis中已经存在的数据进行比较,这个复杂度就是O(n)了
好,那什么是跳跃表呢?
一图胜前言
这个过程不好用语言描述,有点类似平衡二叉树。当我们需要插入数据18,那么我们从第一层开始比较,发现18比11 大,所以我们跳到第二层和22 比较,发现比22小,然后我们调到第三层和11比较,发现比11大,所以18的位置就再11和22之间。
至于上面例子中18会不会造层,说不一定,是随机的。
总结
好,至此redis常见的五种数据类型及常用的命令介绍完毕,今后看redis相关的高级API,也应该能想到对应原生redis哪个命令。