啧啧,面试官又开始为难人了,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适用场景都记下来,以后再遇到这个面试题,秀一把自己,我想面试官会颤抖的。