消息中间件数据库
1.消息中间件的价值安全
1.1 透过示例看消息中间件对应用的解耦服务器
1.1.1.经过服务调用让其余系统感知事件发生的方式网络
假设咱们要作一个用户登陆系统,其中须要支持的一个功能是,用户登陆成功 后发送一条短信到用户的手机,算是一个用户安全的选项,如图6-2所示。异步
而后,咱们须要把用户登陆的信息(时间、IP、用户名等数据)传给咱们的安全 系统,安全系统会进行安全策略相关的处理和判断。此时的结构会变成图6-3所示的 样子。分布式
这样看起来还好,可是若是再增长一些登陆成功后须要被调用的系统呢?如图 6-4所示。post
这会让登陆系统变得很是复杂。每增长一个在登陆成功后须要被调用的系统, 就须要修改登陆系统来进行相关的调用。优雅一点的实现是把这个登陆成功后的服 务调用变为一种可扩展的配置,甚至能够动态生效,但这只能下降变动时的开发和 部署成本,并无下降复杂性。登陆系统要被迫依赖很是多的系统。优化
1.1.2.经过引入消息中间件解耦服务调用spa
咱们思考一下,若是从登陆系统的角度来看,这些系统是登陆系统必须依赖的 吗?答案是否认的,.登陆系统只须要验证用户名和密码的合法性(也许还会包含验证码等登陆验证合法性的必须要素),因此登陆系统必须依赖的是可以提供用户名、 密码的系统,而图6-4中的系统其实都不是登陆系统必须依赖的系统。相反,这些系统是必须依赖登陆系统的,由于它们都关心登陆是否成功这件事情。code
在这样的场景中,咱们须要经过消息中间件把上面的结构解耦,上面结构中的服务调用将会被固定格式的消息的传递所取代。登陆系统负责向消息中间件发送消 息,而其余的系统则向消息中间件来订阅这个消息,而后完成本身的工做,如图6-5 所示。
经过消息中间件解耦,登陆系统就不用关心到底有多少个系统须要知道登陆成 功这件事了,也不用关心如何通知它们,只须要把登陆成功这件事转化为一个消息 发送到消息中间件就能够了。这样,须要了解登陆成功这件事的系统本身去消息中 间件订阅就好了。而且各个系统之间也是互不影响的。
这里须要注意一点,就是当登陆成功时须要向消息中间件发送一个消息,那么 咱们必须保证这个消息发送到了消息中间件,不然依赖这个消息的系统就没法工做 了。这个问题有一个不太优雅的解决方式,如图6-6所示。
图6-6所示的思路是,咱们在数据库中记录状态,而后让用到这个状态的系统本身来查。这个记录状态的数据库是操做中必定会依赖的数据库,若是它出问题就会致使对状态的记录不成功,业务操做也就不会成功了。图6-6所示是把状态记录在用户库的用户记录上了。不过这个例子是有一个小问题的,就是若是用户读信息时数据库正常,这时就能完成密码的验证,可是若是去记录状态时数据库不可用, 那就仍是有问题的(后面咱们会看到如何解决)。若是这里只是对数据库的写操做 的话,那就没有问题了,例如修改用户信息的操做,那么是能够在一个SQL中完成用户信息的更改并设置要发送短信这个状态,这样能够保证操做自己和状态更新的原子性。
对于须要感知状态的应用来讲,须要定时轮询数据库以查看状态,而且在作完操做后,须要更改状态从而使得下次就不用再处理了。
能够说这是一个能解决问题的work around方法,实现也比较简单。不过也存在如下几个问题:
•增长了业务数据库的负担。一个状态字段所占的空间还能够接受,可是这个数据库须要被其余系统持续地定时轮询,而且进行更新,这就大大增长了数据库的负担。
•依赖的复杂和不安全。该方案使得发送短信的服务要依赖业务数据库,这致使依赖复杂而且不合理,另外,发送短信的服务对数据库记录有修改的权限, 这也不安全。
•扩展性很差。对于前面的多个须要在业务动做成功后来作后续工做的系统, 若是把该方式用于这样的系统的话,咱们就须要增长不少个字段,或者使这 些字段变得可共享又相互不能影响。而且会增长大量的定时对业务数据库的 轮询请求。
对于这些问题,咱们也指望经过消息中间件来解决。
2.互联网时代的消息中间件
在开始介绍互联网时代的消息中间件前,咱们必须讲一下JMS.JMS是Java Message Service的缩写,它是Java EE (企业版Java)中的一个关于消息的规范,而Hometq、 ActiveMQ等产品是对这个规范的实现。若是是企业内部或者一些小型的系统,直接使 用JMS的实现产品是一个经济的选择,而在大型系统中有一些场景不适合使用JMS。
在大型互联网中,咱们采用消息中间件能够进行应用之间的解耦以及操做的异步,这是消息中间件的两个最基础的特色,也正是咱们须要的。在此基础上,咱们着重思考的是消息的顺序保证、扩展性、可靠性、业务操做与消息发送一致性,以 及多集群订阅者等方面的问题,这些内容会在后面的小节中呈现给读者。咱们从上 一节提到的保证消息必定被处理开始介绍。
2.1 如何解决消息发送一致性
2.1.1消息发送一致性的定义
首先,咱们须要弄清楚消息发送一致性到底是什么。消息发送一致性是指产生消息的业务动做与消息发送的一致,就是说,若是业务操做成功了,那么由这个操做产生的消息必定要发送出去,不然就丢失消息了。而另外一方面,若是这个业务行为没有发生或者失败,那么就不该该把消息发出去。
2.1.2消息发送一致性很难保证吗
若是要写处理业务逻辑的代码和发送消息的代码,该怎么写呢?
下面是一段伪代码,是在某些实践中的用法。从中能够看到如下两个问题。
void fool (){
//业务操做
//例如写数据库,调用服务等
//发送消息 }
•业务操做在前,发送消息在后,若是业务失败了还好(固然业务本身不以为好),若是成功了,而这时这个应用出问题,那么消息就发不出去了。
•若是业务成功,应用也没有挂掉,可是这时消息系统挂掉了,也会致使消息发不出去。 咱们来看另一种作法,伪代码以下:
void fool () {
//发送消息 //业务操做
//例如写数据库,调用服务等 }
这种方式更不可靠,在业务尚未作时消息就发出了。
在具体的工程实践中,第一种作法丢失消息的比例相对是很低的。固然,对于要求必须保证一致性的场景,上面的两种方案都不能接受。
2.1.3你们熟知的JMS有办法吗
使用JMS能够实现消息发送一致性吗?咱们来看看JMS发送消息的部分。首先看看JMS中几个比较重要的要素。
在JMS消息模型中,有Queue和Topic (在后面会详细介绍)之分,因此,前面的 Destination、ConnectionFactory、Connection、Session、MessageConsumer、 MessageProducer都有对应的子接口。表6-1显示了前面各要素在Queue模型(PTP Domain)和 Topic 模型(Pub/Sub Domain)下的对应关系。
此外,在JMS的API中,咱们看到不少以XA开头的接口,它们其实就是支持 XA协议的接口,它们与表6-1中各要素的对应关系如表6-2所示。
能够看到,XA 系列的接口集中在 ConnectionFactory、Connection 和 Session 上, 而 MessageProducer、QueueSender、TopicPublisher、MessageConsumer、QueueReceiver 和TopicSubscriber则没有对应的XA对象。这是由于事务的控制是在Session层面上 的,而 Session 是经过 Connection 建立的,Connection 是经过 ConnectionFactory 建立 的,因此,这三个接口须要有XA系列对应的接口的定义。Session、Connection、 ConnectionFactory在Queue模型和Topic模型下对应的各个接口也存在相应的XA系 列的对应接口。
下面展现了消息最重要的要素(消息、发送者、接收者)与几个基本元素之间 的关系。
ConnectionFactory->Connection->Session->Message Destination + Session-^ MessageProducer Destination + Sessoin-^ MessageConsumer
在JMS中,若是不使用XA系列的接口实现,那么咱们就没法直接获得发送消息给消息中间件及业务操做这两个事情的事务保证,而JMS中定义的XA系列的接口就是为了实现分布式事务的支持(发送消息和业务操做很难作在一个本地事务中, 后面会讲到一些变通的作法)。可是这会带来以下问题。
•引入了分布式事务,这会带来一些开销并增长复杂性。
•对于业务操做有限制,要求业务操做的资源必须支持XA协议,才可以与发送消息一块儿来作分布式事务。这会成为一个限制,由于并非全部须要与发送消息一块儿作成分布式事务的业务操做都支持XA协议。
2.1.4 有其余的办法吗
从上节能够看到,jMS是能够解决消息发送一致性的问题的,可是存在一 些限制而且成本相对较高。那么,咱们有没有其余的办法呢?
咱们来思考一下要解决的问题,咱们但愿保证业务操做与发送相关消息的动做是一致的,而前面的简单方案不能彻底保证,可是出现问题的几率并不大,因此, 咱们但愿找到一种解决方案,这种方案对正常流程的影响要尽量小,而在有问题 的场景能解决问题。
从这个方面看,即使能够作到业务操做都是支持XA的,若是采用这样的方式引人两阶段提交的话,那么仍是把方案作得有些重了。
针对这个问题,咱们能够用图6-7所示的方案来解决,流程介绍以下。
(1) 业务处理应用首先把消息发给消息中间件,标记消息的状态为待处理。
(2) 消息中间件收到消息后,把消息存储在消息存储中,并不投递该消息。
(3) 消息中间件返回消息处理的结果(仅是入库的结果),结果是成功或者失败。
(4) 业务方收到消息中间件返回的结果并进行处理:
a) 若是收到的结果是失败,那么就放弃业务处理,结束。
b) 若是收到的结果是成功,则进行业务自身的操做。
(5) 业务操做完成,把业务操做的结果发送给消息中间件。
(6) 消息中间件收到业务操做结果,根据结果进行处理:
a) 若是业务失败,则删除消息存储中的消息,结束。
b) 若是业务成功,则更新消息存储中的消息状态为可发送,而且进行调度,进行消息的投递。
这就是整个流程。在这里读者必定会有一个疑问,即在最简单的版本中,咱们 只有业务操做和发消息两步,仍然会可能产生不少异常,那么如今这个过程的步骤 更多,产生异常的可能点更多,是如何可以保证业务操做和发送消息到消息中间件 是一致的呢?
咱们对每个步骤可能产生的异常状况来进行分析。
(1) 业务应用发消息给消息中间件。若是这一步失败了,不管是网络的缘由还 是消息中间件的缘由,或是业务应用自身的缘由,咱们都会看到业务操做没有作, 消息也没有被存储在消息中间件中,业务操做和消息的状态是同样的,没有问题。
(2) 消息中间件把消息入库。若是这一步失败,不管是消息存储有问题,仍是 消息中间件收到业务消息后有问题,或是网络问题,可能形成的结果有两个。一个 是消息中间件失效,那么业务应用是收不到消息中间件的返回结果的;二是消息中 间件插人消息失败,而且有能力返回结果给应用,这时消息存储中都没有消息。
(3) 业务应用接收消息中间件返回结果异常。这里出现异常的缘由多是网络、 消息中间件的问题,也多是业务应用自身的问题。若是业务应用自身没问题,那 么业务应用并不知道消息在消息中间件的处理结果,就会按照消息发送失败来处理, 若是这时消息在消息中间件那里入库成功的话,就会形成不一致。若是是业务应用 有问题,那么若是消息在消息中间件中处理成功的话,也就会形成不一致了;若是 未处理成功,则仍是一致的。
(4) 业务应用进行业务操做。这一步不会产生太大问题。
(5) 业务应用发送业务操做结果给消息中间件。若是这一步出现问题,那么消 息中间件将不知道该如何处理已经存储在消息存储中的消息,可能会形成不一致。
(6) 消息中间件更新消息状态。若是这一步出现问题,与上一步所形成的结果 是相似的。
从上面的分析能够看出,须要了解的两个主要的控制状态和流程的节点就是业务应用和消息中间件,咱们能够分别从业务应用和消息中间件的视角来梳理一下, 如表6-3和表6-4所示。
从上面的梳理和分析能够看到,对于各类异常状况咱们遇到的状态有以下三种:
这三种状况中,第一种状况不须要进行额外的处理,由于自己就是一致的;第 二种和第三种都须要了解业务操做的结果,而后来处理已经在消息存储中、状态是待处理的消息。
那么如何了解业务操做的结果呢?
图6-8展现了这个过程。由消息中间件主动询问业务应用,获取待处理消息所对应的业务操做的结果,而后业务应用须要对业务操做的结果进行检查,而且把结果发送给消息中间件(业务处理结果有失败、成功、等待三种,等待是多出来的一种状态,表明业务操做还在处理中),而后消息中间件根据这个处理结果,更新消息状态。能够说这是发送消息的一个反向的流程。
一样,这个流程也会出现不少异常。不过这个4步的流程就是为了确认业务处理操做结果,真正的操做只是根据业务处理结果来更改消息的状态,因此,前面3 步都与查询相关,若是失败就失败了,而最后一步的更新状态若是失败了,那么就定时重复这个反向流程,重复查询就能够了。
发送消息的正向流程和检查业务操做结果的反向流程合起来,就是解决业务操做与发送消息一致性的方案。在大多数的状况下,反向流程是不须要工做的。咱们来看看正向流程是否带来了额外的负担,对好比表6-5所示。
从上面的对比能够看到,解决一致性的方案是只增长了一次网络操做和一次更新存储中消息状态的操做,就是第5步和第6步两步。而前面4步和传统方式所作的事情都同样,只是顺序有所不一样。因此,总体上带来的额外开销并不大,并且还 有可优化的点。
接着来看一下使用方式。能够看到解决一致性的方案中,在业务应用那里是有一个固化的流程的,能够提供一个封装来方便业务应用的使用,伪代码以下。
Result postMessage(Message, PostMessageCallback){
//发送消息给消息中间件 //获取返回结果 //若是失败,返回失败 //进行业务操做
//获取业务操做结果 / /发送业务操做结果给消息中间件 //返回处理结果 }
能够看到,咱们能够把实现逻辑封装在一个调用中,而后把业务的操做包装成 一个对象传进来,而后整个流程就能够控制在这个方法中了。固然,除了发送一致性的消息以外,也应该提供一个传统的发送消息的接口, 也就是不支持发送一致性的发送接口。此外,为了适应其余的场景(例如与现有的事务处理流程结合等),也会提供独立的接口,就会把这个流程的控制权交给业务应用自身。
2.2 如何解决消息中间件与使用者的强依赖问题
回顾一下解决业务操做和发送消息一致性的方案,会发现咱们更多地关注了如何保持和解决一致性的问题,可是忽略了一个问题,那就是消息中间件变成了业务应用的必要依赖。也就是说,若是消息中间件系统(包括使用的消息存储、业务应用到消息中间件的网络等)出现问题,就会致使业务操做没法继续进行,即使当时业务应用和业务操做的资源都是可用的。
咱们须要思考如何解决这个问题,思路有以下三种:
•提供消息中间件系统的可靠性,可是没有办法保证百分之百可靠。
•对于消息中间件系统中影响业务操做进行的部分,使其可靠性与业务自身的可靠性相同。
•能够提供弱依赖的支持,可以较好地保证一致性。
第一种方案,提高消息中间件系统的可靠性是必需要作的事情,可是咱们没法保证百分之百可靠。
第二种方案,让消息中间件系统中影响业务操做的部分与业务自身具备一样的可靠性,其实就是要保证若是业务能操做成功,就须要消息可以入库成功。由于如 果消息中间件出问题了,能够接受投递的延迟,可是须要保证消息入库,这样业务操做才能够继续进行。那么,可行的方式只有一种,如图6-9所示。
咱们把消息中间件所须要的消息表与业务数据表放到同一个业务数据库中,这 样,业务应用就能够把业务操做和写人消息做为一个本地事务来完成,而后再通知消息中间件有消息能够发送,这样就解决了一致性的问题。从图6-9中能够看到这一 步是虚线表示的,表明它不是一个必要的操做和依赖。消息中间件会定时去轮询业务数据库,找到须要发送的消息,取出内容后进行发送。这个方案对业务系统有如 下三个影响:
•须要用业务本身的数据库承载消息数据。
•须要让消息中间件去访问业务数据库。
•须要业务操做的对象是一个数据库,或者说支持事务的存储,而且这个存储必须可以支持消息中间件的需求。
咱们在上面的基础上进行一下变通,如图6-10所示。这个方案和图6-9中方案的区别是,消息中间件再也不直接与业务数据库打交道。消息表仍是放在业务数据库 中,彻底由业务数据库来控制消息的生成、获取、发送及重试的策略。这样,消息 中间件就不须要与众多使用这种消息一致性发送的业务方的数据库打交道了,不过 比较多的逻辑是从消息中间件的服务端移动到消息中间件的客户端,而且在业务应 用上执行。消息中间件更多的是管理接收消息的应用,而且当有消息从业务应用发 过来后就只管理投递,把原来的调度、重投、投递等逻辑分到了客户端和服务端 两边。
图6-9和图6-10中的两种方式虽然已经解决了大部分问题,可是它们都要求业务操做是支持事务的数据库操做,具备必定的限制性,这里咱们能够再进行一 下变通。
咱们考虑把本地磁盘做为一个消息存储,也就是若是消息中间件不可用,又不肯或不能侵入业务本身的数据库时,能够把本地磁盘做为存储消息的地方,等待消息中间件回复后,再把消息送到消息中间件中(如图6-11所示)。全部的投递、重试等管理,仍然是在消息中间件进行,而本地磁盘的定位只是对业务应用上发送消息 必定成功的一个保证。
这种方式存在的风险是,若是消息中间件不可用,并且写入本地磁盘的数据也 坏了的话,那么消息就丢失了。这确实是个问题,因此,从业务数据上进行消息补发才是最完全的容灾的手段,由于这样才能保证只要业务数据在,就必定能够有办法恢复消息了。
将本地磁盘做为消息存储的方式有两种用法,一是做为一致性发送消息的解决方案的容灾手段,也就是说该方式平时不工做,出现问题时才切换到该方式上,二 是直接使用该方式来工做,这样能够控制业务操做自己调用发送消息的接口的处理时间,此外也有机会在业务应用与消息中间件之间作一些批处理的工做。
最后,咱们来看一下业务操做与发送消息一致性的方案所带来的两个限制。
•须要肯定要发送的消息的内容。由于咱们在业务操做作以前会把状态标记为待处理,这要求先能肯定消息内容;这里能够有一个变通,即先把主要内容也就是可以标记该次业务操做特色的信息发过来,而后等业务操做结束后须要更新状态时再补全内容。不过这仍是要求在业务操做以前可以肯定一些索引性质的信息。
•须要实现对业务的检查。也就是说为了支持反向流程的工做,业务应用必须可以根据反向流程中发回来的消息内容进行业务操做检查,确认这个消息所指向的业务操做的状态是完成、待处理,仍是进行中,不然,待处理状态的消息就没法被处理了。
2.3 消息模型对消息接收的影响
前面讲述了消息发送端的内容,咱们接下来看一下消息模型。在JMS中,有Queue (点对点)和Topic (发布/订阅)两种模型,咱们来看看这两种模型的特色。
2.3.1 JMS Queue模型
图6-12显示的是JMS Queue模型,能够看到,应用1和应用2发送消息到JMS 服务器,这些消息根据到达的顺序造成一个队列,应用3和应用4进行消息的消费。 这里须要注意的是,应用3和应用4收到的消息是不一样的,也就是说在JMS Queue 的方式下,若是Queue里面的消息被一个应用处理了,那么链接到JMS Queue上的 另外一个应用是收不到这个消息的,也就是说全部链接到这个JMS Queue上的应用共同消费了全部的消息。消息从发送端发送出来时不能肯定最终会被哪一个应用消费, 可是能够明确的是只有一个应用会去消费这条消息,因此JMS Queue模型也被称为 Peer To Peer (PTP)方式。
2.3.2 JMS Topic模型
图6-13显示的是JMS Topic模型。从发送消息的部分和JMS Topic内部的逻辑 来看,JMS Topic和JMS Queue是同样的,两者最大的差异在于消息接收的部分,在 Topic模型中,接收消息的应用3和应用4是能够独立收到全部到达Topic的消息的。 JMS Topic模型也被称为Pub/Sub方式。
2.3.3 JMS中客户端链接的处理和带来的限制
在使用JMS时,每一个Connection都有一个惟一的Clientld,用于标记链接的惟一性,也就是说刚才对Queue和Topic的介绍中,咱们是默认一个接收应用只用了一个链接。如今来看一下多链接的状况,如图6-14所示。