文章目录
间接通信的本质是通过一个中介者通信,因此不存在发送者和一个或多个接收者之间的直接耦合。此之谓解耦合。
解耦合主要有两种:
1 空间解耦合:发送者不需要知道他们 正在发给谁
2 时间解耦合:发送者和接受者不需要同时存在
加餐:名字服务,其实也是为了解耦哦!!!!
间接通信主要有如下几种实现方式:
-
组通信,在组通信中,通信通过一个抽象的组进行,发送者并不知道接收者的身份。
-
发布一订阅系统,代表-类方法,这类方法的共同特点是通过中介者将事件分发给多个接收者。
-
消息队列系统,其中消息发送到队列中,接收者从这些队列中提取消息。
-
基于共享内存的方法,包括分布式共享内存和元组空间两种方法,给编程人员提供-一个抽象的全局共享内存抽象。
这里主要是结合Redis 的 发布-订阅系统来继续熟悉 Redis 。
概述
发布一订阅系统别称基于事件的分布式系统。发布者发布结构化的事件到事件服务,订阅者( subscriber)通过订阅( subscription)表达对特定事件感兴趣,其中订阅可以是结构化事件之上的任意模式
。例如,一个订阅者可以表达对所有和本书相关的事件感兴趣,如新版本的出版或者相关的网站更新。发布-订阅系统的任务是把订阅与发布的事件进行匹配,保证事件通知( event notification) 的正确传递。一个给定的事件将被传递到许多潜在的订阅者,因此,发布-订阅本质上是一对多的通信范型。
OK,那么如何区分事件的类型呐?系统的分发又如何完成呐?这里提供了三种方式:
- 基于渠道:在这种方法中,发布者发布事件到命名的渠道,
订阅者订阅其中一个已命名的渠道,并接收所有发送到那个渠道的事件
。 - 基于主题:这种方法中,我们
假设每个通知用- -定数量的域来表达,其中的-一个域表示主题。订阅是根据感兴趣的主题来定义的
。这个方法等价于基于渠道的方法,不同的是基于渠道的方法中的主题是隐式的,而基于主题的方法中的主题作为- -个域被显式地声明了。通过层次化地组织主题,可以增强基于主题的方法的表达能力。这和位图是不是很像
- 基于内容:基于内容的方法是基于主题方法的-般化,它允许订阅表达式具有一个事件通知上的多个域。更具体来说,基于内容的过滤器是用事件属性值的约束组合定义的查询。
类似于布隆过滤器
- 基于类型的实现
一个简单实例:
为了研究Redis中的发布与订阅功能,我们顺便把 Redis 的 Sentinel 讲一下。
Redis中的Sentinel
之所以有这个功能主要是为了支持Redis集群的高可用性,保证机器发生故障时,自动发现,自动复制转移恢复。
由一个或多个Sentinel实例( instance)组成的Sentinel系统( system)可以监视任意多个主服务器, Sentinel系统以及这些主服务器属下的所有从服务器, 并在被监视的主服务器进人下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。由新的主服务器继续处理命令请求。
- 检测到下线,向所有 server1 下的从服务器发送新的复制命令,从新选出的主服务器进行复制操作。故障转移完成。
- 会继续监视下线的 server1,当其重新上线时,设置为新主服务器的从服务器。
那么Sentinel 是如何判断服务器是否在线的呐?别急,我们先来点前提知识点。
Sentinel 服务器
每一个 Sentinel 服务器也会有一个状态,这个结构就会保存服务器中所有和Sentinel功能有关的状态。一般状态还是和原来一样。在redisState 中保存。
/* Main state. */
/* Sentinel 的状态结构 */
struct sentinelState {
// 当前纪元
uint64_t current_epoch;
****注意这里:
// 保存了所有被这个 sentinel 监视的主服务器
// 字典的键是主服务器的名字
// 字典的值则是一个指向 sentinelRedisInstance 结构的指针
dict *masters;
// 是否进入了 TILT 模式?
int tilt;
// 目前正在执行的脚本的数量
int running_scripts;
// 进入 TILT 模式的时间
mstime_t tilt_start_time;
// 最后一次执行时间处理器的时间
mstime_t previous_time;
// 一个 FIFO 队列,包含了所有需要执行的用户脚本
list *scripts_queue;
} sentinel;
master 键是监视主服务器的名字,值是监视主服务器的实例,master的初始值是从配置文件来的。
具体结构如下:
typedef struct sentinelRedisInstance {
// 标识值,记录了实例的类型,以及该实例的当前状态
int flags; /* See SRI_... defines */
// 实例的名字
// 主服务器的名字由用户在配置文件中设置
// 从服务器以及 Sentinel 的名字由 Sentinel 自动设置
// 格式为 ip:port ,例如 "127.0.0.1:26379"
char *name; /* Master name from the point of view of this sentinel. */
// 实例的运行 ID
char *runid; /* run ID of this instance. */
// 配置纪元,用于实现故障转移
uint64_t config_epoch; /* Configuration epoch. */
// 实例的地址:ip + port
sentinelAddr *addr;
// 用于发送命令的异步连接
redisAsyncContext *cc;
// 用于执行 SUBSCRIBE 命令、接收频道信息的异步连接
// 仅在实例为主服务器时使用
redisAsyncContext *pc;
// 已发送但尚未回复的命令数量
int pending_commands;
// cc 连接的创建时间
mstime_t cc_conn_time;
// pc 连接的创建时间
mstime_t pc_conn_time;
// 最后一次从这个实例接收信息的时间
mstime_t pc_last_activity;
// 实例最后一次返回正确的 PING 命令回复的时间
mstime_t last_avail_time;
// 实例最后一次发送 PING 命令的时间
mstime_t last_ping_time;
// 实例最后一次返回 PING 命令的时间,无论内容正确与否
mstime_t last_pong_time;
// 最后一次向频道发送问候信息的时间
// 只在当前实例为 sentinel 时使用
mstime_t last_pub_time;
// 最后一次接收到这个 sentinel 发来的问候信息的时间
// 只在当前实例为 sentinel 时使用
mstime_t last_hello_time;
// 最后一次回复 SENTINEL is-master-down-by-addr 命令的时间
// 只在当前实例为 sentinel 时使用
mstime_t last_master_down_reply_time;
// 实例被判断为 SDOWN 状态的时间
mstime_t s_down_since_time; /* Subjectively down since time. */
// 实例被判断为 ODOWN 状态的时间
mstime_t o_down_since_time; /* Objectively down since time. */
// SENTINEL down-after-milliseconds 选项所设定的值
// 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)
mstime_t down_after_period; /* Consider it down after that period. */
// 从实例获取 INFO 命令的回复的时间
mstime_t info_refresh; /* Time at which we received INFO output from it. */
// 实例的角色
int role_reported;
// 角色的更新时间
mstime_t role_reported_time;
// 最后一次从服务器的主服务器地址变更的时间
mstime_t slave_conf_change_time;
/* Master specific. */
/* 主服务器实例特有的属性 -------------------------------------------------------------*/
// 其他同样监控这个主服务器的所有 sentinel
dict *sentinels; /* Other sentinels monitoring the same master. */
// 如果这个实例代表的是一个主服务器
// 那么这个字典保存着主服务器属下的从服务器
// 字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelRedisInstance 结构
dict *slaves;
// SENTINEL monitor <master-name> <IP> <port> <quorum> 选项中的 quorum 参数
// 判断这个实例为客观下线(objectively down)所需的支持投票数量
int quorum; /* Number of sentinels that need to agree on failure. */
// SENTINEL parallel-syncs <master-name> <number> 选项的值
// 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
int parallel_syncs;
// 连接主服务器和从服务器所需的密码
char *auth_pass;
/* Slave specific. */
/* 从服务器实例特有的属性 -------------------------------------------------------------*/
// 主从服务器连接断开的时间
mstime_t master_link_down_time; /* Slave replication link down time. */
// 从服务器优先级
int slave_priority; /* Slave priority according to its INFO output. */
// 执行故障转移操作时,从服务器发送 SLAVEOF <new-master> 命令的时间
mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF <new> */
// 主服务器的实例(在本实例为从服务器时使用)
struct sentinelRedisInstance *master; /* Master instance if it's slave. */
// INFO 命令的回复中记录的主服务器 IP
char *slave_master_host; /* Master host as reported by INFO */
// INFO 命令的回复中记录的主服务器端口号
int slave_master_port; /* Master port as reported by INFO */
// INFO 命令的回复中记录的主从服务器连接状态
int slave_master_link_status; /* Master link status as reported by INFO */
// 从服务器的复制偏移量
unsigned long long slave_repl_offset; /* Slave replication offset. */
/* Failover */
/* 故障转移相关属性 -------------------------------------------------------------------*/
// 如果这是一个主服务器实例,那么 leader 将是负责进行故障转移的 Sentinel 的运行 ID 。
// 如果这是一个 Sentinel 实例,那么 leader 就是被选举出来的领头 Sentinel 。
// 这个域只在 Sentinel 实例的 flags 属性的 SRI_MASTER_DOWN 标志处于打开状态时才有效。
char *leader;
// 领头的纪元
uint64_t leader_epoch;
// 当前执行中的故障转移的纪元
uint64_t failover_epoch;
// 故障转移操作的当前状态
int failover_state;
// 状态改变的时间
mstime_t failover_state_change_time;
// 最后一次进行故障迁移的时间
mstime_t failover_start_time;
// SENTINEL failover-timeout <master-name> <ms> 选项的值
// 刷新故障迁移状态的最大时限
mstime_t failover_timeout;
mstime_t failover_delay_logged;
// 指向被提升为新主服务器的从服务器的指针
struct sentinelRedisInstance *promoted_slave;
// 一个文件路径,保存着 WARNING 级别的事件发生时执行的,
// 用于通知管理员的脚本的地址
char *notification_script;
// 一个文件路径,保存着故障转移执行之前、之后、或者被中止时,
// 需要执行的脚本的地址
char *client_reconfig_script;
} sentinelRedisInstance;
串起来就是:
创建连向主服务器的网络连接
初始化Sentinel的最后一步是创建连向被监视主服务器的网络连接,Sentinel 将成为主服务器的客户端
,它可以向主服务器发送命令,并从命令回复中获取相关的信息。
对于每个被Sentinel监视的主服务器来说,Sentinel会创建两个连向主服务器的异步网络连接:
- 一个是命令连接,这个连接专门用于向主服务器发送命令,并接收命令回复。
- 另-个是订阅连接,这个连接专门用于订阅主服务器的_ sentinel_ :hello 频道。
为什么需要两个连接?
在Redis目前的发布与订阅功能中,
被发送的信息都不会保存在Redis服务器里面,如果在信息发送时,想要接收信息的客户端不在线或者断线,那么这个客户端就会丢失这条信息
。因此,为了不丢失_ sentine1__ :hello 频道的任何信息,Sentinel 必须专门用一个订阅连接来接收该频道的信息。实现时间上的解耦合。
另一方面,除了订阅频道之外, Sentinel 还必须向主服务器发送命令,以此来与主服务器进行通信,所以Sentinel还必须向主服务器创建命令连接。
- Sentinel 通过每十秒一次的频率,发送命令INFO来获取主服务器的相关信息,其中就会包含主服务器下的从服务器的信息
- 从服务器信息由字典
slaves
保存。 - Sentinel 也会创建到从服务器的命令连接和订阅连接。也会通过每十秒一次的频率,发送命令INFO来获取从服务器的相关信息。
Sentinel 与服务器的具体传送信息流程就是这样子的:
另外,就和订阅报纸一样,所有订阅l频道的 Sentinel 也会收到相关信息。
其中的信息也在sentinelRedisInstance
的Sentinels
中保存。
另外,Sentinel 之间也会创建命令连接。
Sentinel之间不会创建订阅连接
Sentinel在连接主服务器或者从服务器时,会同时创建命令连接和订阅连接,但是在连接其他Sentinel时,却只会创建命令连接,而不创建订阅连接。这是因为Sentinel需要通过接收主服务器或者从服务器发来的频道信息来发现未知的新Sentinel,所以才需要建立订阅连接,而相互已知的Sentinel 只要使用命令连接来进行通信就足够了。
ok ,终于铺垫完了。
这里的下线分为主观和客观,主观就是某Sentinel认为它下线,这里就有可能是网络故障导致的,所以只有客观下线我们才认为他是真正发生故障了。
如何检测主观下线状态
在默认情况下,Sentinel 会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。
- 有效回复:实例返回+PONG、-L0ADING、MASTERDOWN三种回复的其中一种。
- 无效回复:实例返回除+PONG、-L0ADING、MASTERDOWN三种回复之外的其他回复,或者在指定时限内没有返回任何回复。
时限由配置文件决定。
如果超时,会设置为主观下线。
如何检测客观下线状态
判断主观下线时,也会询问其他的 Sentinel看看是否下线,如果到达一定数量就会转为客观下线。
询问命令如下:
接受来自其他 Sentinel 的询问的回复命令如下:
如何选举领头Sentinel - Raft 算法
当-个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel 会进行协商,选举出一个领头Sentinel, 并由领头Sentinel对下线主服务器执行故障转移操作。
- 所有在线的Sentinel都有被选为领头Sentinel 的资格
- 每次进行领头Sentinel选举之后,不论选举是否成功,所有Sentinel的配置纪元 ( configuration epoch )的值都会自增一次。
配置纪元实际上就是-一个计数器,并没有什么特别的。其实就是任期。
- 在一个配置纪元里面,所有Sentinel都有一次将某个Sentinel设置为局部领头Sentinel的机会,并且局部领头- 旦设置,在这个配置纪元里面就不能再更改。
- 每个发现主服务器进人客观下线的Sentinel都会要求其他Sentinel将自己设置为局部领头Sentinel。
-
当一个Sentinel (源Sentinel)向另一个Sentinel (目标Sentinel )发送SENT INEL is-master-down-by-addr命令,并且命令中的runid参数不是*符号而是源Sentinel的运行ID时,这表示源Sentinel要求目标Sentinel将前者设置为后者的局部领头Sentinel。
-
Sentinel 设置局部领头Sentinel的规则是
先到先得
:最先向目标Sentinel发送设置要的源Sentinel将成为目标Sentinel的局部领头Sentinel,而之后接收到的所有设置要求都会被目标Sentinel拒绝。 -
目标Sentinel在接收到SENTINEL is-master-down-by-addr 命令之后,将向源Sentinel返回- .条命令回复,回复中的leader_ runid 参数和 leader_ epoch 参数分别记录了目标Sentinel的局部领头Sentinel的运行ID和配置纪元。
-
源Sentinel在接收到目标Sentinel返回的命令回复之后,会检查回复中leader_epoch参数的值和自己的配置纪元是否相同,如果相同的话,那么源Sentinel 继续取出回复中的leader_ runid 参数,如果leader_ runid 参数的值和源Sentincl的运行ID -致,那么表示目标Sentinel将源Sentinel设置成了局部领头Sentinel。口如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel
具体实现应该不会深究,但是Raft算法要大致了解。
故障转移
选主服务器的方法就是疯狂筛选,最后找到一个数据最新的节点。数据最新就意味着偏移量最大。
Redis中的发布与订阅功能
具体命令罗列:
命令 | 含义 |
---|---|
PUBLISH | $1600 |
SUBSCRIBE | 订阅频道 |
PSUBSCRIBE | 订阅模式,类似于正则匹配 |
频道的订阅与退订
当一个客户端执行SUBSCRIBE命令订阅某个或某些频道的时候,这个客户端与被订阅频道之间就建立起了一种订阅关系。
Redis将所有频道的订阅关系都保存在服务器状态的pubsub_channels字典里面
,这个字典的键是某个被订阅的频道,而键的值则是一个链表,链表里面记录了所有订阅这个频道的客户端:
struct redisServer {
//保存所有频道的订阅关系dict *pubsub_ channels;
// ...
};
那么所谓的订阅和退订都只是链表节点的添加和删除了
模式的订阅与退订
与上相同,服务器也将所有模式的订阅关系都保存在服务器状态的pubsub_patterns属性里面:
struct redisServer {
// ...
//保存所有模式订阅关系
list *pubsub_patterns;
// ...
};
发送消息
很简单喽
查看订阅消息
命令:PUBSUB
- PUBSUB CHANNEL :返回。。。
- PUBSUB numsub :返回频道订阅者数目
- PUBSUB numpat:返回模式订阅者数目
参考:
《分布式系统概念与设计》(这他妈应该是本好书,但是我看不懂啊)
《Redis设计与实现》