Redis 基础-应用(第四章)

1. 分布式锁

1.1. 应用

  一般,先使用 setnx(set if not exists) 指令获取锁,其只允许被一个客户端占用,再设置过期时间,防止异常导致锁无法释放,最后再调用 del 指令释放锁。由于setnx 和 expire 之间不是原子操作,所以可使用set的拓展参数,使之成为原子指令。代码如下:

1
2
3
4
> set lock:codehole true ex 5 nx
OK
... do something critical ...
> del lock:codehole

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采用了高位进位加法遍历