消息队列以及ActiveMQ

消息列队

为什么引入消息列队:

业务体量不断扩大,采用微服务的设计思想,分布式的部署方式,所以拆分了很多的服务,随着体量的增加以及业务场景越来越复杂了,很多场景单机的技术栈和中间件已经不够用了,而且对系统的友好性也下降了,最后做了很多技术选型的工作,我们决定引入消息队列中间件

消息列队使用的三大场景:

1. 异步

2. 削峰

3. 解耦

异步:

我们之前的场景里面有很多步骤都是在一个流程里面需要做完的,就比如说我的下单系统吧,本来我们业务简单,下单了付了钱就好了,流程就走完了。如果系统复杂之后,比如说有一个优惠券系统,OK问题不大,流程里面多100ms去扣减优惠券。在后来搞一个积分系统,也行吧,流程里面多了200ms去增减积分。更不用说发短信等等功能了。

  • 流程就有点像这样子了 ↓

Image

那链路长了就慢了,但是我们发现上面的流程其实可以同时做的呀,你支付成功后,我去校验优惠券的同时我可以去增减积分啊,还可以同时发个短信啊。

那正常的流程我们是没办法实现的呀,怎么办,异步。

你对比一下是不是发现,这样子最多只用100毫秒用户知道下单成功了,至于短信你迟几秒发给他他根本不在意是吧。

Image

解耦

用线程池实现异步有什么问题吗?

你一个订单流程,你扣积分,扣优惠券,发短信,扣库存。。。等等这么多业务要调用这么多的接口,每次加一个你要调用一个接口然后还要重新发布系统。

而且真的全部都写在一起的话,不单单是耦合这一个问题,你出问题排查也麻烦,流程里面随便一个地方出问题搞不好会影响到其他的点,小伙伴说我每个流程都try catch不就行了,相信我别这么做,这样的代码就像个定时炸弹,你不知道什么时候爆炸

但是你用了消息队列,耦合这个问题就迎刃而解了呀。

你下单了,你就把你支付成功的消息告诉别的系统,他们收到了去处理就好了,你只用走完自己的流程,把自己的消息发出去,那后面要接入什么系统简单,直接订阅你发送的支付成功消息,你支付成功了我监听就好了。

// todo 后期加解耦的详细情况

削峰:

你平时流量很低,但是你要做秒杀活动00:00的时候流量疯狂怼进来,你的服务器,Redis,MySQL各自的承受能力都不一样,你直接全部流量照单全收肯定有问题啊,直接就打挂了。

那怎么办?

把请求放到队列里面,然后至于每秒消费多少请求,就看自己的服务器处理能力,你能处理5000QPS你就消费这么多,可能会比正常的慢一点,但是不至于打挂服务器,等流量高峰下去了,你的服务也就没压力了。

你看阿里双十一12:00的时候这么多流量瞬间涌进去,他有时候是不是会慢一点,但是人家没挂啊,或者降级给你个友好的提示页面,等高峰过去了又是一条好汉了。

使用消息列队有什么问题?

使用他是因为他带给我们很多好处,但是使用之后问题也是接踵而至。

从三个点介绍他主要的缺点:

系统复杂性

本来蛮简单的一个系统,我代码随便写都没事,现在你凭空接入一个中间件在那,我是不是要考虑去维护他,而且使用的过程中是不是要考虑各种问题,比如消息重复消费、消息丢失、消息的顺序消费等等,反正用了之后就是贼烦。

数据一致性

这个其实是分布式服务本身就存在的一个问题,不仅仅是消息队列的问题,但是放在这里说是因为用了消息队列这个问题会暴露得比较严重一点。

就像我开头说的,你下单的服务自己保证自己的逻辑成功处理了,你成功发了消息,但是优惠券系统,积分系统等等这么多系统,他们成功还是失败你就不管了?

所有的服务都成功才能算这一次下单是成功的,那怎么才能保证数据一致性呢?

分布式事务:把下单,优惠券,积分。。。都放在一个事务里面一样,要成功一起成功,要失败一起失败。

可用性

你搞个系统本身没啥问题,你现在突然接入一个中间件在那放着,万一挂了怎么办?我下个单MQ挂了,优惠券不扣了,积分不减了,这不是杀一个程序员能搞定的吧,感觉得杀一片。

MQ的落地产品:

Image

JMS

JMS即Java消息服务(Java Message Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。

JSM消息组成

  1. 消息头
  2. 消息体
  3. 消息属性

JSM的优势

1、异步:JMS天生就是异步的,客户端获取消息的时候,不需要主动发送请求,消息会自动发送给可用的客户端。

2、可靠:JMS保证消息只会递送一次。大家都遇到过重复创建消息问题,而JMS能帮你避免该问题,只是避免而不是杜绝,所以在一些糟糕的环境下还是有可能会出现重复。

点对点消息传送模型:

1、点对点消息传送模型(P2P)

在点对点消息传送模型中,应用程序由消息队列,发送者,接收者组成。每一个消息发送给一个特殊的消息队列,该队列保存了所有发送给它的消息(除了被接收者消费掉的和过期的消息)。

点对点消息模型有如下特性:

  1. 每个消息只有一个接受者(自己测试了一下,可以有多个接受者,是当有多个接收者时,每个接收者只能获取随机的几条信息)

  2. 消息发送者和消息接受者并没有时间依赖性。

  3. 当消息发送者发送消息的时候,无论接收者程序在不在运行,都能获取到消息;

  4. 当接收者收到消息的时候,会发送确认收到通知(acknowledgement)。

发布/订阅消息传送模型

在发布/订阅消息模型中,发布者发布一个消息,该消息通过topic传递给所有的客户端。在这种模型中,发布者和订阅者彼此不知道对方,是匿名的且可以动态发布和订阅topic。topic主要用于保存和传递消息,且会一直保存消息直到消息被传递给客户端。

发布/订阅消息模型有如下特性:

(1)、一个消息可以传递给多个订阅者

(2)、发布者和订阅者有时间依赖性,只有当客户端创建订阅后才能接受消息,且订阅者需一直保持活动状态以接收消息。

(3)、为了缓和这样严格的时间相关性,JMS允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。

Active传输协议

分类:

ActiveMQ的几种传输协议:TCP ,OMQP,STOMP,MQTT,NIO… 默认是TCP
协议 描述
TCP 默认的协议,性能相对可以
NIO 基于TCP协议之上的,进行了扩展和优化,具有更好的扩展
UDP 性能比TCP更好,但是不具有可靠性
SSL 安全链接
VM VM本身不是协议,当客户端和代理在同一个Java虚拟机(VM)中运行,他们之间需要通信,但不想占用网络通道,而是直接通信,可以使用该方式

TCP协议简介

  1. 这是默认的Brokert置, TCP的Client监听端1161616

  2. 在网络传输数据前,必须要序列化数据,消息是通过一个Ilwire protocl的来序列化成字节流。默认情况下ActiveMa.把wire protocol做OpenWire,它的目的是促使网络上的效率和数据快速交互。

  3. TCP连接的URI形式如: tep/hostname:portzkeyvalueskey-value,后面的参数是可选

  4. TCP传输的优点:

(1)TCP协议传输可靠性高,

(2)高效性:字节流方式传递,效率很高效性,

(3)可用性:应用广泛,支持任何平台

适合使用NIO协议的场景:

  • 可能有大量的Client去连接到Broker上,一般情况下,大量的Client去连接Broker是被操作系统的线程所限制的。因此,NIO的实现比TCP需要更少的线程去运行,所以建议使用NIO协议

  • 可能对于Broker有一个很迟钝的网络传输,NIO比TCP提供更好的性能。

配置语法:
nio://hostname:port?key=value    key-value可进行参数调优

ActiveMQ的持久化机制

AMQ

  • 性能高于JDBC,写入消息时,会将消息写入日志文件,由于是顺序追加写,性能很高。为了提升性能,创建消息主键索引,并且提供缓存机制,进一步提升性能。每个日志文件的大小都是有限制的(默认32m,可自行配置)。

  • 当超过这个大小,系统会重新建立一个文件。当所有的消息都消费完成,系统会删除这个文件或者归档(取决于配置)。
    主要的缺点是AMQ Message会为每一个Destination创建一个索引,如果使用了大量的Queue,索引文件的大小会占用很多磁盘空间。
    而且由于索引巨大,一旦Broker崩溃,重建索引的速度会非常慢。

配置片段:
<persistenceAdapter>
     <amqPersistenceAdapter directory="${activemq.data}/activemq-data" maxFileLength="32mb"/>
</persistenceAdapter>

KaHaDB

KahaDB是从ActiveMQ5.4开始默认的持久化插件,也是我们项目现在使用的持久化方式。

KahaDb恢复时间远远小于其前身AMQ并且使用更少的数据文件,所以可以完全代替AMQ。

kahaDB的持久化机制同样是基于日志文件,索引和缓存。

配置方式:
<persistenceAdapter>
    <kahaDB directory="${activemq.data}/activemq-data"
    journalMaxFileLength="16mb"/>
</persistenceAdapter>

// directory : 指定持久化消息的存储目录
// journalMaxFileLength : 指定保存消息的日志文件大小,具体根据你的实际应用配置
KahaDB主要特性:
  1. 日志形式存储消息;

  2. 消息索引以B-Tree结构存储,可以快速更新;

  3. 完全支持JMS事务;

  4. 支持多种恢复机制;

(2)KahaDB的结构

消息存储在基于文件的数据日志中。如果消息发送成功,变标记为可删除的。系统会周期性的清除或者归档日志文件。
消息文件的位置索引存储在内存中,这样能快速定位到。定期将内存中的消息索引保存到metadata store中,避免大量消息未发送时,消息索引占用过多内存空间。

Data logs:
Data logs用于存储消息日志,消息的全部内容都在Data logs中。
同AMQ一样,一个Data logs文件大小超过规定的最大值,会新建一个文件。同样是文件尾部追加,写入性能很快。
每个消息在Data logs中有计数引用,所以当一个文件里所有的消息都不需要了,系统会自动删除文件或放入归档文件夹。

Metadata cache :
缓存用于存放在线消费者的消息。如果消费者已经快速的消费完成,那么这些消息就不需要再写入磁盘了。
Btree索引会根据MessageID创建索引,用于快速的查找消息。这个索引同样维护持久化订阅者与Destination的关系,以及每个消费者消费消息的指针。

Metadata store
在db.data文件中保存消息日志中消息的元数据,也是以B-Tree结构存储的,定时从Metadata cache更新数据。Metadata store中也会备份一些在消息日志中存在的信息,这样可以让Broker实例快速启动。
即便metadata store文件被破坏或者误删除了。broker可以读取Data logs恢复过来,只是速度会相对较慢些。

LevelDB

  • 从ActiveMQ 5.6版本之后,又推出了LevelDB的持久化引擎。
    目前默认的持久化方式仍然是KahaDB,不过LevelDB持久化性能高于KahaDB,可能是以后的趋势。

  • 在ActiveMQ 5.9版本提供了基于LevelDB和Zookeeper的数据复制方式,用于Master-slave方式的首选数据复制方案。

JDBC

使用JDBC持久化方式,数据库会创建3个表:activemq_msgs,activemq_acks和activemq_lock。

activemq_msgs用于存储消息,Queue和Topic都存储在这个表中。

1. 配置方式

配置持久化的方式,都是修改安装目录下conf/acticvemq.xml文件,

首先定义一个mysql-ds的MySQL数据源,然后在persistenceAdapter节点中配置jdbcPersistenceAdapter并且引用刚才定义的数据源。

<persistenceAdapter> 
    <jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="false" /> 
</persistenceAdapter>

dataSource指定持久化数据库的bean,createTablesOnStartup是否在启动的时候创建数据表,默认值是true,这样每次启动都会去创建数据表了,一般是第一次启动的时候设置为true,之后改成false。

使用MySQL配置JDBC持久化:

<beans>
    <broker brokerName="test-broker" persistent="true" xmlns="http://activemq.apache.org/schema/core">
        <persistenceAdapter>
            <jdbcPersistenceAdapter dataSource="#mysql-ds" createTablesOnStartup="false"/>
        </persistenceAdapter>
    </broker>
    <bean id="mysql-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost/activemq?relaxAutoCommit=true"/>
        <property name="username" value="activemq"/>
        <property name="password" value="activemq"/>
        <property name="maxActive" value="200"/>
        <property name="poolPreparedStatements" value="true"/>
    </bean>
</beans>
2. 数据库表信息
  1. activemq_msgs用于存储消息,Queue和Topic都存储在这个表中:
  • ID:自增的数据库主键

  • CONTAINER:消息的Destination

  • MSGID_PROD:消息发送者客户端的主键

  • MSG_SEQ:是发送消息的顺序,MSGID_PROD+MSG_SEQ可以组成JMS的MessageID

  • EXPIRATION:消息的过期时间,存储的是从1970-01-01到现在的毫秒数

  • MSG:消息本体的Java序列化对象的二进制数据

  • PRIORITY:优先级,从0-9,数值越大优先级越高

  1. activemq_acks用于存储订阅关系。如果是持久化Topic,订阅者和服务器的订阅关系在这个表保存:

主要的数据库字段如下:

  • CONTAINER:消息的Destination

  • SUB_DEST:如果是使用Static集群,这个字段会有集群其他系统的信息

  • CLIENT_ID:每个订阅者都必须有一个唯一的客户端ID用以区分

  • SUB_NAME:订阅者名称

  • SELECTOR:选择器,可以选择只消费满足条件的消息。条件可以用自定义属性实现,可支持多属性AND和OR操作

  • LAST_ACKED_ID:记录消费过的消息的ID。

activemq_lock在集群环境中才有用,只有一个Broker可以获得消息,称为Master Broker,

其他的只能作为备份等待Master Broker不可用,才可能成为下一个Master Broker。

这个表用于记录哪个Broker是当前的Master Broker。

高级特性:

重复消费

就比如有这样的一个场景,用户下单成功后我需要去一个活动页面给他加GMV(销售总额),最后根据他的GMV去给他发奖励,这是电商活动很常见的玩法。

类似累计下单金额到哪个梯度给你返回什么梯度的奖励这样。

我只能告诉你这样的活动页面10000%是用异步去加的,不然你想,你一个用户下一单就给他加一下,那就意味着对那张表就要操作一下,你考虑下双十一当天多少次对这个表的操作?这数据库或者缓存都顶不住吧。

而且大家应该也有这样的体会,你下单了马上去看一些活动页面,有时候马上就有了,有时候却延迟有很久,为啥?这个速度取决于消息队列的消费速度,消费慢堵塞了就迟点看到呗。

你下个单支付成功你就发个消息出去,我们上面那个活动的开发人员就监听你的支付成功消息,我监听到你这个订单成功支付的消息,那我就去我活动GMV表里给你加上去,听到这里大家可能觉得顺理成章。

Image

但是我告诉大家一般消息队列的使用,我们都是有重试机制的,就是说我下游的业务发生异常了,我会抛出异常并且要求你重新发一次。

我这个活动这里发生错误,你要求重发肯定没问题。但是不止你一个人监听这个消息啊,还有别的服务也在监听,他们也会失败啊,他一失败他也要求重发,但是你这里其实是成功的,重发了,你的钱不就加了两次了?是不是这个道理?

就像这个样子,看下面 ↓

Image

就好比上面的这样,我们的积分系统处理失败了,他这个系统肯定要求你重新发送一次这个消息对吧,积分的系统重新接收并且处理成功了,但是别人的活动,优惠券等等服务也监听了这个消息呀,那不就可能出现活动系统给他加GMV加两次,优惠券扣两次这种情况么?

真实的情况其实重试是很正常的,服务的网络抖动开发人员代码Bug,还有数据问题等都可能处理失败要求重发的。

开发过程中怎么保证重复消费问题:
一般我们叫这样的处理叫接口幂等。

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。

幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

通俗了讲就是你同样的参数调用我这个接口,调用多少次结果都是一个,你加GMV同一个订单号你加一次是多少钱,你加N次都还是多少钱。

但是如果不做幂等,你一个订单调用多次钱不就加多次嘛,同理你退款调用多次钱也就减多次了。

大致处理流程如下:

Image

怎么实现幂等:

一般幂等,会分场景去考虑,看是强校验还是弱校验,比如跟金钱相关的场景那就很关键呀,就做强校验,别不是很重要的场景做弱校验。

强校验:

比如你监听到用户支付成功的消息,你监听到了去加GMV是不是要调用加钱的接口,那加钱接口下面再调用一个加流水的接口,两个放在一个事务,成功一起成功失败一起失败。

每次消息过来都要拿着订单号+业务场景这样的唯一标识(比如天猫双十一活动)去流水表查,看看有没有这条流水,有就直接return不要走下面的流程了,没有就执行后面的逻辑。

之所以用流水表,是因为涉及到金钱这样的活动,有啥问题后面也可以去流水表对账,还有就是帮助开发人员定位问题。

弱校验:

这个简单,一些不重要的场景,比如给谁发短信啥的,我就把这个id+场景唯一标识作为Redis的key,放到缓存里面失效时间看你场景,一定时间内的这个消息就去Redis判断。

用KV就算消息丢了可能这样的场景也没关系,反正丢条无关痛痒的通知短信嘛

猜你喜欢

转载自blog.csdn.net/qq_44161695/article/details/105138071