Redis除了缓存,还能做什么?

Posted by kzdgt on Sunday, January 21, 2024

面试官:Redis除了缓存,还能做什么? - 掘金 (juejin.cn)

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

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

img

1、登录鉴权

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

img

对应操作如下:

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相关命令,则恰好解决了这个问题。

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

img

对应操作如下:

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还提供了求交集、求并集等一系列直接操作集合的方法,非常适合于求共同或单方好友、粉丝、爱好之类的业务场景,实现起来特别方便。

img

对应操作如下:

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实现的。

img

对应操作如下:

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的过期时间机制,对其进行限制。

img

对应操作如下:

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数据结构实现消息队列的功能,这样可以在电商秒杀,或者在线教育集中约课等高并发写场景下,提供消峰功能。

img

对应操作如下:

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来实现栈功能,进而实现浏览器历史记录场景。

img

对应操作如下:

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来实现分布式锁,是一种非常高效的方式。

img

对应操作如下:

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位来进行状态存储。

img

对应操作如下:

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统计这种空间消耗巨大,但数据不需要特别精确的业务场景。

img

对应操作如下:

scss复制代码redis> pfadd page1 user1    //user1访问page1uv计数+1
(integer) 1
redis> pfadd page1 user1    //user1再次访问page1uv不计数
(integer) 0
redis> pfadd page1 user2    //user2访问page1uv计数+1
(integer) 1
redis> pfadd page1 user3    //user3访问page1uv计数+1
(integer) 1
redis> pfadd page1 user4    //user4访问page1uv计数+1
(integer) 1
redis> pfcount page1    //获取page1的uv
(integer) 4
redis> pfadd page2 user1    //user1访问page2uv计数+1
(integer) 1
redis> pfadd page2 user5    //user5访问page2uv计数+1
(integer) 1
redis> pfadd page2 user6    //user6访问page2uv计数+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

「真诚赞赏,手留余香」

kzdgt Blog

真诚赞赏,手留余香

使用微信扫描二维码完成支付