前言
Redis拥有丰富的数据结构,了解这些数据结构的常用命令以及在合适的场景选用合适的数据结构会让我们使用Redis事半功倍
String
String是Redis中最常用的数据结构,key-value中value不仅可以是字符串,也可以是整数,下面是常用的命令
127.0.0.1:6379> set k1 1
OK
# 必须是数字才能加减,incr表示加1
127.0.0.1:6379> incr k1
(integer) 2
# decr表示减1
127.0.0.1:6379> decr k1
(integer) 1
# incrby加指定数字
127.0.0.1:6379> incrby k1 10
(integer) 11
# decrby减指定数字
127.0.0.1:6379> decrby k1 2
(integer) 9
127.0.0.1:6379> set k2 abc
OK
# 获取字符串长度
127.0.0.1:6379> strlen k2
(integer) 3
# 字符串后追加,redis有空间预分配策略,append后总长度len+addlen<1MB,按新长度的两倍扩容
# append后总长度len+addlen>1MB,按新长度加上1MB扩容
127.0.0.1:6379> append k2 def
(integer) 6
127.0.0.1:6379> get k2
"abcdef"
# 批量添加
127.0.0.1:6379> mset k3 v3 k4 v4 k5 v5
OK
# 批量获取,key不存在则返回nil
127.0.0.1:6379> mget k3 k4 k6
1) "v3"
2) "v4"
3) (nil)
# 无则添加
127.0.0.1:6379> setnx k6 v6
(integer) 1
127.0.0.1:6379> get k6
"v6"
# 无则添加,有则更新,这个命令还能更新过期时间
127.0.0.1:6379> setex k7 20 v7
OK
127.0.0.1:6379> ttl k7
(integer) 16
127.0.0.1:6379> set k7 abcdefg
OK
# 截取字符串
127.0.0.1:6379> getrange k7 1 3
"bcd"
# 重写字符串
127.0.0.1:6379> setrange k7 4 abc
(integer) 7
127.0.0.1:6379> get k7
"abcdabc"
127.0.0.1:6379> setrange k7 4 abcd
(integer) 8
127.0.0.1:6379> get k7
"abcdabcd"
Hash
Redis中的Hash跟Java中的HashMap是差不多的,常用来关联一个key的多种数据,比如某个用户的姓名、年龄,下面是常用的命令
# hash写入
127.0.0.1:6379> hset user name zhangsan age 24
(integer) 2
# hash获取某个field
127.0.0.1:6379> hget user name
"zhangsan"
127.0.0.1:6379> hget user age
"24"
# 删除某个field
127.0.0.1:6379> hdel user name
(integer) 1
127.0.0.1:6379> hgetall user
1) "age"
2) "24"
# 查询某个key有多少组value
127.0.0.1:6379> hlen user
(integer) 1
127.0.0.1:6379> hset user name zhangsan
(integer) 1
127.0.0.1:6379> hlen user
(integer) 2
# 查看该field是否存在
127.0.0.1:6379> hexists user age
# 存在返回1
(integer) 1
127.0.0.1:6379> hexists user agee
# 不存在返回0
(integer) 0
# 查看所有key
127.0.0.1:6379> hkeys user
1) "age"
2) "name"
# 查看所有value
127.0.0.1:6379> hvals user
1) "24"
2) "zhangsan"
List
Redis中的List就是一个双向链表,常用来做异步队列
# 从左边入进入链表
127.0.0.1:6379> lpush list 2 1 3
(integer) 3
# 从右边进入链表
127.0.0.1:6379> rpush list 4
(integer) 4
# 左边出链表
127.0.0.1:6379> lpop list 1
1) "3"
# 右边出链表
127.0.0.1:6379> rpop list 1
1) "4"
# 获取list的长度
127.0.0.1:6379> llen list
(integer) 2
# 按照索引下标获得元素
127.0.0.1:6379> lindex list 0
"1"
# 返回所有数据
127.0.0.1:6379> lrange list 0 -1
1) "1"
2) "2"
# 返回数据列表
127.0.0.1:6379> lrange list 0 0
1) "1"
# 重点讲讲lrem这个命令
# 我们可以看到执行lrem list 2 3这个命令之后,删除了左边的两个3
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "3"
3) "3"
4) "6"
5) "5"
6) "4"
7) "3"
127.0.0.1:6379> lrem list 2 3
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "6"
3) "5"
4) "4"
5) "3"
# 我们可以看到执行lrem list -2 3这个命令之后,删除了右边的两个3
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "6"
3) "5"
4) "4"
5) "3"
6) "3"
7) "3"
127.0.0.1:6379> lrem list -2 3
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "6"
3) "5"
4) "4"
5) "3"
# 执行lrem list 0 3这个命令之后,不管左边、右边,都删
127.0.0.1:6379> lrange list 0 -1
1) "3"
2) "6"
3) "5"
4) "4"
5) "3"
127.0.0.1:6379> lrem list 0 3
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "6"
2) "5"
3) "4"
# 截取指定范围的值再赋给原key
127.0.0.1:6379> lrange list 0 -1
1) "6"
2) "5"
127.0.0.1:6379> ltrim list 0 0
OK
127.0.0.1:6379> lrange list 0 -1
1) "6"
# rpoplpush 从原list的右边取出一个元素push到另外一个list
127.0.0.1:6379> lrange list 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
127.0.0.1:6379> rpoplpush list list2
"6"
127.0.0.1:6379> lrange list2 0 -1
1) "6"
127.0.0.1:6379> lrange list 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
Set
Set我们可以理解为数学中的集合,集合中的元素是不会重复的
# 添加元素
127.0.0.1:6379> sadd set 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> smembers set
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
# 查看集合大小
127.0.0.1:6379> scard set
(integer) 6
# 集合中是否存在此member,1表示存在,0表示不存在
127.0.0.1:6379> sismember set 1
(integer) 1
127.0.0.1:6379> sismember set 10
(integer) 0
# 移除set中的元素
127.0.0.1:6379> srem set 1 2
(integer) 2
127.0.0.1:6379> smembers set
1) "3"
2) "4"
3) "5"
4) "6"
# 删除元素并返回
127.0.0.1:6379> sadd set 1 2 3 4 5 6
(integer) 6
127.0.0.1:6379> spop set 2
1) "5"
2) "6"
127.0.0.1:6379> smembers set
1) "1"
2) "2"
3) "3"
4) "4"
# 随机取集合中的元素,但是不删除
127.0.0.1:6379> srandmember set 1
1) "4"
127.0.0.1:6379> smembers set
1) "1"
2) "2"
3) "3"
4) "4"
5) "59"
set还提供了交集、并集、差集的命令
# 集合交集
127.0.0.1:6379> smembers set
1) "1"
2) "2"
3) "3"
4) "4"
5) "59"
127.0.0.1:6379> smembers set2
1) "2"
2) "3"
127.0.0.1:6379> sinter set set2
1) "2"
2) "3"
# 集合并集
127.0.0.1:6379> sunion set set2
1) "1"
2) "2"
3) "3"
4) "4"
5) "59"
# 集合差集
127.0.0.1:6379> sdiff set set2
1) "1"
2) "4"
3) "59"
应用场景
Set是一个集合,存储不重复的元素,根据这个特性我们可以用set来记录唯一性的数据
比如:某应用每天用户的签到记录、某应用每天用户的登录记录
Redis的Set还提供了交集、并集、差集的命令,我们可以使用这些命令计算很多指标
比如:我们想要获取某天用户的次日留存,只需要将某天的新增用户记录到一个set中,次日的活跃用户记录到另外一个set中,两个set取交集,便是次日留存
比如:我们想要获取连续7天登录的用户,只需要将每日登录的用户记录到一个set中,求连续7天的set的交集便是连续7天登录的用户
比如:我们想要获取某两个用户的共同好友,也是将两个用户的好友set求交集
Sorted Set
有序集合是面试的高频考点,常用来做排行榜,如等级排名、分数排名
# 添加元素
127.0.0.1:6379> zadd sset 10 user1
(integer) 1
127.0.0.1:6379> zadd sset 20 user2
(integer) 1
127.0.0.1:6379> zadd sset 30 user3
(integer) 1
# 获取有序集合中的所有元素,获取到的是有序的
127.0.0.1:6379> zrange sset 0 -1
1) "user1"
2) "user2"
3) "user3"
# 删除元素
127.0.0.1:6379> zrem sset user2
(integer) 1
127.0.0.1:6379> zrange sset 0 -1
1) "user1"
2) "user3"
# 查看玩家的分数
127.0.0.1:6379> zscore sset user3
"30"
# 查看分数前两名
127.0.0.1:6379> zrevrange sset 0 1 withscores
1) "user3"
2) "30"
3) "user2"
4) "20"
# 查看某个玩家的排名
127.0.0.1:6379> zrevrank sset user3
(integer) 0
127.0.0.1:6379> zrevrank sset user2
(integer) 1
# 增减玩家的分数
127.0.0.1:6379> zincrby sset 6 user2
"26"
127.0.0.1:6379> zrevrange sset 0 2 withscores
1) "user3"
2) "30"
3) "user2"
4) "26"
5) "user1"
6) "10"
应用场景
Sorted Set常用来做排行榜,比如积分排行榜,点赞排行榜
zadd key score member,只需要在score中写入积分,点赞数,member中写入用户名,即可完成一个排行榜
还有一个比较骚的操作,如果我们想要按照积分排行,在积分相等的情况下,按照点赞排行,你会怎么设计?
score是可以作为一个浮点数的,只要我们将score的整数位设置为积分数,小数位设置为点赞数,自然就能实现需求
HyperLogLog
# 增加计数
127.0.0.1:6379> pfadd user a
(integer) 1
127.0.0.1:6379> pfadd user b
(integer) 1
127.0.0.1:6379> pfadd user c
(integer) 1
# 获取计数
127.0.0.1:6379> pfcount user
(integer) 3
127.0.0.1:6379> pfadd user d
(integer) 1
127.0.0.1:6379> pfcount user
(integer) 4
应用场景
基数统计通常是用来统计一个集合中不重复元素的个数
比如有这样的一个需求,产品经理想要知道每天访问网站的uv,并且不需要知道用户明细,你会怎么实现?
看了上面的数据结构,也许你已经想到了一个简单的方案,使用一个独立set存储每一个访问过的用户的id,但是如果页面访问量非常大的话,这个set的大小也是非常惊人的,而且这个需求只需要知道最终结果,不需要知道明细,记录所有的明细就有点浪费了
这个时候我们可以使用Redis的HyperLogLog数据结构来解决这个问题,HyperLogLog提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是0.81%
GeoHash
地址坐标常使用GeoHash来实现
# 添加坐标
127.0.0.1:6379> geoadd company 116.48105 39.996794 juejin
(integer) 1
127.0.0.1:6379> geoadd company 116.514203 39.905409 ireader
(integer) 1
127.0.0.1:6379> geoadd company 116.489033 40.007669 meituan
(integer) 1
# 可以批量添加
127.0.0.1:6379> geoadd company 116.562108 39.787602 jd 116.334255 40.027400 xiaomi
(integer) 2
# 两个坐标之间的距离,km为单位
127.0.0.1:6379> geodist company juejin ireader km
"10.5501"
# 范围20公里以内最多3个元素按距离正排,不会排除自身
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc
1) "ireader"
2) "juejin"
3) "meituan"
# 根据定位查找 附近的车、附近的餐馆
127.0.0.1:6379> georadius company 116.489033 40.007669 20 km withdist count 3 asc
1) 1) "meituan"
2) "0.0001"
2) 1) "juejin"
2) "1.3878"
3) 1) "ireader"
2) "11.5746"
通用类
127.0.0.1:6379> setex k200 2000 v200
OK
# 对象类型
127.0.0.1:6379> type k200
string
# 获取过期时间
127.0.0.1:6379> ttl k200
(integer) 1993
# 更新过期时间
127.0.0.1:6379> expire k200 200
(integer) 1
127.0.0.1:6379> ttl k200
(integer) 197