Redis数据类型
# Redis数据类型
Redis是一个Key-Value的存储系统,使用C语言编写。Key的类型是字符串,我们常说的Redis数据类型值的是Value的数据类型:
- string字符串类型、list列表类型、set集合类型、sortedset(zset)有序集合类型、hash类型。
- bitmap位图类型、geo地理位置类型。
- stream类型(Redis5.0新增)
在redis-cli
中可以使用help @<group>
命令,可以看到 redis 对这些类型指令分组,group通常有generic、string、set、sorted_set等等,恰好是上面的类型,所以平时可以用这个指令去查阅,此外,针对不同的类型的基本操作都有提供指令给我们,根据这个特性去记忆会非常方便。(注意,bitmap被归类为string)
127.0.0.1:6379> help
redis-cli 5.0.5
To get help about Redis commands type:
"help @<group>" to get a list of commands in <group>
"help <command>" for help on <command>
"help <tab>" to get a list of possible help topics
"quit" to exit
To set redis-cli preferences:
":set hints" enable online hints
":set nohints" disable online hints
Set your preferences in ~/.redisclirc
下文只会列出常见的指令,其他的指令可以自己使用help
指令帮助。
注意:Redis中命令是忽略大小写的,例如:set 等同于 SET,但是key是不忽略大小写!
# key的设计
Redis中的key不忽略大小写。日常开发中,建议key的设计使用这种格式: <table>:<primarykey>:<column>
,说明:
- 使用“:”分割,
- table:把表名转换为key前缀, 比如:
user:
; - primarykey:表主键值
- column:列名
比如:有用户表user,转换为redis的key-value存储
id | username | |
---|---|---|
1 | zhangsan | zhangsan@qq.com |
- username 的 key:
user:1:zhangsan
- email 的 key:
user:1:email
# string字符串类型
redis的string类型能够表达3种值:字符串、整数、浮点数。常见操作命令如下表:
命令 | 说明 |
---|---|
set [key value] | 赋值 |
get [key] | 取值 |
getset [key value] | 赋值,并返回旧值。 |
set [key, value nx|xx] | nx:新建赋值,要求key不存在,由它新建;xx:更新值,要求key存在 原子操作 |
setnx [key, value] | 新建赋值,同上 |
setex [key, time, value] | 赋值,设定过期时间毫秒 |
mset [key1 value1 ...] | 批量设置 |
mget [key1 key2 …] | 返回库中多个 string 的 value。 |
msetnx [key value ...] | 新建赋值的批量操作,原子操作 |
append [key value] | 名称为 key 的 string 的值附加 value |
substr [key start end] | 返回名称为 key 的 string 的 value 的子串 |
incr [key] | 增 1 操作 |
incrby [key integer] | 增加 integer |
decr [key] | 减 1 操作 |
decrby [key integer] | 减少 integer |
strlen | 长度,涉及到对象的编码,redis的二进制安全 |
有人可能会疑惑?Redis是怎么区分存放的是字符串还是整数?
- 这涉及到Redis的底层数据结构,对于每种类型Redis都是用RedisObject结构体封装的,里面封装了
type
字段以及encoding
字段。我们常说的类型更多的是看它的encoding
。 - 通过
type key
,通过读取 RedisObject 的type
字段获得对象的类型。 - 通过
object encoding
命令,可以查看对象采用的编码方式。
127.0.0.1:6379> set k1 zhangsan
OK
127.0.0.1:6379> type k1
string
127.0.0.1:6379> object encoding k1
"embstr"
127.0.0.1:6379> set k2 100
OK
127.0.0.1:6379> type k2
string
127.0.0.1:6379> object encoding k2
"int"
# 应用场景
incr可用于实现乐观锁,业务应用有:抢购、秒杀、点赞,评论等,
规避并发下,对数据库的事务操作完全由redis内存操作代替
setnx可用于实现分布式锁,当value不存在时采用赋值
# bitmap位图类型
由于Redis将它的指令归类为了string
这里接着讲bitmap。
bitmap是Redis提供的进行位操作的数据类型,可以极大的节省储存空间。
key可以用来表示某个元素,而每个bit位可以用来表示元素对应的值或者状态,底层结构:
常见操作命令如下:
命令 | 说明 |
---|---|
setbit key offset value | 设置key在offset处的bit值[只能是0或者 |
getbit key offset | 获得key在offset处的bit值 |
bitcount key | 获得key的bit位为1的个数 |
bitpos key value | 返回第一个被设置为bit值的索引值 |
bitop and[or/xor/not] destkey key | 对多个key 进行逻辑运算后存入destkey |
# 应用场景
统计用户登录/签到天数。日期作为偏移量,1表示签到
setbit user:01:sign 1 1 # 一年第二天登录 setbit user:01:sign 7 1 # 一年第八天登录 setbit user:01:sign 364 1 # 一年最后一天登录 127.0.0.1:6379> BITCOUNT user:01:sign # 一年登陆天数 [integer] 3
127.0.0.1:6379> setbit user:sign:1000 20200101 1 #id为1000的用户20200101签到 [integer] 0 127.0.0.1:6379> setbit user:sign:1000 20200103 1 #id为1000的用户20200103签到 [integer] 0 127.0.0.1:6379> getbit user:sign:1000 20200101 #获得id为1000的用户20200101签到状态 1 表示签到 [integer] 1 127.0.0.1:6379> getbit user:sign:1000 20200102 #获得id为1000的用户20200102签到状态 0表示未签到 [integer] 0 127.0.0.1:6379> bitcount user:sign:1000 #获得id为1000的用户签到次数 [integer] 2 127.0.0.1:6379> bitpos user:sign:1000 1 #id为1000的用户第一次签到的日期 [integer] 20200101
统计活跃用户。日期为key,用户id为偏移量,1表示活跃
查询用户在线状态。日期为key,用户id为偏移量,1表示在线
127.0.0.1:6379> setbit 20200201 1000 1 #20200201的1000号用户上线 [integer] 0 127.0.0.1:6379> setbit 20200202 1001 1 #20200202的1000号用户上线 [integer] 0 127.0.0.1:6379> setbit 20200201 1002 1 #20200201的1002号用户上线 [integer] 0 127.0.0.1:6379> bitcount 20200201 #20200201的上线用户有2个 [integer] 2 127.0.0.1:6379> bitop or desk1 20200201 20200202 #合并20200201的用户和20200202上线了的用户 [integer] 126 127.0.0.1:6379> bitcount desk1 #统计20200201和20200202都上线的用 户个数 [integer] 3
# list列表类型
redis的list列表可以存储有序、可重复的元素,底层是用双向链表实现,获取头部或尾部附近的记录是极快的。
学过算法的可以知道,双向链表通过操作的配合可以实现栈、队列的功能,此外list列表还支持阻塞队列、类似数组的一些指令。
常见指令如下:
命令 | 说明 |
---|---|
lpush [key value ......] | 从左侧插入列表 |
rpush [key value ......] | 从右侧插入列表 |
lpop [key] | 从列表左侧取出第一个元素 |
rpop [key] | 从列表右侧取出第一个元素 |
lrange [key start, end] | 返回名称为 key 的 list 中 start 至 end 之间的元素 |
lindex [key index] | 返回名称为 key 的 list 中 index 位置的元素 |
lset [key index value] | 将列表index位置的元素设置成value的值 |
linsert [key BEFORE/AFTER pivot value] | 将value插入到列表,且位于值pivot之前或之后 |
lrem [key count value] | 删除列表中与value相等的元素 当count>0时,lrem会从列表左边开始删除值为value的元素; 当count<0时,lrem会从列表后边开始删除值为value的元素; 当count=0时,lrem删除所有值为value的元素 |
ltrim [key start end] | 截取 list,只保留start到end区间 |
llen [key] | 返回 list 的长度 |
blpop [key1 key2 .... timeout] | lpop 命令的 block 版本。 从列表左侧取出,当列表为空时阻塞,可以设置最大阻塞时间,单位为秒 |
brpop [key1 key2 … timeout] | rpop 命令的 block 版本。 从列表右侧取出,当列表为空时阻塞,可以设置最大阻塞时间,单位为秒 |
rpoplpush [srckey dstkey] | 从srckey列表右侧弹出,并插入到dstkey列表左侧 |
brpoplpush [srckey dstkey] | 从srckey列表右侧弹出,并插入到dstkey列表左侧,会阻塞。 |
# 应用场景
- 作为栈和队列使用:栈,同向命令;队列,反向命令
- 应用缓存各种列表。例如商品列表、评论列表
# hash类型(散列表)
Redis的hash是一个string 类型的field和value 的映射表,它提供了字段和字段值的映射。相当于在key-value的value又嵌套了一个key-value集合。
常见命令如下:
命令 | 说明 |
---|---|
hset [key field value] | 赋值,不区别新增和修改 |
hget [key field] | 获取一个字段值 |
hmset [key1 field1 key 2 field2 ...] | 批量赋值 |
hmget [key field1 field2 ...] | 批量获取字段值 |
hgetall [key] | 获取所有的键field及其对应的 value |
hkeys [key] | 获取所有的键fields |
hvals [key] | 获取所有的value |
hincrby [key field integer] | 指定字段增加 integer(可以是负数) |
hincrbyfloat [key field float] | 指定字段增加 float(可以是负数) |
hexists [key field] | 指定字段是否存在 |
hdel [key field] | 删除指定字段 |
hlen [key] | 获取字段数量 |
# 应用场景
- 对象的存储
- 表数据的映射
- 对field进行数值计算场景:点赞,收藏,详情页
# set集合类型
Redis的set,同Java的Set定义差不多,他可以存储无序的唯一元素。
redis的set主要提供集合的操作,例如取交集、并集、差集的指令,可以使用带store
的指令,让结果指向新的key。
还提供了随机事件。
命令 | 说明 |
---|---|
sadd [key member1 member2 ......] | 为集合添加新的成员 |
smembers [key] | 返回集合所有成员 |
srem [key member1 member2 ......] | 删除指定成员 |
spop [key] | 返回集合中一个随机元素,并将该元素删除 |
srandmember [key count] | 返回集合中一个随机元素,但不会删除该元素 count > 0:取出一个去重的结果集(不能超过已有集) count < 0:取出一个带重复的结果集,一定满足你要的数量 count = 0,不返回 |
smove [srckey dstkey member] | 迁移srckey集合指定元素member到dstkey |
scard [key] | 返回集合元素数量 |
sismember [key member] | 判断指定元素是否存在 |
sinter [key1 key2 …] | 求多集合交集 |
sinterstore [dstkey key1 key2 ......] | 求多集合交集并保存到 dstkey 的集合 |
sunion [key1 key2 …] | 求多集合并集 |
sunionstore [dstkey key1 key2 ......] | 求多集合并集并保存到 dstkey 的集合 |
sdiff [key1 key2 …] | 求差集, 前后 key 顺序不同,结果不同 |
sdiffstore [dstkey key1 key2 ......] | 求多集合差集并将差集保存到 dstkey 的集合 |
# 应用场景
- 存储不能重复的且不需要顺序的数据结构
- 随机事件,根据时间场景,选择srandmember、spop。例如关注的用户可以通过spop进行随机抽奖
# sortedset有序集合类型
sortedset,又称zset,是Redis提供的有序唯一集合。
为什么又称为zset?因为相关指令都是zxxx开头。不使用s开头是因为string类型占用了,redis就直接取26个字母的最后一个z命名。
与set相比,zset的关注点是快速排序:元素本身是无序的,redis给每个元素关联了一个score
,物理内存按照左小右大进行排序(不随命令zrang、zrevrang发生变化)。底层实现是跳跃表。
# 物理内存按照左小右大进行排序,假如有一个如下的分数排名
127.0.0.1:6379> zrange math_score 0 -1 withscores
1) "zhangsan"
2) "80"
3) "wangwu"
4) "90"
5) "lisi"
6) "99"
# 反序
127.0.0.1:6379> zrevrange math_score 0 -1 withscores
1) "lisi"
2) "99"
3) "wangwu"
4) "90"
5) "zhangsan"
6) "80"
# 想要获得倒数第一第二名,分数从大到小的排名,理想结果应该是 lisi - > wangwu,结果却是如下
# 可以理解为 redis是从反序中拿出了 倒数第一二个
127.0.0.1:6379> zrevrange math_score -2 -1 withscores
1) "wangwu"
2) "90"
3) "zhangsan"
4) "80"
# 正确写法
127.0.0.1:6379> zrange math_score -2 -1 withscores
1) "wangwu"
2) "90"
3) "lisi"
4) "99"
此外,zset还提供了集合的操作(交集、并集),同样支持store
,还附加了权重、聚合的指令选择。在上面数学的分数上,增加语文的分数,进行累加
127.0.0.1:6379> zrange chinese_score 0 -1 withscores
1) "lisi"
2) "80"
3) "wangwu"
4) "80"
5) "zhangsan"
6) "90"
127.0.0.1:6379> zunionstore total 2 math_score chinese_score weights 1 1.2
(integer) 3
127.0.0.1:6379> zrange total 0 -1 withscores
1) "wangwu"
2) "186"
3) "zhangsan"
4) "188"
5) "lisi"
6) "195"
常见指令如下:
命令 | 说明 |
---|---|
zadd [key score1 member1 score2 member2 ......] | 为有序集合添加成员 |
zrem [member1 member2 .....] | 删除有序集合中指定成员 |
zcard [key] | 获得有序集合中的元素数量 |
zcount [key min max] | 返回集合中score值在[min,max]区间 |
zincrby [key increment member] | 在集合的member分值上加increment |
zscore [key member] | 获得集合中member的分值 |
zrank [key member] | 获得集合中member的排名(按分值从小到大) |
zrevrank [key member] | 获得集合中member的排名(按分值从大到小) |
zrange [key start end withscores] | 获得集合中指定区间成员,按分数递增排序,可以添加withscores显示分数。 |
zrevrange [key start end withscores] | 获得集合中指定区间成员,按分数递减排序,可以添加withscores显示分数。 |
zinterstore deskey numkeys key [key ...] [weights weight] [aggregate sum|min|max] | 取得集合中的交集,deskey存储并集结果,numkeys为集合的数量,weights为权重数量,如果使用了权重,那么两个要一一对应。默认使用sum的方式统计 |
zunionstore deskey numkeys key [key ...] [weights weight] [aggregate sum|min|max] | 取得集合中的并集,deskey存储并集结果,numkeys为集合的数量,weights为权重数量,如果使用了权重,那么两个要一一对应。默认使用sum的方式统计 |
# 应用场景
- 各种排行榜。比如:点击排行榜、销量排行榜、关注排行榜等。
# geo地理位置类型
geo是Redis用来处理位置信息的。在Redis3.2中正式使用。主要是利用了Z阶曲线、Base32编码和geohash算法。
# Z阶曲线
在x轴和y轴上将十进制数转化为二进制数,采用x轴和y轴对应的二进制数依次交叉后得到一个六位数编码。把数字从小到大依次连起来的曲线称为Z阶曲线,Z阶曲线是把多维转换成一维的一种方法。
# Base32编码
Base32这种数据编码机制,主要用来把二进制数据编码成可见的字符串,其编码规则是:任意给定一个二进制数据,以**5个位(**bit)为一组进行切分(base64以6个位(bit)为一组),对切分而成的每个组进行编码得到1个可见字符。Base32编码表字符集中的字符总数为32个(0-9、b-z去掉a、i、l、o),这也是Base32名字的由来。
# geohash算法
Gustavo在2008年2月上线了geohash.org网站。Geohash是一种地理位置信息编码方法。 经过geohash映射后,地球上任意位置的经纬度坐标可以表示成一个较短的字符串。可以方便的存储在数据库中,附在邮件上,以及方便的使用在其他服务中。以北京的坐标举例,[39.928167,116.389550]可以转换成 wx4g0s8q3jf9 。
Redis中经纬度使用52位的整数进行编码,放进zset中,zset的value元素是key,score是GeoHash的52位整数值。在使用Redis进行Geo查询时,其内部对应的操作其实只是zset(skiplist)的操作。通过zset的score进行排序就可以得到坐标附近的其它元素,通过将score还原成坐标值就可以得到元素的原始坐标。
常见操作命令如下表:
命令 | 说明 |
---|---|
geoadd key 经度1 纬度1 成员名称1 经度2 纬度2 成员名称2... | 添加地理坐标 |
geohash key 成员名称1 成员名称2 ... | 返回标准的geohash串 |
geopos key 成员名称1 成员名称2 ... | 返回成员经纬度 |
geodist key 成员1 成员2 单位 | 返回成员经纬度 |
zincrby [key increment member] | 在集合的member分值上加increment |
georadiusbymember key 成员 值单位 m|km|ft|mi count数 asc[desc] [store key] [storedist key] | 根据成员查找附近的成员 |
# 应用场景
- 记录地理位置
- 计算距离
- 查找"附近的人"
举例
# 添加zhangsan、wangwu、lisi用户地址
127.0.0.1:6379> geoadd user:addr 116.31 40.05 zhangsan 116.38 39.88 wangwu 116.47 40.00 lisi
(integer) 3
# 获取geohash码
127.0.0.1:6379> geohash user:addr zhangsan wangwu lisi
1) "wx4eydyk5m0"
2) "wx4fb7cmfe0"
3) "wx4gd3fbgs0"
# 获取经纬度
127.0.0.1:6379> geopos user:addr zhangsan wangwu lisi
1) 1) "116.31000012159347534"
2) "40.04999982043828055"
2) 1) "116.38000041246414185"
2) "39.88000114172373145"
3) 1) "116.46999925374984741"
2) "39.99999991084916218"
#计算zhangsan到wangwu的距离,默认单位是m,可以追加单位km|ft|mi
127.0.0.1:6379> geodist user:addr zhangsan wangwu
"19827.6139"
# 获得距离zhangsan 20km以内的按由近到远的前三名的成员名称、距离及经纬度
127.0.0.1:6379> georadiusbymember user:addr zhangsan 20 km withcoord withdist count 3 asc
1) 1) "zhangsan"
2) "0.0000"
3) 1) "116.31000012159347534"
2) "40.04999982043828055"
2) 1) "lisi"
2) "14.7187"
3) 1) "116.46999925374984741"
2) "39.99999991084916218"
3) 1) "wangwu"
2) "19.8276"
3) 1) "116.38000041246414185"
2) "39.88000114172373145"
# stream数据流类型
stream是redis 5.0后新增的数据结构,用于可持久化的消息队列。几乎满足了消息队列具备的全部内容,包括:消息ID的序列化生成、消息遍历、消息的阻塞和非阻塞读取、消息的分组消费、未完成消息的处理、消息队列监控。
每个Stream都有唯一的名称,它就是Redis的key,首次使用 xadd 指令追加消息时自动创建。常见操作命令如下表:
命令 | 说明 |
---|---|
xadd key ID field string [field string ...] | 将指定消息数据(field string)追加到指定队列(key)中,ID,如果是ß*表示最新生成的id(当前时间+序列号) |
xread [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] | 从消息队列中读取,COUNT:读取条数,BLOCK:阻塞读(默认不阻塞)key:队列 名称 id:消息id |
xrange key start end [COUNT] | 读取队列中给定ID范围的消息 COUNT:返回消息条数(消息id从小到大) |
xrevrange key start end [COUNT] | 读取队列中给定ID范围的消息 COUNT:返回消息条数(消息id从大到小) |
xdel key id | 删除队列的消息 |
xgroup create key groupname id | 创建一个新的消费组 |
xgroup destory key groupname | 删除指定消费组 |
xgroup delconsumer key groupname cname | 删除指定消费组中的某个消费者 |
xgroup setid key id | 修改指定消息的最大id |
xreadgroup group groupname consumer COUNT streams key | 从队列中的消费组中创建消费者并消费数据(consumer不存在则创建) |
# 应用场景
- 还能是啥,就是消息队列咯
举例
127.0.0.1:6379> xadd topic:001 * name zhangsan age 23 # 追加到指定队列
"1648490332084-0"
127.0.0.1:6379> xadd topic:001 * name lisi age 24 name wangwu age 16 #追加到指定队列
"1648490437123-0"
127.0.0.1:6379> xrange topic:001 - + # 读取队列,从小到达排序
1) 1) "1648490332084-0"
2) 1) "name"
2) "zhangsan"
3) "age"
4) "23"
2) 1) "1648490437123-0"
2) 1) "name"
2) "lisi"
3) "age"
4) "24"
5) "name"
6) "wangwu"
7) "age"
8) "16"
# 从消息队列topic:001中读取1条消息
127.0.0.1:6379> xread COUNT 1 streams topic:001 0
1) 1) "topic:001"
2) 1) 1) "1648490332084-0"
2) 1) "name"
2) "zhangsan"
3) "age"
4) "23"
#创建的group1
127.0.0.1:6379> xgroup create topic:001 group1 0
OK
# 创建cus1消费者加入到group1消费组,消费第一条
127.0.0.1:6379> xreadgroup group group1 cus1 count 1 streams topic:001 >
1) 1) "topic:001"
2) 1) 1) "1648491148138-0"
2) 1) "name"
2) "zhangsan"
3) "age"
4) "23"
#继续消费 第二条
127.0.0.1:6379> xreadgroup group group1 cus1 count 1 streams topic:001 >
1) 1) "topic:001"
2) 1) 1) "1648491157235-0"
2) 1) "name"
2) "lisi"
3) "age"
4) "24"
5) "name"
6) "wangwu"
7) "age"
8) "16"
#继续消费, 没有数据了
127.0.0.1:6379> xreadgroup group group1 cus1 count 1 streams topic:001 >
(nil)