前言

本文主要介绍了Redis的除过常见的用作缓存之外的一些别的使用场景。

分布式锁

适⽤场景:对并发高、能容忍⼀定的锁错误的场景,如通过分布式锁控制避免缓存穿透后,进⼊ 数据库请求的数量

原理

setnx key value:将 key 设置值为 value ,如果 key 不存在,这种情况下等同SET命令。当 key 存在时,什么也不做。 SETNX 是”SET if Not eXists”的简写。

伪代码:

1
2
3
4
5
6
7
8
if(setnx(key, 1) == 1){  //设置值
expire(key, 1); //设置过期时间
try{
//do something
} finally {
del(key); //删除
}
}

redis命令:

1
2
3
> setnx key value
> expire
> del key

存在的问题

死锁问题一:

描述:假如某个客户端在执行了SETNX命令、加锁之后,紧接着却在操作共享数据时发⽣了异常,结 果⼀直没有执行最后的DEL命令释放锁。因此,锁就⼀直被这个客⼾端持有,其它客户端⽆法拿到 锁,也无法访问共享数据和执行后续操作,这会给业务应⽤带来影响。

解决方案:

给锁增加一个过期时间

死锁问题二:

描述:setnx,expire是分步执行的,不具备原子性,如果在加锁命令执⾏完成后expire命令或者del 命令因为异常没有执行,会导致锁没有设置超时时间而造成死锁。

解决方案:

2.6.12后⽀持SET key value NX EX 1,⽀持nx与过期时间在⼀个命令内完成,可替代setnx命令。

锁失效问题:

描述:如果执行业务的时间大于超时时间,会导致业务还没执行完,锁却过期了。此时其他线程可以 获取锁,导致数据出现不⼀致风险。

解决方案: • 超时时间需要经过评估,尽可能给出合适的超时时间。 • 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。

误解锁问题:

描述:由于解锁时并没有区分上锁的对象,可能会造成误解锁。如:某个线程A持有了分布式锁,释放 锁时,由于网络原因导致超时,于是又进行了重试,即执行了两次del命令。而事实上,两个命令在 redis服务端都执行成功了。如果两次命令中间,有另外⼀个线程进行了加锁操作,那么线程A第⼆次 执行del命令时,就会把,另外⼀个线程的锁给释放了。

解决方案: • 区分来自不同线程的锁操作,value设置⼀个唯⼀值,这个值只有当前线程知道。

主从切换时多线程持有锁问题:

描述:发生主从切换时,可能导致多个线程同时持有锁。 线程A对master节点写⼊了锁,此时会异步复制给对应的slave节点。但是这个过程中⼀旦发生master节点宕机,主备切换,slave节点从变为了master节点。 这时线程B来尝试加锁的时候,在新的master节点上也能加锁,此时就会导致多个客⼾端对同⼀个分 布式锁完成了加锁 。

解决方案:

RedLock:部署多个redis实例,写入锁时需要半数以上redis写成功才算加锁成功。注意:慎用,存在较多的争议。且引入了很高的复杂度。

限流

适用场景:集群限流

计数器法

描述

为限流的每个接口设置⼀个key,value为String类型,使用incr命令计数。 根据限流的周期,设置过期时间。如每秒20次请求,那过期时间就设置为1s。

优点:实现简单;性能高;占用内存少; 缺点:精确度低。 比如设置每秒20次请求,在1s的周期内,在前900ms没有流量,但在最后10ms内流量剧增,来了20次请求,然后在第二秒刚开始的的前10ms内又来了20次请求,这样子的话,从整体来看已经超过了每秒20次的请求限制了。

滑动窗口计数法

描述

将限流时间周期分为N个小周期,分别记录各个小周期的访问次数,并根据时间删除过期的小周期 比如某个接口,每秒钟最多只能处理100个请求。设置⼀个1秒钟的滑动窗口,窗口中有10个格子,每个格子100毫秒,每100毫秒移动⼀次,每次移动都需要记录当前服务请求的次数。格⼦每 次移动的时候判断⼀次,当前访问次数和LinkedList中最后⼀个相差是否超过100,如果超过就需 要限流了。

优点:性能高

缺点:实现复杂;精度取决于划分窗口的个数。

滑动窗口日志法

描述

• 为限流的每个接口设置⼀个key,类型为zset,score存储访问的时间戳; • 每次请求,使用zadd增加⼀条记录; • 使用zcount根据score统计时间窗口内的记录数量,判断是否限流; • 使用zremrangebyscore根据score删除过期时间戳的记录。

优点:精确度高; 缺点:实现复杂;性能差;内存空间占用大;

异步消息队列

Redis的list(列表)数据结构常用来作为异步消息队列使用,使用 rpush/lpush 操作入队列,使用 lpop 和 rpop 来出队列,如下图:

适用场景:Redis的消息队列不是专业的消息队列,它没有那么多的高级特性,没有ack保证,如果对消息的可靠性有着极致的追求,那么它就不适合适用。

问题:空队列处理方式

描述:队列空了会导致pop陷入死循环。不但客⼾端cpu飙⾼,redis的QPS也会被拉高。 解决方法:通过让线程休眠(sleep)解决

解决⽅法:通过让线程休眠(sleep)解决

问题:消费延迟

描述:通过休眠会使得消费端延迟变大

解决方法:使用阻塞读代替非阻塞读,即blpop命令代替lpop

问题:异常处理

描述:空链接自动断开问题,即blpop有超时参数,当超时后redis会自动断开,导致blpop抛出异常;

解决方法:使用时注意捕获异常

延迟队列

使用zset数据结构作为延迟队列。将消息序列化为zset的member,消息到期时间作为score。通过zrangebyscore 命令来获取已经到达消费时间的消息。

问题:并发问题

描述:为了提高消费速度或者提高可用性,通常会有多个线程消费队列,如何确保任务不会被多次执行;

解决方法:通过zrem命令来抢占任务,zrem成功表示抢占成功,失败则表示已经被其他线程抢占。

问题:空转问题

描述:因为只有一个线程会抢占成功,其他线程则白取了任务;

解决方法:使用lua脚本优化,将zrangebyscore和zrem一同挪到服务端进行原子化操作。

总结

本文主要介绍了Redis丰富的使用场景,包括分布式锁、集群限流、消息队列、延迟队列,通过本文了解了Redis更加丰富的使用经验,以后可以对Redis的其他用法进行尝试,而不仅仅是停留在之前一直使用Redis作为缓存这一个初级阶段了。