面试官:Redis除了缓存,还能做什么?

啧啧,面试官又开始为难人了,Redis做缓存难道不香吗,非要问我还能做什么。当代孔乙己有木有,非要问茴香豆中"茴"字的四种写法。

吐槽完毕,接下来咱们好好研究研究"Redis还能做什么",争取在下次面试的时候秒他。

1、登录鉴权

用户登录鉴权,以及对应的登录验证码或token到期失效,是系统最为常见的功能之一。而Redis key的超时失效功能,则非常适合于这种业务场景。

swift 复制代码
redis> setex captchalogin|13436669876 60 3456
"OK"
redis> get captchalogin|13436669876
"3456"
redis> get captchalogin|13436669876
(nil)
redis> setex tokencheck|12345 86400 54321
"OK"
redis> get tokencheck|12345
"54321"

以上实现场景为:

(1)系统登录场景,用户输入手机号后,点击发送短信验证码,通过Redis存储前缀 + 手机号作为key,验证码作为value,并设置60秒过期时间。

(2)用户在60秒内进行登录验证,则可以从Redis中获取到验证码,验证相同则登录成功,超过60秒则获取不到验证码值,登录失败。

(3)用户登录后生成token,Redis存储前缀 + token作为key,用户ID作为value,并设置为一天过期。

(4)接下来可以通过token进行鉴权,并获取对应的用户ID。

2、计数器

计数器是一种非常常见的业务场景,类似于知乎的帖子点赞、收藏,电商的库存扣减等。

但在高并发场景下,用MySQL数据库去硬扛这种读写压力是比较吃力的,Redis的INCR、DECR、INCRBY、DECRBY相关命令,则恰好解决了这个问题。

我们以知乎点赞场景进行举例:

php 复制代码
redis> set article1 0    //初始化,将article1的点赞数设置为0
"OK"
redis> incr article1    //article1被点赞一次
(integer) 1
redis> decr article1    //article1被取消点赞一次
(integer) 0
redis> incrby article1 2    //通过incrby,可以实现article1被点赞N次
(integer) 2
redis> decrby article1 2    //通过decrby,可以实现article1被取消点赞N次
(integer) 0

3、粉丝关注

粉丝关注场景本身还好,但涉及到计算共同粉丝,单方粉丝之类的会比较麻烦,这时就轮到Redis Set数据类型粉墨登场了。

Set是一个无序的天然去重的集合,即:Key-Set。此外,Set还提供了求交集、求并集等一系列直接操作集合的方法,非常适合于求共同或单方好友、粉丝、爱好之类的业务场景,实现起来特别方便。

对应操作如下:

php 复制代码
redis> SADD Tony Mary    //Mary成为了Tony的粉丝
(integer) 1
redis> SADD Tony Lynn    //Lynn成为了Tony的粉丝
(integer) 1
redis> SMEMBERS Tony     //Tony的粉丝列表
1) "Mary"
2) "Lynn"
redis> SADD Tom Mary     //Mary成为了Tom的粉丝
(integer) 1
redis> SADD Tom Eric     //Eric成为了Tom的粉丝
(integer) 1
redis> SMEMBERS Tom      //Tom的粉丝列表
1) "Mary"
2) "Eric"
redis> SINTER Tony Tom   //Tony和Tom的共同粉丝
1) "Mary"
redis> SUNION Tony Tom   //Tony和Tom的所有粉丝
1) "Mary"
2) "Lynn"
3) "Eric"
redis> SDIFF Tony Tom   //Tony的粉丝,但不是Tom的粉丝
1) "Lynn"
redis> SDIFF Tom Tony   //Tom的粉丝,但不是Tony的粉丝
1) "Eric"

4、排行榜

Zset(SortedSet),是Set的可排序版,是通过增加一个排序属性score来实现的,适用于排行榜和时间线之类的业务场景,且在高并发场景下具备非常优秀的性能。

ZSet在排行榜场景中,具备高性能的原因有二:

  • 用空间换时间的预计算思想。
  • 优秀的底层数据结构,通过skiplist(跳表)+ dict(哈希表)+ listpack实现的。

对应操作如下:

php 复制代码
redis> ZADD 家电全品类 5.5 海尔       //添加了海尔电器和5.5亿销售额
(integer) 1
redis> ZADD 家电全品类 4.5 美的       //添加了美的电器和4.5亿销售额
(integer) 1
redis> ZADD 家电全品类 3.2 小米       //添加了小米电器和3.2亿销售额
(integer) 1
redis> ZADD 家电全品类 2.7 格力       //添加了格力电器和2.7亿销售额
(integer) 1
redis> ZCARD 家电全品类              //家电全品类的数量
(integer) 4
redis> ZSCORE 家电全品类 格力        //获取格力的销售额
"2.7"
redis> ZREVRANGE 家电全品类 0 -1 WITHSCORES    //家电全品类的倒序输出
1) "海尔"
2) "5.5"
3) "美的"
4) "4.5"
5) "小米"
6) "3.2"
7) "格力"
8) "2.7"
redis> ZRANGE 家电全品类 0 -1 WITHSCORES      //家电全品类的正序输出
1) "格力"
2) "2.7"
3) "小米"
4) "3.2"
5) "美的"
6) "4.5"
7) "海尔"
8) "5.5"
redis> ZINCRBY 家电全品类 2.2 格力            //为格力增加2.2亿销售额
"4.9"
redis> ZREVRANGE 家电全品类 0 -1 WITHSCORES   //增加销售额后的排行榜变化
1) "海尔"
2) "5.5"
3) "格力"
4) "4.9"
5) "美的"
6) "4.5"
7) "小米"
8) "3.2"

5、防刷

防刷:用户在极短时间内,频繁发起请求去调用系统中的某个接口,该情况下我们需要对其进行限制。

举例如下:我们限制用户每秒钟只能下单一次,若用户在一秒钟内连续三次下单,这时只有第一个下单是成功的,其他两个我们会通过Redis的过期时间机制,对其进行限制。

对应操作如下:

shell 复制代码
redis> set createorder|userid|1234 "" EX 1 NX    //userid为1234的用户第一次下单成功,设置一秒钟过期时间 
"OK"
redis> set createorder|userid|1234 "" EX 1 NX    //userid为1234的用户一秒钟内第二次下单,结果不成功
(nil)
redis> set createorder|userid|1234 "" EX 1 NX    //userid为1234的用户超过一秒钟再次下单,结果成功
"OK"

6、消息队列

Redis可以通过list数据结构实现消息队列的功能,这样可以在电商秒杀,或者在线教育集中约课等高并发写场景下,提供消峰功能。

php 复制代码
redis> lpush mybooks java    //往mybooks list中填充java,实现生产者功能
(integer) 1
redis> lpush mybooks mysql    //往mybooks list中填充mysql,实现生产者功能
(integer) 2
redis> lpush mybooks redis    //往mybooks list中填充redis,实现生产者功能
(integer) 3
redis> rpop mybooks    //往mybooks list中取出java,实现消费者功能 
"java"
redis> rpop mybooks    //往mybooks list中取出mysql,实现消费者功能 
"mysql"
redis> rpop mybooks    //往mybooks list中取出redis,实现消费者功能 
"redis"

7、浏览器历史记录

每当我们访问一个新的网页,浏览器就会自动存储下来,当我们点击"后退"按钮时,最近一次访问的网页就会展示出来。

我们可以通过Redis list来实现栈功能,进而实现浏览器历史记录场景。

php 复制代码
redis> lpush mybrowser sohu    //浏览sohu
(integer) 1
redis> lpush mybrowser sina    //浏览sina
(integer) 2
redis> lpush mybrowser baidu    //浏览baidu
(integer) 3
redis> lpop mybrowser    //后退
"baidu"
redis> lpop mybrowser    //后退
"sina"
redis> lpop mybrowser    //后退
"sohu"

8、分布式锁

单机模式下,我们可以用synchronized来轻松实现锁机制,但在分布式集群场景下,则需要用分布式锁来代替synchronized。

通过Redis来实现分布式锁,是一种非常高效的方式。

swift 复制代码
redis> set mytasklock "tony" ex 10 nx    //获取分布式锁成功,加锁人为tony,过期时间为10秒
"OK"
redis> set mytasklock "tom" ex 10 nx    //获取分布式锁失败,加锁人为tom
(nil)
redis> del mytasklock    //释放分布式锁
(integer) 1              //该步骤需要通过lua脚本实现原子性操作------"如果加锁人为tony,则释放锁"

当然,目前主流的分布式锁解决方案是通过Redisson来实现的,相比于上述方案,Redisson解决了锁的可重入和续期问题。

9、用户签到

用户签到、用户出勤、当天活跃用户等场景,虽然我们用Redis Set数据结构也可以实现,但用户量级庞大的情况下,会极大占用内存空间。

这种情况下,非常适合Redis BitMap数据结构,通过其bit位来进行状态存储。

php 复制代码
redis> setbit userid|1234|202312 0 1    //用户1234,在2023年12月1日签到(偏移量从0开始,所以减1)
(integer) 0
redis> setbit userid|1234|202312 1 1    //用户1234,在2023年12月2日签到
(integer) 0
redis> setbit userid|1234|202312 3 1    //用户1234,在2023年12月4日签到
(integer) 0
redis> getbit userid|1234|202312 3    //查询用户1234,在2023年12月4日是否签到
(integer) 1
redis> getbit userid|1234|202312 2    //查询用户1234,在2023年12月3日是否签到
(integer) 0
redis> bitcount userid|1234|202312    //查询用户1234,在2023年12月的签到天数
(integer) 3

10、网站UV统计

假设如下场景,某大型网站需要统计每个网页每天的UV(Unique Visitor)数据,与PV(Page View)的不同点在于,UV需要进行去重操作,同一个用户一天内的多次访问一个网页,只能计数一次。

如果我们通过Redis Set存储用户ID的方式进行解决,非常耗费内存空间。这时,我们可以使用HyperLogLog。

Redis HyperLogLog 提供不精确的去重计数方案,标准误差是 0.81%,但仅仅占用12k的内存空间,非常适用于大型网站UV统计这种空间消耗巨大,但数据不需要特别精确的业务场景。

scss 复制代码
redis> pfadd page1 user1    //user1访问page1,uv计数+1
(integer) 1
redis> pfadd page1 user1    //user1再次访问page1,uv不计数
(integer) 0
redis> pfadd page1 user2    //user2访问page1,uv计数+1
(integer) 1
redis> pfadd page1 user3    //user3访问page1,uv计数+1
(integer) 1
redis> pfadd page1 user4    //user4访问page1,uv计数+1
(integer) 1
redis> pfcount page1    //获取page1的uv
(integer) 4
redis> pfadd page2 user1    //user1访问page2,uv计数+1
(integer) 1
redis> pfadd page2 user5    //user5访问page2,uv计数+1
(integer) 1
redis> pfadd page2 user6    //user6访问page2,uv计数+1
(integer) 1
redis> pfcount page2    //获取page2的uv
(integer) 3
redis> pfmerge page1and2 page1 page2    //将page1和page2merge成一个
"OK"
redis> pfcount page1and2    //获取page1and2的uv
(integer) 6

结语

把上述10种Redis适用场景都记下来,以后再遇到这个面试题,秀一把自己,我想面试官会颤抖的。

相关推荐
zwhdlb2 分钟前
Java + 工业物联网 / 智慧楼宇 面试问答模板
java·物联网·面试
Pitayafruit3 分钟前
Spring AI 进阶之路04:集成 SearXNG 实现联网搜索
spring boot·后端·ai编程
风象南6 分钟前
SpringBoot 自研「轻量级 API 防火墙」:单机内嵌,支持在线配置
后端
码熔burning17 分钟前
JVM 面试精选 20 题(续)
jvm·面试·职场和发展
Victor35623 分钟前
Redis(14)Redis的列表(List)类型有哪些常用命令?
后端
Victor35623 分钟前
Redis(15)Redis的集合(Set)类型有哪些常用命令?
后端
卷福同学24 分钟前
来上海三个月,我在马路边上遇到了阿里前同事...
java·后端
bobz9659 小时前
小语言模型是真正的未来
后端
DevYK10 小时前
企业级 Agent 开发实战(一) LangGraph 快速入门
后端·llm·agent
艾伦~耶格尔10 小时前
【集合框架LinkedList底层添加元素机制】
java·开发语言·学习·面试