消息队列在收发两端,主要是依靠业务代码,配合请求确认的机制,来保证消息不会丢失的。而在服务端,一般采用持久化和复制的方式来保证不丢消息。

把消息复制到多个节点上,不仅可以解决丢消息的问题,还可以保证消息服务的高可用。即使某一个节点宕机了,还可以继续使用其他节点来收发消息。所以大部分生产系统,都会把消息队列配置成集群模式,并开启消息复制,来保证系统的高可用和数据可靠性。

对于消息队列来说,它最核心的功能就是收发消息。也就是消息生产和消费这两个流程。我们在之前提到了消息队列一些常见问题,比如,“如何保证消息不会丢失?”“为什么会收到重复消息?”“消费时为什么要先执行消费业务逻辑再确认消费?”,针对这些问题,我讲过它们的实现原理,这些最终落地到代码上,都包含在这一收一发两个流程中。

我们选择代码风格比较简明易懂的 RocketMQ 作为分析对象。一起分析 RocketMQ 的 Producer 的源代码,学习消息生产的实现过程。

前面文章中提到过,我在一台配置比较高的服务器上,对 Kafka 做过一个极限的性能压测,想验证一下 Kafka 到底有多快。我使用的种子消息大小为 1KB,只要是并发数量足够多,不开启压缩时,可以打满万兆网卡的全部带宽,TPS 接近 100 万。开启压缩时,TPS 可以达到 2000 万左右,吞吐量提升了大约 20 倍!

现代的消息队列,都使用磁盘文件来存储消息。因为磁盘是一个持久化的存储,即使服务器掉电也不会丢失数据。绝大多数用于生产系统的服务器,都会使用多块儿磁盘组成磁盘阵列,这样不仅服务器掉电不会丢失数据,即使其中的一块儿磁盘发生故障,也可以把数据从其他磁盘中恢复出来。

使用磁盘的另外一个原因是,磁盘很便宜,这样我们就可以用比较低的成本,来存储海量的消息。所以,不仅仅是消息队列,几乎所有的存储系统的数据,都需要保存到磁盘上。

不知道你有没有发现,在高并发、高吞吐量的极限情况下,简单的事情就会变得没有那么简单了。一个业务逻辑非常简单的微服务,日常情况下都能稳定运行,为什么一到大促就卡死甚至进程挂掉?再比如,一个做数据汇总的应用,按照小时、天这样的粒度进行数据汇总都没问题,到年底需要汇总全年数据的时候,没等数据汇总出来,程序就死掉了。

在上节中,我们解决了如何实现高性能的网络传输的问题。那是不是程序之间就可以通信了呢?这里面还有一些问题需要解决。

我们知道,在 TCP 的连接上,它传输数据的基本形式就是二进制流,也就是一段一段的 1 和 0。在一般编程语言或者网络框架提供的 API 中,传输数据的基本形式是字节,也就是 Byte。一个字节就是 8 个二进制位,8 个 Bit,所以在这里,二进制流和字节流本质上是一样的。

那对于我们编写的程序来说,它需要通过网络传输的数据是什么形式的呢?是结构化的数据,比如,一条命令、一段文本或者是一条消息。对应到我们写的代码中,这些结构化的数据是什么?这些都可以用一个类(Class)或者一个结构体(Struct)来表示。

上一节学习了异步的线程模型,异步与同步模型最大的区别是,同步模型会阻塞线程等待资源,而异步模型不会阻塞线程,它是等资源准备好后,再通知业务代码来完成后续的资源处理逻辑。这种异步设计的方法,可以很好地解决 IO 等待的问题。

我们开发的绝大多数业务系统,它都是 IO 密集型系统。跟 IO 密集型系统相对的另一种系统叫计算密集型系统。通过这两种系统的名字,估计你也能大概猜出来 IO 密集型系统是什么意思。

消息积压的直接原因,一定是系统中的某个部分出现了性能问题,来不及处理上游发送的消息,才会导致消息积压。

所以,我们先来分析下,在使用消息队列时,如何来优化代码的性能,避免出现消息积压。然后再来看看,如果你的线上系统出现了消息积压,该如何进行紧急处理,最大程度地避免消息积压对业务的影响。

在消息传递过程中,如果出现传递失败的情况,发送方会执行重试,重试的过程中就有可能会产生重复的消息。对使用消息队列的业务系统来说,如果没有对重复消息进行处理,就有可能会导致系统的数据出现错误。

比如说,一个消费订单消息,统计下单金额的微服务,如果没有正确处理重复消息,那就会出现重复统计,导致统计结果错误。

你可能会问,如果消息队列本身能保证消息不重复,那应用程序的实现不就简单了?那有没有消息队列能保证消息不重复呢?

对于刚刚接触消息队列的同学,最常遇到的问题,也是最头痛的问题就是丢消息了。对于大部分业务系统来说,丢消息意味着数据丢失,是完全无法接受的。

其实,现在主流的消息队列产品都提供了非常完善的消息可靠性保证机制,完全可以做到在消息传递过程中,即使发生网络中断或者硬件故障,也能确保消息的可靠传递,不丢消息。

如果你研究过超过一种消息队列产品,你可能已经发现,每种消息队列都有自己的一套消息模型,像队列(Queue)、主题(Topic)或是分区(Partition)这些名词概念,在每个消息队列模型中都会涉及一些,含义还不太一样。

为什么出现这种情况呢?因为没有标准。曾经,也是有一些国际组织尝试制定过消息相关的标准,比如早期的 JMS 和 AMQP。但让人无奈的是,标准的进化跟不上消息队列的演进速度,这些标准实际上已经被废弃了。

“RabbitMQ?”“Kafka?”“RocketMQ?”…在日常学习与开发过程中,我们常常听到消息队列这个关键词。我也在我的多篇文章中提到了这个概念。可能你是熟练使用消息队列的老手,又或者你是不懂消息队列的新手,不论你了不了解消息队列,本文都将带你搞懂消息队列的一些基本理论。如果你是老手,你可能从本文学到你之前不曾注意的一些关于消息队列的重要概念,如果你是新手,相信本文将是你打开消息队列大门的一板砖。

一 概述

在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

Clojure(Lisp 语言的一种方言)、Groovy、Scala 等语言都是运行在 Java 虚拟机之上。下图展示了不同的语言被不同的编译器编译成.class文件最终运行在 Java 虚拟机之上。.class文件的二进制格式可以使用 WinHex 查看。

可以说.class文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。

JDK 命令行工具

这些命令在 JDK 安装目录下的 bin 目录下:

  • jps (JVM Process Status): 类似 UNIX 的 ps 命令。用户查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;
  • jstat( JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据;
  • jinfo (Configuration Info for Java) : Configuration Info forJava,显示虚拟机配置信息;
  • jmap (Memory Map for Java) :生成堆转储快照;
  • jhat (JVM Heap Dump Browser ) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果;
  • jstack (Stack Trace for Java):生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。

写在前面

本节常见问题

问题答案在文中都有提到

  • 如何判断对象是否死亡(两种方法)。
  • 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。
  • 如何判断一个常量是废弃常量
  • 如何判断一个类是无用的类
  • 垃圾收集有哪些算法,各自的特点?
  • HotSpot 为什么要分为新生代和老年代?
  • 常见的垃圾回收器有那些?
  • 介绍一下 CMS,G1 收集器。
  • Minor Gc 和 Full GC 有什么不同呢?