redis的使用场景
0.key-value 快速缓存
1.分布式锁
2.会话管理
3.时间序列
4.计数器Counter
5.速率限制器Rate limiter
6.可靠的消息队列
7.循环队列Circular list
8.事件通知Event notification
9.标签系统
10.排名榜
11.指标的统计
12.基于集合的共同好友
提及Redis我们的第一印象肯定是缓存,因为Redis是基于内存的数据库(速度快),所以适合用作缓存。但是除了被当做缓存使用,基于Reids提供的各种数据类型,我们还是可以实现很多功能。
分布式锁(SET)
我们设计分布式锁时需要考虑一下两个方面:
- 一次性只能有一个客户端获得锁。
- 避免死锁(获取锁的客户端在释放锁之前就崩溃后者网络不可到达的情况)。
以上两点都可以通过Redis中的SET命令来达到:
SET resource-name anystring NX EX max-lock-time
NX参数表示只有在键不存在时才能执行成功,这样多个客户端同时执行SET命令只有一个会成功,也就保证了只有一个客户端可以获得锁。
max-lock-time用来设置键的超时时间,这样在获取到锁的客户端崩溃或者网络不可达时依旧可以删除键,避免其他客户端一直不能获取到锁而造成死锁。
通常客户端获取锁并执行相关操作之后就会调用DEL命令来删除键,但是在网络延迟的情况下有可能会出现两个客户端同时获取到锁的情况:
- A、B两个客户端同时获取锁,A获得了锁,键的超时超时时间设置为60s。
- A执行相关的操作因为不明原因时间超过60s,键因为过期被删除。
- B获取到锁并执行相关操作。
- A执行操作完成调用DEL删除键。
- C执行SET命令也可以获得锁。
- 最后就出现了B、C两个客户端都获得锁的情况。
采用随机值的方式来解决上述问题,并在删除时不是通过键直接删除,而是通过判断值的方式来删除,这样就可以避免上述情况。
if redis.call(“get”,KEYS[1]) == ARGV[1]
then
return redis.call(“del”,KEYS[1])
else
return 0
end
除此之外,Redis还提供了RedLock algorithm来实现分布式锁,介于文章主题的原因,在这里就不详细介绍,大家可以去官网学习后者等后续文章。
会话管理
利用Redis的过期机制和消息通知机制可以实现在分布式架构对会话的统一管理,我们常用的spring-session就有基于redis的实现。
时间序列(APPEND)
在每次新增内容长度固定时,就可以使用APPEND命令来拼接新内容到字符串,然后通过GETRANGE来获取值,通过SETRANGE来设置值。
在使用时,可以以时间戳为键来存放这段时间内的值。例如每小时内的采集点存储,可以以小时为键。
APPEND ts2021082312 "0043"
计数器Counter(INCR)
可以通过INCR命令统计对网站的访问次数,每次用户访问就调用一次INCR。
结合EXPIRE命令也可以实现最近用户访问的页面。例如存在页面A、B、C、D,可以分别以vist:A、vist:B、vist:C、vist:D为键,每次用户访问对应的页面则调用INCR命令将访问次数加1同时设置新的过期时间60s。可以通过查询以vist开头的键列表查询最近60秒内被访问的页面列表。
速率限制器Rate limiter
在一些提供API的服务中,可能会存在需要限流的场景,例如每秒每个ip的请求数不超过10。通过Redis的INCR和EXPIRE可以实现该功能。
方案一
通过时间戳和IP地址组成键,收到请求时将IP和时间戳组成的键对应的值加1并对键设置过期时间。接着判断键对应的值加1之后的结果是否大于10,如果大于10则返回异常,否则请求可以正常通过。
方案二
以IP为键,并且将过期时长设置为1秒来实现。
方案三
通过Redis中的LIST来保存当前IP,通过LLEN来获取列表的长度并判断是否大于阈值。
可靠的消息队列
在使用Redis的RPUSH和RPOP来实现队列时,在消费者执行RPOP命令获取到消息后未执行对应业务之前就崩溃了,这样的场景就造成了消息丢失。
通过LMOVE命令,可以从列表获取元素的同时将元素插入额外的处理列表(processing)中,并在业务执行完成之后执行LREM命令去删除已经处理的消息。
上面提及的消息队列只是一种简单的实现模式,消息只能消费一次。在软件架构时其实还是要选用kafka这样的功能强大的中间件。
循环队列Circular list
LMOVE命令中如果source和destination是相同的键,那么就可以将头部元素移到尾部位置。
LMOVE circular circular LEFT RIGHT
对于一个需要持续执行的任务,例如判断系统中多个网址是否可用。可以将地址放入Redis列表中,并启动多个客户端执行调用上面的LMOVE命令来获取网址并完成检测工作,这样就确保了每个地址都会被检测到,并且新增的地址也会被检测到。
因为LMOVE命令是原子的而且是在同一个列表中移动元素,确保了成员不会丢失,最终确保了该方案的可靠性。
事件通知Event notification
因为Redis中的Set是没有提供的阻塞操作,我们可以将List的阻塞当做事件通知来完成Set的相关的阻塞操作。
标签系统
依赖Redis中Set的不存在重复成员的特性可以用来实现标签系统。
假设我们的业务场景是对TED的视频做标签管理。我们有资源S1、S2,S3,S1是关于健康(health)的、S2是关于数学(math)的、S3两者都有。我们就可以利用SET的命令完成以下需求:
// 给每个资源打上对应的标签
SADD S1 health
SADD S2 math
SADD S3 health math
// 标记拥有标签的资源
SADD tag:health S1 S3
SADD tag:math S3
获取标签列表
// 在标签过多的情况下通过迭代器的方式获取列表
SCAN 0 MATCH tag:*
获取同时拥有health和math两个标签的资源
SINTER tag: health tag: math
S1和S3拥有的相同的标签
SINTER S1 S3
排名相关
因为Sorted sets是带有排序功能,可以按照得分排序,因此利用此特性可以实现排行榜、获取排名前5的人等功能。
ZADD rank 10 a 9 b 8 d 7 f 6 h 5 k 4 l 3 p 2 i 1 z
// 获取排名前5
ZRANGE rank 0 4 rev
指标的统计
很多系统可能都会统计系统的日登录人数或一段时间的访问人数。通常我们有很多的方案来实现,比如可以通过审计系统将用户行为记录到一张表中,在查询时通过SQL语句去查询,这样虽然能达到效果,但是效率肯定不会太好。
我们可以使用Redis的bitmap来实现。
bitmap的最大特点就是效率快占用空间小,在对 1.28 亿用户的模拟中,“每日独立用户”等典型指标在 MacBook Pro 上耗时不到 50 毫秒,仅占用 16 MB 内存(128000000/8/1024/1024)。
对于日登录人数的指标我们可以做如下设计:
- 将用户的ID设置为增长的整数。
- 以当前日期为键在用户访问系统时获取到对应的用户ID,对应索引的位设置为1。
- 调用BITCOUNT来获取值为1的个数,即日登录人数。
- 对于某一段时间内系统的登录人数(不重复的)可以通过bitop命令来实现。
// 用户操作时设置对应索引的值为1,此操作的时间复杂度为O(1)
setbit yyyy-mm-dd user_id 1
bitcount yyyy-mm-dd
// 获取22号、23号、24号这三天的活跃用户人数
bitop or dest 2021-08-22 2021-08-23 2021-08-23
bitcount dest