1. 分布式锁
1.1. 应用
一般,先使用 setnx(set if not exists)
指令获取锁,其只允许被一个客户端占用,再设置过期时间,防止异常导致锁无法释放,最后再调用 del
指令释放锁。由于setnx
和 expire
之间不是原子操作,所以可使用set
的拓展参数,使之成为原子指令。代码如下:
1 | > set lock:codehole true ex 5 nx |
1.2. 注意
1.2.1. 超时问题
加锁和释放锁之间的逻辑执行的时间太长,以至于超出了过期限制,此时就会出现问题。由于没有完美解决方案,相对安全的有两种:
- 稍微安全一点的方案是为
set
指令的value
参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除key
- 另一种是使用
Lua
脚本,因为Lua
脚本可以保证连续多个指令的原子性执行
1.2.2. 可重入性
1.2.3. 锁冲突处理
客户端在处理请求时加锁失败,一般有 3 种策略:
- 直接抛出特定类型的异常,本质上是放弃当前请求的,由用户决定是否重试发起新的请求
- 使用
sleep
阻塞消息处理线程,但会早成延时 - 使用延时队列,将当前冲突的请求存放到另一个队列,延后处理以避免冲突
2. 异步消息队列/延时队列
2.1. 应用
list
(列表) 数据结构常用来作为异步消息队列使用,使用 rpush/lpush
操作入队列,使用lpop/rpop
来出队列。
延时队列可以通过 Redis 的 zset
(有序列表) 来实现。我们将消息序列化成一个字符串作为 zset
的 value
,这个消息的到期处理时间作为 score
,然后用多个线程轮询 zset
获取到期的任务进行处理。
2.2. 注意
2.2.1. 消息队列空了
如果消息队列空了,客户端就会陷入 pop
的空轮询,空轮询会拉高客户端的 CPU 和 Redis 的 QPS。解决方案有两种:
- 使用
sleep
,但是会增大消息延迟 - 使用
blpop/brpop
代替lpop/rpop
,也就是阻塞读,在队列无数据时,立即进入休眠,一旦数据到来,则立刻苏醒,消息的延迟几乎为零,但是闲置过久,服务器一般会主动断开连接,减少闲置资源占用,这个时候blpop/brpop会抛出异常来,所以需要捕捉异常
2.2.2. 其他技术
Redis
常用来作为只有一组消费者的消息队列,它没有非常多的高级特性,没有 ack 保证,专业的消息队列中间件常用Rabbitmq
和 Kafka
等
3. 位图
3.1. 应用
记录 Boolean
数据,比如记录月活数据,签到记录等,可以使用 Redis 的位数组,其会自动扩展
4. HyperLogLog
4.1. 应用
HyperLogLog
数据结构是 Redis 的高级数据结构,用来提供不精确的去重计数方案
5. 布隆过滤器
5.1. 应用
布隆过滤器可以理解为一个不怎么精确的 set
结构,当使用它的 contains
方法判断某个对象是否存在时,可能会误判
6. 限流
6.1. 应用
- 简单限流:用于控制用户行为,避免垃圾请求,例如限定用户的某个行为在指定的时间里只能允许发生 N 次
- 漏斗限流:漏斗的剩余空间就代表着当前行为可以持续进行的数量,漏嘴的流水速率代表着系统允许该行为的最大频率
7. 经纬度计算
7.1. 应用
利用地理位置 GEO
模块,来计算实现“附近的人”功能,其算法是业界比较通用的地理位置距离排序算法 GeoHash
7.2. 注意
同样的实现,可以使用mongodb
8. 查询特定前缀的 key
8.1. 应用
- 可以使用
keys
,再配合通配符来查询,但是其是遍历查找,数据大时效率很低 - 使用
Scan
,它通过游标分步进行,不会阻塞线程,支持通配符与limit
,但返回结果可能会有重复值
8.2. 注意
limit
不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量(约等于)- 考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏,
Scan
采用了高位进位加法遍历