Redis 基础-应用(第四章)

1. 分布式锁

1.1. 应用

  一般,先使用 setnx(set if not exists) 指令获取锁,其只允许被一个客户端占用,再设置过期时间,防止异常导致锁无法释放,最后再调用 del 指令释放锁。由于setnxexpire 之间不是原子操作,所以可使用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(有序列表) 来实现。我们将消息序列化成一个字符串作为 zsetvalue,这个消息的到期处理时间作为 score,然后用多个线程轮询 zset 获取到期的任务进行处理。

2.2. 注意

2.2.1. 消息队列空了

  如果消息队列空了,客户端就会陷入 pop 的空轮询,空轮询会拉高客户端的 CPU 和 Redis 的 QPS。解决方案有两种:

  • 使用 sleep,但是会增大消息延迟
  • 使用 blpop/brpop代替 lpop/rpop,也就是阻塞读,在队列无数据时,立即进入休眠,一旦数据到来,则立刻苏醒,消息的延迟几乎为零,但是闲置过久,服务器一般会主动断开连接,减少闲置资源占用,这个时候blpop/brpop会抛出异常来,所以需要捕捉异常

2.2.2. 其他技术

  Redis 常用来作为只有一组消费者的消息队列,它没有非常多的高级特性,没有 ack 保证,专业的消息队列中间件常用RabbitmqKafka

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