分布式锁
- 分布式锁类似数据库锁,都是为了解决并发时非原子操作导致的问题而设计的。
- 设置分布式锁一般使用
setnx
(set if not exists) 指令,调用del
指令释放:SETNX mykey "Hello" del mykey
上锁->解锁
过程中可能死锁,故使用expire
命令加入过期时间:上锁->设置过期->解锁
:SETNX mykey "Hello" expire mykey 5 del mykey
上锁->设置过期
过程中可能死锁,Redis 2.8 之后setnx
命令和expire
命令可以一起作为原子操作执行(其中的NX
即为SETNX
的最新实现),这也是目前 Redis 官方推荐使用的 Redlock 方式:set mykey "Hello" EX 5 NX del mykey
超时问题
问题原因
如图所示,如果 Client 1 的执行时间超过了自己设置的锁过期时间,则该锁可能会被其他程序持有,产生数据异常:
(Source: How to do distributed locking)
解决方案
-
分布式锁不要用于较长时间的任务
-
为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key。使用 Lua 脚本将匹配 key 和 del 操作作为原子操作执行:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
可重入性
- 可重入性是指线程在持有锁的情况下再次请求加锁。
- Redis 可重入分布式锁要求包装客户端的 set 方法,使用线程的 Threadlocal 变量存储当前持有锁的计数。
- 由于可重入分布式锁会加重程序复杂性,一般建议优化代码逻辑,避免使用可重入分布式锁。
锁冲突
由于锁冲突导致无法成功加锁的情况,可以使用以下 3 种策略来处理客户端加锁失败的情况:
- 直接抛出异常:本质上是对当前请求的放弃,由用户决定是否重新发起新的请求。适合由用户直接发起的请求。
- sleep:会导致队列的后续消息处理出现延迟。死锁的 key 加锁失败会堵死线程。适合碰撞少 / 消息少的场景。
- 延时队列:加入延时队列稍后重试。适合异步消息处理,避免冲突。
消息队列
- Redis 队列适用于只有一组队列消息消费者的情况。
- Redis 的消息队列功能简单,没有太多高级特性。
- Redis 的消息队列没有 ack 保证,消息可靠性不足,适用于数据敏感性不强的场景。
异步消息队列
- 通常使用
list
结构来实现异步消息队列。 - 使用
rpush
/lpush
命令入队列,lpop
/rpop
命令出队列。
空队列
问题原因
队列为空时,客户端会陷入 pop 死循环的空轮询。空轮询会增加系统负载和 Redis 的 QPS,大大降低 Redis 的查询效率。
解决方案
- 线程闲置:使用 sleep 让线程闲置,降低轮询频率。
- 队列延迟:使用
blpop
/brpop
阻塞读命令(b-blocking)替代lpop
/rpop
作为消息消费者接受数据的方法,在队列没有数据的时候,会立即进入休眠状态。
ps. 线程阻塞时,Redis 客户端连接成为闲置连接,闲置超时会被服务器主动断开连接,导致阻塞读异常。使用时注意完善异常捕获和重连机制。
延时消息队列
- 通常使用
zset
来实现延时消息队列。 - 将消息序列化成字符串作为
zset
的value
,消息的到期处理时间作为score
,使用多线程轮询获取到期的任务进行处理。 - 多线程需要考虑并发争抢问题,通过
zrem
来决定唯一的属主,确保任务不能被多次执行:success = redis.zrem("delay-queue", value)
- 另外注意异常捕获,避免因为个别任务处理问题导致循环异常退出。
PubSub
-
Redis 消息队列不支持消息的多播机制,
PubSub
(PublisherSubscriber,发布者订阅者模型)是 Redis 实现多播模式的模块。 -
客户端发起订阅命令,获得订阅成功通知后,即可收到生产者发布的信息。如果当前没有消息,会返回空,所以 PubSub 默认不是阻塞的。可以使用 listen 来阻塞监听消息来进行处理。
-
Redis 不允许连接在 subscribe 等待消息时进行其它的操作。
> subscribe codehole.image codehole.text # 同时订阅 2 个主题 馈信息 1) "subscribe" 2) "codehole.image" 3) (integer) 1 1) "subscribe" 2) "codehole.text" 3) (integer) 2 > psubscribe codehole.* # 模式匹配订阅 以收到 1) "psubscribe" 2) "codehole.*" 3) (integer) 1 > publish codehole.image https://www.google.com/dudo.png (integer) 1 > publish codehole.text " 你好,欢迎加入码洞 " (integer) 1 > publish codehole.blog '{"content": "hello, everyone", "title": "welcome"}' (integer) 1
-
PubSub 消费者接收的消息格式形如:
{ 'pattern': None, 'type': 'message', 'channel': 'codehole', 'data': 'python comes' }
data
:消息内容。字符串格式。channel
:订阅的主题名称。type
:消息类型。普通消息为 message;控制消息为 subscribe;模式订阅的反馈为 psubscribe;取消订阅指令为 unsubscribe 和 punsubscribe。pattern
:订阅模式。通过 subscribe 指令订阅的主题,该字段为空。
-
PubSub 的槽点比较致命,一般不推荐将 PubSub 应用于消息队列场景:
- 生产者传递消息时,如果没有对应消费者,消息将被丢弃。
- PubSub 的消息不会持久化,如果消费者宕机导致没有及时接收到,则将丢失消息。