设计一个消息中间件的前置(三)| 数据结构及高可用设计

前两篇讲到了基本思路和netty+protobuf框架,今天这篇介绍一下前置的基本数据结构及用户校验,熔断的流程。

数据源及高可用

前置的数据源有两个,按照优先级来说,应该是这样的顺序:

  1. 缓存(Redis)
  2. 数据库(MySQL)——>前置内存

其中前置对缓存的访问使用实时读的方式,对数据库的访问使用定时读取全量数据,更新本地内存的方式。其主要思路是减少对数据库的并发访问,此外,考虑到缓存和数据库都可能出现宕机的情况,为了保证前置仍然可用,我们需要保证前置的内存中有一份最新的全量数据。

具体来说,每一次接收客户端校验请求时,我们优先从Redis获取数据,如果Redis客户端获取异常,则从内存中获取信息校验并返回给客户端。前置启动时需要开启一个定时线程,每隔一定时间同步Mysql数据和内存。

这个设计主要是为了保证服务的高可用,考虑数据源异常总共有四种场景:

  1. redis异常:可以更新权限信息,前置从mysql中定时同步数据;
  2. mysql异常:可以更新权限信息,前置优先从redis获取数据;
  3. mysql和redis都异常:无法更新权限信息,前置从本地内存中获取数据进行校验;
  4. 所有前置节点异常:启动应急方案,客户端直连mq集群,由mq上的权限校验功能来校验用户权限。

数据结构

在上一篇中介绍了protobuf的基础结构,发送端发送一个用户名+密码的组合,redis返回校验数据和路由地址。这个算是准入认证+路由机制,而进一步可以实现区分topic的权限校验。protobuf数据结构设计如下:

syntax = "proto3";
message request {
    string usrname = 1;
    string pwd = 2;
    string updateTime = 3;
}
message response {
    string retcode = 1;
    string updateTime = 2;
    string address = 3;
    map<string,string> permission = 4;
}

retCode有几种类型的值:

int NO_NEED_UPDATE = 1;                //无需校验
int WRONG_PASSWORD = 2;                //密码错误
int AUTHORIZATION_PASS = 3;            //校验通过
int SERVER_ERROR = 4;                  //服务异常

我在request中增加了一个updateTime,在response中增加了一个updateTime和类型为map的permission键值对结构。

updateTime是一个用于判断是否需要更新本地数据的开关,服务端收到request中的updateTime时,比对数据源中该用户的权限更新时间是否大于这个updateTime,如果不需要更新,则返回NO_NEED_UPDATE,否则就去数据源获取一下数据并带上数据源中的更新时间,生成response返回给客户端。这一设计的基本用途是减少无用的网络的传输和数据源访问操作。

permission这个map主要是用来存放从数据源中拿到的该用户的权限信息,返回给客户端后,在客户端本地保存一份。而后在生产者每次调用producer.send或者消费者每次调用consumer.poll方法时,在内存中校验一次是否有这个队列的权限。

基于request和response的结构,在尽量减少数据源读操作的情况下,Redis缓存和内存中的数据结构就很明确了:

键值对:
用户名 = 密码 
用户名:address = 路由地址
用户名:updateTime = 用户权限最后一次更新时间
MAP:
用户名:permission = {TOPICNAME = r/w/rw}

附上校验过程的伪代码:

if (checkPassword(request.getUserName) {
    if (checkUpdateTime(request.getUpdateTime) {
      return NO_NEED_UPDATE;
    } else {
      try {
          putAddressToResponse();
          putPermissionToResponse();
          putUpdateTimeToResponse();
          return AUTHORIZATION_PASS;
       } catch (Exception e) {
           return SERVER_ERROR;    //获取数据异常则返回服务异常
       }
    }
} else {
    return WRONG_PASSWORD;
}

熔断及服务降级设计

熔断和服务降级的机制不管触发条件是什么,我们首先要保证能提供熔断的能力。这里考虑使用准实时熔断机制。
客户端定时访问服务端,重复权限校验的流程,如果触发了熔断条件,数据源中的校验数据会被更新并反馈给客户端,客户端接收到WRONG_PASSWORD(拒绝该用户)时断开连接,并等待一定时间后重试。

消息中间件是一个可以很方便为系统实现服务降级功能的组件,只要将功能按照TOPIC来区分,经过TOPIC的权限校验,可以控制哪个TOPIC允许发送消息,哪个TOPIC允许接收消息。客户端在每次发送和接收时根据从前置上获取到的权限,进行一次复杂度为O(1)的判断,通过后才能进行发送和接收操作。如果是对某个topic的权限进行了更新,则会在对某个TOPIC的每次发送和接收操作时被拒绝,等待一段时间后重试。

当然,存放本地内存的方式其实只适合那些比较“遵守规则”的用户,也就是企业级内部用户,如果需要对外提供服务,权限校验功能可不能仅仅保存在客户端内存中,还应该在MQ上也加上配套的权限校验功能。

更进一步的方案

考虑这套方案,在熔断的设计上并不是即时生效的,时效性的提高可以考虑两种方案:

  1. 减少客户端两次校验的间隔时间,需要调优netty服务的效率
  2. 客户端改为长连接,需要配套心跳机制等。这时前置的整体架构就类似RocketMQ的NameServer了。

其实第一个方案中间隔时间减少到一定程度后,就可以考虑使用第二个方案了,毕竟每次创建连接的消耗是很大的。具体还是根据实际情况来考虑吧。

此外,上面写的这套方案中,每次服务端接收数据后都会在线程中阻塞式地访问数据源而后返回反馈,对更加大量并发的情况下,可以考虑使用异步处理的方式。

网络上有很多netty服务端优化的文章,这里就不一一列举了。

    原文作者:MisterCH
    原文地址: https://www.jianshu.com/p/e2cf6916c284
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞