引言
实际面试过程中更多看重的是对Redis相关数据结构的活学活用,同时也可能会引申出Redis相关底层数据结构原理的实现,笔者最近面试过程中对这块内容有点生疏,所以本文也是为了笔者个人查漏补缺所写。
文章内容很短,大家可放心食用,同时欢迎各位在评论区进行补充。
当value是字符串类型时,根据字符串格式的不同,可以分为三类:
但是不论是何种格式,底层都是采用字节数组形式存储,只不过是编码方式不同,如下图所示:
常见命令:
set k1 v1 [NX | EX expireTime ]
get k1
mset k1 v1 k2 v2
mget k1 k2
incr count (val类型必须是int类型)
incrby count 2
incrbyfloat floatCount 1.5
setnx k3 v3
setex k4 v4 expireTime
# string 中每个位的操作
SETBIT key offset value # 将指定位置设置为0或1
GETBIT key offset # 获取指定位置的bit值
BITCOUNT key [start end] # 统计bitmap中值为1的bit位数量
BITOP operation destkey key [key ...] # 对指定的多个 key 进行位操作,并将结果保存在 destkey 中。操作可以是 AND、OR、XOR、NOT
String数据结构的应用场景如下图所示,大家可以右键在新标签页中打开浏览:
这里针对String类型作为bitmap的应用场景进行特殊说明:
# setbit onlineUsers userId 1
> setbit onlineUsers 1 1
0
> setbit onlineUsers 2 1
0
> setbit onlineUsers 10 1
0
> setbit onlineUsers 25 1
0
> bitcount onlineUsers
4
> setbit online_one 1 1
0
> setbit online_two 1 1
0
> setbit online_two 2 1
0
> setbit online_two 3 1
0
> bitop and both_two_online online_one online_two
1
> bitcount both_two_online
1
# 签到 sign:userId:yyyy-MM
> setbit sign:1:2023-08 27 1
0
> setbit sign:1:2023-08 28 1
0
> setbit sign:1:2023-08 29 1
0
> bitcount sign:1:2023-08
3
当val类型为List时,其可能存在以下三种编码情况:
常见命令:
LPUSH key element ... # 向列表左侧插入一个或多个元素
LPOP key # 移除并返回列表左侧第一个元素,没有则返回nil
RPUSH key element ... # 向列表右侧插入一个或多个元素
RPOP key # 移除并返回列表右侧的第一个元素
LRANGE key start end # 返回该范围内的所有元素
BLPOP , BRPOP # 没有元素时超时阻塞等待 , 而不是直接返回nil
redis提供的List列表类型其实可以用来替换我们常见的三种数据结构:
当val类型为Hash时,可能存在以下几种编码情况:
String 和 Hash 数据结构都可以用来存储对象信息,对于String数据类型来说,通常存储JSON序列化后的对象信息,当需要频繁修改对象某个属性时,这种方式就不太合适了。
Hash类型可以针对某个属性单独修改,没有序列化,也无需修改整个对象,如果某个对象内部属性需要频繁修改,如: 商品价格,销量,关注数,评价数等,可以考虑hash类型。如果需要单独读取对象内部某几个属性,也可以考虑Hash类型。
当val类型为Set时,其底层编码有以下两种情况:
Redis中的Set数据结构具备什么特点:
常用命令:
sadd key member ...
srem key member ... # 删除
scard key # 返回set中元素的个数
sismember key member # 判断元素是否存在当前set中
smembers # 获取set中所有元素
sinter k1 k2 # 求k1和k2关联的两个set集合的交集
sdiff k1 k2 # 求差集
sunion k1 k2 # 求并集
set 和 zset 是面试重点,这两个数据结构大家要尤其注意其应用场景。
# 点赞此篇文章
> sadd like:t0001 u0001
(integer) 1
> sadd like:t0001 u0001
(integer) 0
> sadd like:t0001 u0002
(integer) 1
> sadd like:t0001 u0003
(integer) 1
# 返回当前文章点赞总数
> scard like:t0001
3
# 用户u0002取消点赞
> srem like:t0001 u0002
1
> scard like:t0001
2
# 判断用户u0002是否点赞了当前文章
> sismember like:t0001 u0002
(integer) 0
# 获取点赞当前文章的所有用户
> smembers like:t0001
1) "u0003"
2) "u0001"
# 为当前物品添加标签信息
> sadd tags:i5001 物美价廉
(integer) 1
> sadd tags:i5001 4K高清
(integer) 1
# 返回当前物品具备的所有标签信息
> smembers tags:i5001
1) "4K高清"
2) "物美价廉"
# 为每个商品添加标签
> sadd 苹果手机 价格昂贵
(integer) 1
> sadd 苹果手机 流畅好用
(integer) 1
> sadd 小米手机 价格实惠
(integer) 1
> sadd 小米手机 流畅好用
(integer) 1
> sadd 华为手机 价格适中
(integer) 1
> sadd 华为手机 流畅好用
(integer) 1
# 获取三者的共同点
> sinter 苹果手机 小米手机 华为手机
1) "流畅好用"
# 获取苹果手机相较于小米手机的不同点
> sdiff 苹果手机 小米手机
1) "价格昂贵"
# 获取小米手机相较于苹果手机的不同点
> sdiff 小米手机 苹果手机
1) "价格实惠"
# 获取苹果手机和小米手机的标签集合
> sunion 苹果手机 小米手机
1) "价格实惠"
2) "流畅好用"
3) "价格昂贵"
# 用户1关注了用户2,3,4
> sadd 1:follow 2
(integer) 1
> sadd 1:follow 3
(integer) 1
> sadd 1:follow 4
(integer) 1
# 用户2关注了用户3,1
> sadd 2:follow 3
(integer) 1
> sadd 2:follow 1
(integer) 1
# 用户2的粉丝有用户1
> sadd 2:fans 1
(integer) 1
# 用户1的粉丝有用户2
> sadd 1:fans 2
(integer) 1
# 求用户1和用户2共同关注的人
> sinter 1:follow 2:follow
1) "3"
# 求用户1可能认识的人 -- 需要从返回结果中移除用户1自身
> sdiff 2:follow 1:follow
1) "1"
# 求用户2可能认识的人 -- 需要从返回结果中移除用户2自身
> sdiff 1:follow 2:follow
1) "2"
2) "4"
当val类型为ZSet时,其具备以下特性:
ZSet结构可能存在两种编码类型:
Redis提供的ZSet功能很类似Java中的TreeMap:
常见命令:
zadd key score member
zrem key member
zscore key member # 获取每个member的分数
zcard key # 获取当前zset集合中的元素个数
zcount key min max # 统计score在指定范围内的所有元素个数
zincrby key incrment member # 为zset中某个member的socre增加指定大小
zrange key min max # 按照score排序后,根据索引获取指定范围内的元素
zrangebyscore key min max # 按照score排序后,根据score获取指定分数段内的元素
zdiff,zinter,zunion # 求差集,交集,并集
所有排名默认升序,如果要降序则在命令后添加REV即可。
# 记录2023-8-24的热点信息排行榜 , 依次添加每个信息 , 记录每个新闻的热点数
> zincrby hotNews:2023-8-24 30000 福岛核废水
30000.0
> zincrby hotNews:2023-8-24 25000 日本3天已连发5次地震
25000.0
> zincrby hotNews:2023-8-24 25000 ShowMaker谈LPL中单
25000.0
> zincrby hotNews:2023-8-24 23000 大脑在替熬夜负重前行
23000.0
> zincrby hotNews:2023-8-24 22000 俄罗斯海岸现恐怖怪鱼
22000.0
# 按照默认升序返回所有热点信息
> zrange hotNews:2023-8-24 0 -1
1) "俄罗斯海岸现恐怖怪鱼"
2) "大脑在替熬夜负重前行"
3) "ShowMaker谈LPL中单"
4) "日本3天已连发5次地震"
5) "福岛核废水"
# 按照降序返回
> zrevrange hotNews:2023-8-24 0 -1
1) "福岛核废水"
2) "日本3天已连发5次地震"
3) "ShowMaker谈LPL中单"
4) "大脑在替熬夜负重前行"
5) "俄罗斯海岸现恐怖怪鱼"
# 获取排名前三的热点信息 -- 降序返回并携带score热度值
> zrevrange hotNews:2023-8-24 0 2 withscores
1) "福岛核废水"
2) 30000.0
3) "日本3天已连发5次地震"
4) 25000.0
5) "ShowMaker谈LPL中单"
6) 25000.0
这是一道真实的面试题,笔者当时一时绕进去了,没给出正确答案,此处给出我自己的理解:
这里出于简单,就不翻转了,排序规则改为按照得分降序,分数相同的情况下,再按照time降序排列
下面给出返回排名前三的用户 id,score 和 time 的具体命令实现:
# 排行榜添加条目信息
# 用户1得分80,时间戳为1
> zadd rank:2023-8-24 343597383681 1
(integer) 1
# 用户2得分75,时间戳为1
> zadd rank:2023-8-24 322122547201 2
(integer) 1
# 用户3得分90,时间戳为1
> zadd rank:2023-8-24 386547056641 3
(integer) 1
# 用户4得分75,时间戳为2
> zadd rank:2023-8-24 322122547202 4
(integer) 1
# 按照默认升序返回整个排行榜信息
> zrange rank:2023-8-24 0 -1
1) "2" # 用户2得分75,时间戳为1
2) "4" # 用户4得分75,时间戳为2
3) "1" # 用户1得分80,时间戳为1
4) "3" # 用户3得分90,时间戳为1
# 返回排名前三的用户信息
> zrevrange rank:2023-8-24 0 2 withscores
1) "3"
2) 386547056641.0
3) "1"
4) 343597383681.0
5) "4"
6) 322122547202.0
用户id可以从返回的member中直接获取到,而当前用户得分和time可以通过位运算获取到:
# score + 时间戳(32bit)
386547056641 & (2的31次方 - 1) = 8589934591 --> 获取时间戳结果为1
386547056641 >> 32 = 90
我们可以利用优先级队列实现延迟队列,只需要将优先级定义为任务执行的时间戳即可,然后应用线程不断循环,直到发现队列头部第一个任务到期了,则从队列移除并执行任务。
关于Redis数据结构,大家需要重点关注String,Set和ZSet的应用,特别是Set和ZSet,绝对是面试场景题的重要考点。
本文只列举了部分笔者目前所能想到的内容,各位大佬如果有补充,可以在评论区留言。