“据不完全统计,90%的程序员在工作中使用Redis的时候只会用到string的数据类型”
上面的话当然是一句玩笑话,大家不必当真哈! (。◕ˇ∀ˇ◕)
Redis目前在我们的工作当中已经是一个必不可少的工具了,面试的时候也已经成为面试官们的必考题。
接下来我会从Redis的数据类型出发,举例讲解每种数据类型在实际工作当中能具体都有哪些使用场景,也包含了一些骚操作。
观前提醒:阅读本文需要有一定的Redis基础知识哦!
Redis的使用场景
String
- 商品编号、订单号使用incr命令自增
> set key 1
OK
> incr key
(integer) 2
> incr key
(integer) 3
> get key
"3"
- 文章浏览量
同上也可以使用incr命令 - 限流
实现限流功能,主要需要使用set key value ex 200 nx这个命令,这里value需要设置为整数,每次接口请求时,对value进行校验,只要没有达到限流阈值,那就对key进行incr,然后执行具体的业务代码,命令如下:
#假设200秒内,只允许5次请求
#首先每次请求过来,先去获取key
> get ratelimit
(nil)
#发现key不存在,则进行设置,value设置为1,代表请求了一次,ttl设置为100秒。本次请求通过。
> set ratelimit 1 ex 200 nx
OK
#又来了一个请求,同样是先获取key
> get ratelimit
"1"
#发现key存在,且value等于1,未达到5次限流阈值,然后对key操作自增。本次请求通过。
> incr ratelimit
(integer) 2
#后续操作原理同上。。。。
> get ratelimit
"2"
> incr ratelimit
(integer) 3
> get ratelimit
"3"
> incr ratelimit
(integer) 4
> get ratelimit
"4"
> incr ratelimit
(integer) 5
#这个时候,在200秒时间范围内又来了一个请求,获取key,发现value已经等于5,达到了限流阈值。本次请求拦截。
> get ratelimit
"5"
Hash
- 首先需要明确Redis中的hash就相当于是Java中的Map,并且是 Map<String,Map<Object,Object>> 这种形式的,因此可以用来对象
- 简单版购物车功能
通过上图,可以发现,通过几个命令,就可以基本实现购物车的常用功能了,命令如下:
#给uid为9527的用户添加商品1,数量为1件
> hset shopcar_uid9527 prodid_1 1
1
#给uid为9527的用户添加商品2,数量为1件
> hset shopcar_uid9527 prodid_2 1
1
#用户对商品2增加了购买数量
> hincrby shopcar_uid9527 prodid_2 3
(integer) 4
#用户的购物车此时含有2种商品
> hlen shopcar_uid9527
2
#全选购物车中的所有商品,可以得到商品id及其对应的购买数量
> hgetall shopcar_uid9527
1) "prodid_1"
2) "1"
3) "prodid_2"
4) "4"
List
- 微信订阅号文章
#给用户推送一篇文章,id为789
> lpush uid_9527_likearticle 789
(integer) 1
#同上
> lpush uid_9527_likearticle 666
(integer) 2
#同上
> lpush uid_9527_likearticle 999
(integer) 3
#同上
> lpush uid_9527_likearticle 234234
(integer) 4
#同上
> lpush uid_9527_likearticle 888
(integer) 5
#展示用户收到最新的3篇文章
> lrange uid_9527_likearticle 0 2
1) "888"
2) "234234"
3) "999"
- 限流
list实现可以实现类似令牌桶模式的限流功能,主要需要以下几个步骤:
1) 定时任务:一直往list中push值,直到list的length达到设定的阈值
2)每次处理请求时,先使用pop命令弹出,如果成功则处理请求,否则就是触发了限流
具体命令如下:
#统计一下目前桶中有多少令牌
> llen key
(integer) 0
#未达到桶的最大值,定时任务往桶中放入令牌
> lpush key 1
(integer) 1
> lpush key 1
(integer) 2
> lpush key 1
(integer) 3
#达到桶的最大值,定时任务停止放令牌
> llen key
(integer) 3
#每次请求,从桶中pop,成功则处理该请求
> rpop key
"1"
> rpop key
"1"
> rpop key
"1"
#pop失败,达到限流阈值
> rpop key
(nil)
Set
- 抽奖
#往奖池中添加参与活动的用户id
> sadd users1 111 222 333 444 555 666 777 888 999 12312 234 9527
(integer) 12
#抽取5名幸运用户
> srandmember users1 5
1) "444"
2) "999"
3) "333"
4) "234"
5) "666"
srandmember命令用于返回集合中的一个或多个随机元素。但是不会移除其中的元素,如果要实现那种阶梯式抽奖,已中奖的用户不再参与抽奖的功能,可以使用spop命令
#往奖池中添加参与活动的用户id,一共12名用户
> sadd users2 111 222 333 444 555 666 777 888 999 12312 234 9527
(integer) 12
#抽取三等奖3名
> spop users2 3
1) "555"
2) "999"
3) "888"
#奖池里还剩9名用户
> scard users2
9
#抽取二等奖2名
> spop users2 2
1) "12312"
2) "444"
#奖池里还剩7名用户
> scard users2
7
#抽取一等奖1名
> spop users2 1
1) "111"
#奖池里还剩6名用户
> scard users2
6
- 朋友圈点赞
#用户22、33点赞了朋友圈
> sadd WechatMoments 22 33
(integer) 2
#用户666点赞了朋友圈
> sadd WechatMoments 666
(integer) 1
#用户33取消了点赞
> srem WechatMoments 33
1
#展示所有点赞的用户
> smembers WechatMoments
1) "22"
2) "666"
#统计朋友圈点赞的数量
> scard WechatMoments
2
#判断用户22是否点赞了该朋友圈
> sismember WechatMoments 22
(integer) 1
#判断用户33是否点赞了该朋友圈
> sismember WechatMoments 33
(integer) 0
- 共同关注的人
#用户u1关注了这些大V
> sadd u1 1 2 3 4 5 6
(integer) 6
#用户u2关注了这些大V
> sadd u2 5 6 7 8 9
(integer) 5
#用户u1、u2共同关注了如下大V
> sinter u1 u2
1) "5"
2) "6"
- 可能认识的人
#用户u1有如下好友
> sadd u1 1 2 3 4 5 6
(integer) 6
#用户u2有如下好友
> sadd u2 1 4 5 6 9 11
(integer) 6
#用户u1、u2存在共同的好友
> sinter u1 u2
1) "1"
2) "4"
3) "5"
4) "6"
#那么用户u1、u2可能有认识对方的一些好友
> sdiff u1 u2
1) "2"
2) "3"
> sdiff u2 u1
1) "9"
2) "11"
sorted set
- 排行榜(热搜、VIP充值)
#用户A、B、C等用户分别充值了如下的金额
> zadd ranklist 100 A 200 B 300 C 444 D 234 E 35 F 534 G
(integer) 7
#此时充值排行榜的前三名分别是如下几个用户
> zrevrange ranklist 0 2 withscores
1) "G"
2) 534.0
3) "D"
4) 444.0
5) "C"
6) 300.0
#用户B又充值了999.99元
> zincrby ranklist 999.99 B
1199.99
#充值排行榜的前三名就发生了变化
> zrevrange ranklist 0 2 withscores
1) "B"
2) 1199.99
3) "G"
4) 534.0
5) "D"
6) 444.0
#查看用户C在排行榜中排名,注意下标从0开始,因此就是排在第四名
> zrank ranklist C
3
- 限流
zset要实现限流的功能,score可以设置成时间戳,然后通过统计一定时间范围内的数量,来判断是否达到了限流的阈值,过程比较复杂,我直接用代码来展示:
/**
* @param redisKey redis限流功能的key
* @param timeLimit 时间限制范围(毫秒)
* @param countLimit 数量限制
* @return
*/
public boolean tryAcquire(String redisKey, long timeLimit, int countLimit) {
Jedis jedis = new Jedis();
// 获取当前时间
long nowTime = System.currentTimeMillis();
// 获取时间限制范围内的起始时间
long startTime = nowTime - timeLimit;
// 获取该时间范围内已经存在的成员数量
Long alreadyExistsCount = jedis.zcount(redisKey, startTime, nowTime);
if (alreadyExistsCount > countLimit) {
// 时间范围内已经存在了超过限制的成员数量了,说明已经达到限流阈值了,
return false;
}
// 以UUID作为redis的value
String uuid = UUID.randomUUID().toString();
// 向zset中添加新成员
Long zadd = jedis.zadd(redisKey, nowTime, uuid);
if (zadd != 1) {
// 添加失败
return false;
}
// 移除zset中除了本次时间范围之外的所有成员数据
Long oldExistsCount = jedis.zcount(redisKey, 0, startTime);
if (oldExistsCount > 0) {
jedis.zremrangeByScore(redisKey, 0, startTime);
}
// 查询刚才添加的成员在zset中的排名,此时因为已经移除了本次时间范围之外的所有数据,因此rank不应超过限流数量
Long zrank = jedis.zrank(redisKey, uuid);
if (zrank > countLimit) {
// 排名超过了限流数量,则说明达到限流阈值了
// 主动移除刚才添加的成员
jedis.zrem(redisKey, uuid);
return false;
}
// 未达到限流阈值,返回成功
return true;
}
以上就是Redis5大基本数据类型的使用场景了,接下来我介绍一下几个不常见的类型。
bitmap
BitMap是什么?
就是通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身。我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间。
命令介绍
指令 SETBIT key offset value
设置或者清空key的value(字符串)在offset处的bit值(只能只0或者1)。
使用场景
- 上班打卡、活动签到
#工作日打卡功能
#第一个数字代表是星期几,第二个数字代表是否打卡,其中0-未打卡,1-已打卡
> setbit sign 1 1
0
> setbit sign 2 1
0
> setbit sign 3 0
0
> setbit sign 4 0
0
> setbit sign 5 1
0
#查看星期一是否打卡:已打卡
> getbit sign 1
1
#查看星期三是否打卡:未打卡
> getbit sign 3
0
#统计一周打卡天数
> bitcount sign
3
- 统计活跃用户数
#每天当用户登录时就设置一下
> setbit 2021_02_01 22 1
0
> setbit 2021_02_01 33 1
0
> setbit 2021_02_01 9527 1
0
> setbit 2021_02_02 9527 1
0
> setbit 2021_02_02 22 1
0
> setbit 2021_02_03 33 1
0
> setbit 2021_02_03 9527 1
0
#将这3天用户登录的数据“整合”
> bitop and ActiveUsers 2021_02_01 2021_02_02 2021_02_03
1191
#统计连续3天都登录的用户数量
> bitcount ActiveUsers
1
HyperLogLog
介绍
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
什么是基数?
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。
使用场景:网页的UV(Unique Visitor:独立访客)
传统的方式,可以使用set保存用户的id,然后可以统计set中的元素数量作为判断标准。
但是,这个方式如果保存了大量的用户id,则会占用大量的内存空间,比较浪费,我们的目的就是为了计数,而不是保存用户的id。
代码示例:
#用户每次访问网页,记录一下
> pfadd visit1 a
(integer) true
> pfadd visit1 b
(integer) true
> pfadd visit1 c
(integer) true
> pfadd visit1 d
(integer) true
> pfadd visit1 b
(integer) false
#统计“visit1”网页总共的UV
> pfcount visit1
(integer) 4
#记录另一个网页的访问信息
> pfadd visit2 c
(integer) true
> pfadd visit2 a
(integer) true
> pfadd visit2 x
(integer) true
> pfadd visit2 y
(integer) true
> pfadd visit2 z
(integer) true
> pfcount visit2
(integer) 5
#将多个HyperLogLog合并为一个HyperLogLog
> pfmerge total visit1 visit2
(integer) 1
#统计出整个网站的UV
> pfcount total
(integer) 7
注意:如果数据量比较大的话,HyperLogLog统计可能会存在一点误差,大概是0.81%,但是针对UV统计这种场景的话,完全是可以忽略不计的。
GEO
介绍
GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。
GEO 操作方法有:
- geoadd:添加地理位置的坐标。
- geopos:获取地理位置的坐标。
- geodist:计算两个位置之间的距离。
- georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
- georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
- geohash:返回一个或多个位置对象的 geohash 值。
使用场景
看了上面的介绍,相信很多同学都已经能够想象出GEO能实现哪些功能了。比如常见的定位、附近的人、两地之间的距离等等,这些功能都可以使用GEO来实现。
代码如下:
- 录入、获取城市的地理位置信息(纬度、经度、名称)
注意:
该命令以采用标准格式的参数x,y,所以经度必须在纬度之前。这些坐标的限制是可以被编入索引的,区域面积可以很接近极点但是不能索引。具体的限制,由EPSG:900913 / EPSG:3785 / OSGEO:41001 规定如下:
- 有效的经度从-180度到180度。
- 有效的纬度从-85.05112878度到85.05112878度。
当坐标位置超出上述指定范围时,该命令将会返回一个错误。
#添加一些城市的地理位置信息
127.0.0.1:6379> geoadd city 116.397128 39.916527 beijing
(integer) 1
127.0.0.1:6379> geoadd city 121.48941 31.40527 shanghai
(integer) 1
127.0.0.1:6379> geoadd city 106.54041 29.40268 chongqing
(integer) 1
127.0.0.1:6379> geoadd city 118.8921 31.32751 nanjing
(integer) 1
#获取城市的地理位置信息
127.0.0.1:6379> geopos city shanghai
1) 1) "121.48941010236740112"
2) "31.40526993848380499"
- 查看两地之间的距离
127.0.0.1:6379> geodist city shanghai beijing
"1052105.5643"
#查找上海和北京之间的直线距离,单位:KM。大家可以百度一下,差不多很接近了。
127.0.0.1:6379> geodist city shanghai beijing km
"1052.1056"
- 查看附近的人
#获得附近人的地址,通过半径俩查询,这里就以城市为例了
# 121 31 是我目前大致的经纬度信息,可以看到100km范围内的城市只有上海
127.0.0.1:6379> georadius city 121 31 100 km
1) "shanghai"
#把半径扩大到500km时,城市就包括了上海和南京
127.0.0.1:6379> georadius city 121 31 500 km
1) "nanjing"
2) "shanghai"
#再扩大范围,查询出来的城市就更多了
127.0.0.1:6379> georadius city 121 31 50000 km withcoord withdist
1) 1) "chongqing"
2) "1400.2709"
3) 1) "106.54040783643722534"
2) "29.40268053517299762"
2) 1) "nanjing"
2) "203.8973"
3) 1) "118.89209836721420288"
2) "31.32750976275760735"
3) 1) "shanghai"
2) "64.8057"
3) 1) "121.48941010236740112"
2) "31.40526993848380499"
4) 1) "beijing"
2) "1075.4316"
3) 1) "116.39712899923324585"
2) "39.91652647362980844"
#如果说附近的人很多时,可以使用“count”返回指定数量的结果集
georadius city 121 31 50000 km withcoord withdist count 1
1) 1) "shanghai"
2) "64.8057"
3) 1) "121.48941010236740112"
2) "31.40526993848380499"
- 找出位于指定元素周围的其他元素
127.0.0.1:6379> georadiusbymember city shanghai 300 km
1) "nanjing"
2) "shanghai"
- 其他
GEO底层其实是由zset实现的,所以也可以使用zset的相关命令
#查看地图中全部元素
127.0.0.1:6379> zrange city 0 -1
1) "chongqing"
2) "nanjing"
3) "shanghai"
4) "beijing"
#移除某个地址信息
127.0.0.1:6379> zrem city nanjing
(integer) 1
127.0.0.1:6379> zrange city 0 -1
1) "chongqing"
2) "shanghai"
3) "beijing"
总结
经过上面的讲解,相信大家对Redis的使用场景都有了一个更深刻的理解了。回到文章开头的那句玩笑话,为什么绝大多数人都只是使用string的set key value呢?其实还是了解的少了,光看Redis的那些命令,是苍白空洞的,只有跟实际业务联系起来,才能真正发挥Redis的优势。
当然,你也完全没有必要把上面的命令全部记住,包括在面试中,面试官并不会过于关心命令的准确性,还是看重你会用来做些什么事情,具体命令随时查看文档就可以了。
我列出的这些场景还是一小部分,大家受到启发后可以扩展,也欢迎在评论区留言,谢谢大家啦!O(∩_∩)O哈哈~