要是针对某些特定的业务场景,如果不使用消息队列会让系统的一些业务实现变得很复杂。这些场景很多,比如电商系统的订单与库存服务,考试系统的提交与日志服务等。将这些复杂的场景抽象起来,其实使用消息队列的场景集中在达到三个目的:解耦、异步、挫峰。
这里主要针对耦合度比较高的系统场景,举个简单的例子,比如说电商场景,订单服务需要被支付、仓库、商品等服务调用,而这其中订单服务就会出现很大的问题,它需要去时刻检测另外的服务是否还活着,如果调用失败了是不是还需要存储或者重发,一旦处理的逻辑不当会造成整个系统的数据不一致。
在计算机科学的世界里,没有什么问题是引入一个中间层解决不了的。(虽然这句话的下半句是但是这个中间层会引起新的问题。)
但是引入消息队列之后,场景就变成了,订单服务提交数据给消息队列服务器,无需关心另外系统的消费情况,即将订单服务与其他服务解耦开了。而这里面的消息队列就起着一种类似与C#中委托订阅,比较经典的Pub/Sub模型的作用。
这里主要针对实际对响应时间要求比较高的应用场景,举个我曾经实际遇到过的场景,简单说就是一个电子签章的场景,涉及到显式签名、隐式签名与数据库中签名状态记录的更新。显示签名指在对应的文档模板上通过相应文档操作更新上显示的签名或图章,隐式签名指的是密码学的相关操作,数据库记录更新即更新相应记录,便于后续的操作。
如果采用一般的模式(无消息队列),签一次名的总时间为1500ms左右(密码与文档操作会消耗大量的时间),这无论是从用户体验与系统瓶颈来看都是不友好的。一般的应用产品,理论上每个请求应在200ms使用完成。
引入消息队列后,我们可以将显示与隐式签名的服务调用与数据库状态记录的更新解耦开,将签名签章请求发送给单独的消息队列服务器,只更新数据库的记录(起到类似与锁的作用),然后再由显式与隐式签名服务去消费消息队列里的请求。这样对于用户来说,一次响应的时间大概在10ms不到,因为不涉及长时间的运算,对于系统来说,有效的解决了系统的当前瓶颈,因为很多情况下,用户只是签名完成即可,不需要去下载真正的签名文档。
顾名思义就是解决系统达到峰值并发引起的问题,比如一个正常的考试系统,当不考试的时候,系统应该是几乎没有请求的,而当有考试的时候,系统是有很大的并发量的,当并发量足够大,且没有redis,cache之类的东西,请求会容易把数据库直接搞挂,从而导致系统的崩溃,严重的可引起微服务系统的雪崩效应。
如果引入了消息队列,就可以解决这样的问题,这时消息队列起的是一个缓冲的作用,保证请求被后台程序稳定消费,将系统的峰值分摊到后面空闲的时候处理。
缺点应该是比较明显,场景很复杂,引入消息队列会带来很多新的问题。
引入消息队列固然解决了一定棘手的问题,但仍然存在很多问题,比方说消息如何保证被可靠消费,消息如何保证没有在传输过程中失败。
和一些微服务中重要的组件一致,比方说配置中心,注册中心等,消息队列的可用性仍然需要可靠的保证,并需要一定的容错措施。
就像刚才在异步场景举的列子,用户固然接受到已经操作成功的消息,你还需要保证下面还未执行的操作顺利执行,如果执行失败也要有相应的处理措施,如果毫无处理,在某种意义上就造成了整个系统的数据不一致问题。
总的来说还是刚才说的那句名句,在计算机科学的世界里,没有什么是引入一个中间层解决不了的,但是这个中间层会带来新的问题。
世面上比较常用的是Kafka,ActiveMQ,RabbitMQ,RocketMQ消息队列
名称 | Kafka | ActiveMQ | RabbitMQ | RocketMQ |
---|---|---|---|---|
吞吐量 | 10w | 1w | 1w | 10w |
发布订阅主题数量造成的影响 | 较大 | 无 | 无 | 较小 |
效率 | 毫秒 | 毫秒 | 微秒 | 毫秒 |
可用性 | 极高 | 高 | 高 | 极高 |
使用架构 | 分布式 | 主从 | 主从 | 分布式 |
可靠性 | 极高 | 会丢 | 极高 | 需要优化达到不丢 |
功能 | 简单,一般用于大数据 | 完善 | 并发强 | 扩展性好 |
使用场景 | 大数据 领域 | 社区不活跃不建议 | 不可控但性能强 | 社区有黄的风险 |
上面提到,这个场景下的消息队列主要基于主从场景。RabbitMQ的操作主要有三种模式:single,normal cluster,image cluster来保证高可用,只需要在它的控制台选择相应的策略配置即可切换。
–针对single模式,没有必要讨论,只是本地运行学习使用。
–针对normal cluster模式,简单说就是普通集群部署的方式,在多台机器或现在很流行的容器中启动RabbitMQ实例,RabbitMQ使真实的队列只存在与其中的一个实例中,并在各个实例中同步更新队列的配置信息(一般称之为元数据),当我们需要找到真实队列的实例并开始消费时,我们可以通过元数据简单找到。也就是说,真实的消费场景是连接任意实例,这个实例去拉取真正实例的数据。这种模式的缺点比较明显,一个是如果真实队列所在的实例发生故障,那么可用性会造成影响,另一个是如何选择连接的实例,即如何保证各个实例负载均衡。总而言之,只是提升了一定的消息消费性能,并没有真正做到我们现在讲的高可用。
–针对image cluster模式,与上述的normal cluster场景有明显不同,实际的队列会存在于各个实例中,每个实例都相当于有一个实际队列的完整镜像,在写的时候,会从写入的实例开始使用分布式同步的方法将这个消息同步到各个实例,在读的时候,只需要采用随机,轮询等经典负载均衡算法连接合适的实例即可获取,没有拉取数据的成本与风险。优点很明显,实现了高可用,缺点也很明显,每个实例都存了一份数据,很难拓展,到后期每写入一个消息,成本都增加了。
天然适合分布式的场景,架构和现在流行的K8S很像,就是各个层次的细化做的细致,广的来看,针对整体的结构,分成若干个broker节点,针对消息订阅发布的主题,则将其分成各个partition,将各个partition存放在各个broker节点上。
通俗的讲,就是数据不连续放在一个机器或容器上,而是分散开。这也是它和RabbitMQ最大的不同,也决定了它在大数据领域很有市场。
Kafka在0.8以前是没有高可用性保证的,而在0.8以后提供了一种叫作replica副本的机制。每个partition的数据会在写入完成后通过分布式的方式去同步到其他实例或机器中,也就是形成了各个夫本,然后会使用一个共识算法去选举一个leader replica出来,由它去管理其他的replica。这是一种中心化的做法,每次写入只需要写入leader,然后让leader去同步数据到所有的follower上,读的时候直接读取leader数据。从而达到了高可用,即使leader因为过劳挂了,只需要从其他的replica中重新使用共识算法选举出一个新的leader即可。
更细致的描绘一下,写的时候默认的共识方式是,写入leader,leader将数据从内存写入磁盘,其他broke自动去pull数据,并返回给leader pull的结果,当leader发现所有的broke都成功复制了数据,再返回成功写入的响应。当然可以更换别的共识策略。
更具体的可以看官方的文档。
所谓幂等性即每个消息无论消费多少次对系统产生的结果都是一致的,在消息队列的场景下,要保证幂等性,首先要保证消息不被重复消费。
拿kafka来说,kafka是通过偏移量来实现消息的消费,每次写入消息时候获取消息的偏移量来定位消息的位置,消息完成后,每隔一段时间将偏移量提交,来保证宕机后继续能使用上次的消息消费。
设想固然是美好的,但是这个操作并不是Atomic的操作。会存在还未提交过偏移量,但是消息的确已经被消费的那一刹那重启的问题,会导致重启回复后的消费再次被消费。
这种问题消息队列并没有给我们保证好,需要我们自己在业务里判断。
比方说简单的数据库写入消息,我们只需要下写入的时候判断一下这个请求是否已经被消费过即可,当然业务的场景会复杂,也需要采用不同的方法去操作。
有很多方法可以保证,比如说以下场景
– 普通关系型数据库的Create场景,先read一下,再判断是Create还是update
– Nosql,自身就支持幂等性。
– 复杂场景,使用分布式唯一id,可以用redis,uuid等方法。
– 使用数据库自身约束,具体场景具体分析。
即保证数据被可靠传输,拿RabbitMQ来说。众所周知,消息队列的引入同时会出现三个角色,即producer,customer和消息队列本身,那么问题必然出现在这三个角色其中一环。
和上面一样,分析三种情况
一般来说是不会有消息不按正确的顺序被消费的问题,但是还是存在一些特殊且不少见的场景