广播
可用redis的pubsub机制来支持集群内的广播。
基于redis的分布式锁
加锁
使用setnx命令:
SET lock_key random_value NX PX 5000
其中:
random_value
是客户端生成的唯一的字符串,用于在删除时唯一标识client身份。
NX
代表只在键不存在时,才对键进行设置操作。
PX 5000
设置键的过期时间为5000毫秒,用于异常情况下(如client崩溃),该锁可以被自动释放,供其他client使用。
若命令成功,说明获取到锁。否则,表示未获取到锁。
解锁
使用lua脚本:
-- 只有唯一字符串匹配,才能认定身份,执行删除操作
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
redisson库
前面的内容只是为了说明基于redis分布式锁的实现原理,实际中,可直接使用redisson库的RLock,它在基本的加解锁之外,还提供了同一线程可重入持有锁的能力。
redisson库还提供了很多好用的分布式组件,值得好好学习一下。
分布式超时器
分布式超时器可基于redisson的分布式延迟队列(distributed delay queue)来实现,在此基础上做一些高可用的封装。
redisson分布式延迟队列原理
整个分布式延迟队列用到了3个redis数据结构:
- 时间最小堆,列头就是最近即将到期的消息
- 顺序消息队列,按设置的先后顺序排列的消息
- 目标消息队列,已到期的消息,待消费者拉取
生产者进程做了两件事:
- 往时间最小堆和顺序消息队列插入消息
- 定时从时间最小堆和顺序队列里把到期的消息移到目标队列
所以生产者是一个极其重要的角色。
消费者作用相对简单,就是消费到期消息。
设计思路
我们介绍一下分布式超时器的设计思路。
首先,有这么一个原则:超时事件的设置进程不一定是该超时事件的响应进程,因为设置进程有可能在某个时刻恰好挂掉或重启,但超时事件的处理不能丢(确保高可用)。
其次,任何分布式组件的设计难点都在于分布式条件下的异常处理,分布式超时器也不例外,我想到的有如下异常点:
- redis故障了怎么处理?
- 超时事件的处理进程未处理完就崩溃,怎么办?
- 所有进程都不在的情况下,超时事件会不会丢失,待进程起来后能不能补发?
- 灰度切换对超时事件的处理有无特殊要求?
对上述问题,我们逐一讨论解决:
- redis故障一般是断AZ的情况,一般redis集群里的master和slave节点分属不同的AZ,理论上不会出现master和slave同时挂掉的情况,那么在master 挂掉时,集群能自动把老master带的slave推举为新master,同时我们把redisson的readmode设为MASTER_SLAVE,以确保在master选举过程中,也能正常读取redis(readmode=MASTER_SLAVE时redisson在读master失败时会去读slave)。当然,由于主从复制的异步特点,新master选举出来后,小概率下可能存在数据丢失。这种情况下,可通过特殊机制去补偿,比如提前将超时事件存入mysql,并定期检查mysql里有无已过期的事件,若有就补偿处理掉。
- 处理超时事件的进程半途崩溃,超时事件未处理完。同样可在mysql里记录超时事件的处理状态,若有超时事件的处理状态一直是pending,则补偿处理。
- 如前所述,redisson的分布式延迟队列内部使用了三个专门的redis数据结构来存放未到期和到期的超时事件,所以,如果所有进程不在,这些超时事件还会保留在redis里,待进程恢复后,可以继续处理。
- 关于灰度,考虑到我们的实际情况是:新版本作为灰度存在只有很短的一段时间,大部分时间都是老版本作为灰度存在,分布式超时器框架可考虑不让灰度节点来处理超时事件(毕竟要做到后向兼容不是一件易事)