2. Clasificación de mensajes RocketMQ

1. Mensajes ordinarios
1 Clasificación de envío de mensajes
El productor también tiene múltiples opciones sobre cómo enviar mensajes, y diferentes métodos producirán diferentes efectos en el sistema.
Enviar mensajes sincrónicamente
significa que después de que el Productor envía un mensaje, enviará el siguiente mensaje después de recibir el ACK devuelto por MQ. Este método tiene la mayor confiabilidad de mensajes, pero la eficiencia de envío de mensajes es demasiado baja.
Insertar descripción de la imagen aquí
Enviar mensajes de forma asincrónica
Enviar mensajes de forma asincrónica significa que después de que el Productor envía un mensaje, no necesita esperar a que MQ devuelva ACK y envíe directamente el siguiente mensaje. Se puede garantizar la confiabilidad de los mensajes con este método y también se puede mejorar la eficiencia del envío de mensajes.
Insertar descripción de la imagen aquí
El envío de mensajes unidireccional
significa que el Productor solo es responsable de enviar mensajes y no espera ni procesa el ACK de MQ. MQ no devuelve ACK en este método de envío. Este método tiene la mayor eficiencia en el envío de mensajes, pero la confiabilidad del mensaje es pobre.
Insertar descripción de la imagen aquí
2 Ejemplos de código
Crear un proyecto
Crear un proyecto Maven Java rocketmq-test.
Importar dependencias
Importar dependencias del cliente de rocketmq.

<properties>
<project.build.sourceEncoding>UTF-
8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.8.0</version>
</dependency>
</dependencies>

Definir productor de envío de mensajes de sincronización

public class SyncProducer {
    
    
public static void main(String[] args) throws Exception {
    
    
// 创建一个producer,参数为Producer Group名称
DefaultMQProducer producer = new DefaultMQProducer("pg");
// 指定nameServer地址
producer.setNamesrvAddr("rocketmqOS:9876");
// 设置当发送失败时重试发送的次数,默认为2次
producer.setRetryTimesWhenSendFailed(3);
// 设置发送超时时限为5s,默认3s
producer.setSendMsgTimeout(5000);
// 开启生产者
producer.start();
// 生产并发送100条消息
for (int i = 0; i < 100; i++) {
    
    
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("someTopic", "someTag", body);
// 为消息指定key
msg.setKeys("key-" + i);
// 发送消息
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
// 关闭producer
producer.shutdown();
}
}
// 消息发送的状态
public enum SendStatus {
    
    
SEND_OK, // 发送成功
FLUSH_DISK_TIMEOUT, // 刷盘超时。当Broker设置的刷盘策略为同步刷盘时才可能出
现这种异常状态。异步刷盘不会出现
FLUSH_SLAVE_TIMEOUT, // Slave同步超时。当Broker集群设置的Master-Slave的复
制方式为同步复制时才可能出现这种异常状态。异步复制不会出现
SLAVE_NOT_AVAILABLE, // 没有可用的Slave。当Broker集群设置为Master-Slave的
复制方式为同步复制时才可能出现这种异常状态。异步复制不会出现
}

Definir productor de envío de mensajes asincrónicos

public class AsyncProducer {
    
    
public static void main(String[] args) throws Exception {
    
    
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketmqOS:9876");
// 指定异步发送失败后不进行重试发送
producer.setRetryTimesWhenSendAsyncFailed(0);
// 指定新创建的Topic的Queue数量为2,默认为4
producer.setDefaultTopicQueueNums(2);
producer.start();
for (int i = 0; i < 100; i++) {
    
    
byte[] body = ("Hi," + i).getBytes();
try {
    
    
Message msg = new Message("myTopicA", "myTag", body);
// 异步发送。指定回调
producer.send(msg, new SendCallback() {
    
    
// 当producer接收到MQ发送来的ACK后就会触发该回调方法的执行
@Override
public void onSuccess(SendResult sendResult) {
    
    
System.out.println(sendResult);
}
@Override
public void onException(Throwable e) {
    
    
e.printStackTrace();
}
});
} catch (Exception e) {
    
    
e.printStackTrace();
}
} // end-for
// sleep一会儿
// 由于采用的是异步发送,所以若这里不sleep,
// 则消息还未发送就会将producer给关闭,报错
TimeUnit.SECONDS.sleep(3);
producer.shutdown();
}
}

Definir productor de envío de mensajes unidireccionales

public class OnewayProducer {
    
    
public static void main(String[] args) throws Exception{
    
    
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketmqOS:9876");
producer.start();
for (int i = 0; i < 10; i++) {
    
    
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("single", "someTag", body);
// 单向发送
producer.sendOneway(msg);
}
producer.shutdown();
System.out.println("producer shutdown");
}
}

Definir consumidores de mensajes

public class SomeConsumer {
    
    
public static void main(String[] args) throws MQClientException {
    
    
// 定义一个pull消费者
// DefaultLitePullConsumer consumer = new
DefaultLitePullConsumer("cg");
// 定义一个push消费者
DefaultMQPushConsumer consumer = new
DefaultMQPushConsumer("cg");
// 指定nameServer
consumer.setNamesrvAddr("rocketmqOS:9876");
// 指定从第一条消息开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET
);
// 指定消费topic与tag
consumer.subscribe("someTopic", "*");
// 指定采用“广播模式”进行消费,默认为“集群模式”
// consumer.setMessageModel(MessageModel.BROADCASTING);
// 注册消息监听器
consumer.registerMessageListener(new
MessageListenerConcurrently() {
    
    
// 一旦broker中有了其订阅的消息就会触发该方法的执行,
// 其返回值为当前consumer消费的状态
@Override
public ConsumeConcurrentlyStatus
consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
    
    
// 逐条消费消息
for (MessageExt msg : msgs) {
    
    
System.out.println(msg);
}
// 返回消费状态:消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 开启消费者消费
consumer.start();
System.out.println("Consumer Started");
}
}

2. Mensajes secuenciales
1. ¿Qué son los mensajes secuenciales
?Los mensajes secuenciales se refieren a mensajes (FIFO) que se consumen estrictamente en el orden en que se envían.
De forma predeterminada, los productores enviarán mensajes a diferentes colas de partición de cola en el modo de sondeo Round Robin; cuando consuman mensajes, extraerán
mensajes de varias colas. En este caso, no se puede garantizar el orden de envío y consumo. Si el mensaje solo se envía a la misma
cola y el mensaje solo se extrae de esta cola cuando se consume, el orden del mensaje está estrictamente garantizado.
2 ¿Por qué se necesitan mensajes secuenciales
? Por ejemplo, hay TOPIC ORDER_STATUS (estado del pedido) y hay 4 colas debajo de él. Diferentes mensajes en este tema se utilizan para
describir diferentes estados del pedido actual. Supongamos que el pedido tiene estado: No pagado, Pagado, Envío, Envío exitoso, Envío fallido.
Según el estado del pedido anterior, el productor puede generar los siguientes mensajes en secuencia de tiempo:
Pedido T0000001: No pagado --> Pedido T0000001: Pagado --> Pedido T0000001: Entrega --> Pedido
T0000001:
Mensaje de error de entrega enviado Después de ingresar MQ, Si se selecciona Cola mediante una estrategia de sondeo, el almacenamiento de mensajes en MQ puede ser el siguiente:

Insertar descripción de la imagen aquí
En este caso, esperamos que el orden en el que el Consumidor consume los mensajes sea consistente con lo que enviamos, sin embargo, con los métodos de entrega y consumo de MQ anteriores
, no podemos garantizar que el orden sea correcto. Para mensajes con secuencia anormal, incluso si el consumidor está configurado con un cierto estado de tolerancia a fallas, no puede
manejar por completo tantas combinaciones aleatorias.

Insertar descripción de la imagen aquí
Con base en la situación anterior, se puede diseñar la siguiente solución: para mensajes con el mismo número de pedido, se colocan en una cola a través de una determinada estrategia y
luego el consumidor adopta una determinada estrategia (por ejemplo, un hilo procesa una cola de forma independiente). para garantizar el procesamiento El orden de los mensajes
) puede garantizar el orden de consumo.

3 Clasificación del orden
Dependiendo del alcance del pedido, RocketMQ puede garantizar estrictamente el orden de dos tipos de mensajes: orden de partición y orden global.

Ordenado globalmente

Insertar descripción de la imagen aquí
Cuando solo hay una Cola involucrada en el envío y el consumo, el orden garantizado es el orden de los mensajes en todo el Tema, lo que se denomina orden global.
Especifique el número de colas al crear un tema. Hay tres formas de especificar:
1) Al crear un Productor en el código, puede especificar el número de colas para el tema creado automáticamente.
2) Especificar el número de colas al crear manualmente un tema en la consola visual RocketMQ.
3) Especifique el número de colas al crear manualmente un tema mediante el comando mqadmin.

Orden de partición
Insertar descripción de la imagen aquí
Si hay varias colas participando, solo puede garantizar el orden de los mensajes en la cola de partición de cola, lo que se denomina orden de partición.
¿Cómo implementar la selección de cola? Al definir Productor, podemos especificar el selector de cola de mensajes, y este selector lo
define nuestra propia implementación de la interfaz MessageQueueSelector.
Al definir el algoritmo de selección de un selector, generalmente es necesario utilizar la clave de selección. Esta clave de selección puede ser una clave de mensaje u otros
datos. Pero no importa quién seleccione la clave, esta no se puede repetir y es única.
El algoritmo de selección general consiste en tomar la clave de selección (o su valor hash) en el módulo del número de colas contenidas en el tema, y ​​el resultado
es el QueueId de la cola seleccionada.
Hay un problema con el algoritmo de módulo: los resultados de módulo de diferentes claves seleccionadas y números de cola pueden ser los mismos, es decir,
mensajes con diferentes claves seleccionadas pueden aparecer en la misma cola, es decir, el mismo consumidor puede consumir mensajes con diferentes teclas seleccionadas. ¿Cómo solucionar este problema
? El enfoque general es obtener la clave de selección del mensaje y juzgarla. Si es un mensaje que el Consumidor actual necesita
consumir, consúmalo directamente, de lo contrario no haga nada. Este enfoque requiere que el consumidor pueda
obtener la clave seleccionada junto con el mensaje. En este momento, es una mejor práctica utilizar la clave de mensaje como clave de selección.
¿El enfoque anterior causará los siguientes nuevos problemas? Si se extrae el mensaje que no pertenece a ese Consumidor, ¿puede
el Consumidor que debería consumir el mensaje seguir consumiendo el mensaje? Los mensajes de la misma cola no pueden ser procesados ​​por mensajes del mismo grupo.
Diferentes consumidores consumen al mismo tiempo. Por lo tanto, los consumidores que consumen mensajes con diferentes claves seleccionadas en una cola deben pertenecer a
grupos diferentes. El consumo entre consumidores de diferentes grupos está aislado entre sí y no se afecta entre sí.

public class OrderedProducer {
    
    
public static void main(String[] args) throws Exception {
    
    
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketmqOS:9876");
producer.start();
for (int i = 0; i < 100; i++) {
    
    
Integer orderId = i;
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("TopicA", "TagA", body);
SendResult sendResult = producer.send(msg, new
MessageQueueSelector() {
    
    
@Override
public MessageQueue select(List<MessageQueue> mqs,
Message msg, Object arg) {
    
    
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.println(sendResult);
}
producer.shutdown();
}
}

3. Mensaje retrasado
1. ¿Qué es un mensaje retrasado?
Cuando se escribe un mensaje en el Broker, el mensaje se puede consumir y procesar después de un período de tiempo específico, lo que se denomina mensaje retrasado.
El uso de mensajes retrasados ​​​​de RocketMQ puede realizar la función de tareas programadas sin utilizar un temporizador. Los escenarios de aplicación típicos son el
escenario en transacciones de comercio electrónico donde el pedido se cierra debido al tiempo de espera y la falta de pago, y el escenario donde la reserva del boleto de la plataforma 12306 se cancela después del tiempo de espera y la falta de pago.
En las plataformas de comercio electrónico, se envía un mensaje retrasado cuando se crea un pedido. Este mensaje se entregará al sistema empresarial backend
(Consumidor) después de 30 minutos, luego de recibir el mensaje, el sistema empresarial backend determinará si se ha pagado el pedido correspondiente. Si no se
completa, el pedido se cancela y la mercancía se vuelve a colocar en el inventario; si se completa el pago, se ignora.
En la plataforma 12306, se enviará un mensaje retrasado una vez que la reserva del boleto se haya realizado correctamente. Este mensaje se entregará al
sistema empresarial backend (Consumidor) después de 45 minutos, luego de recibir el mensaje, el sistema empresarial backend determinará si se ha pagado el pedido correspondiente. Si
no se completa, cancela la reserva y vuelve a colocar el boleto en el pool de boletos; si se completa el pago, se ignora.
2 Nivel de retraso
La duración del retraso de los mensajes retrasados ​​no admite retrasos de longitud arbitrarios y se especifica mediante un nivel de retraso específico. El nivel de retraso se define en
las siguientes variables en la clase MessageStoreConfig del servidor RocketMQ:
Insertar descripción de la imagen aquí
es decir, si el nivel de retraso especificado es 3, significa que la duración del retraso es 10 segundos, es decir, el nivel de retraso comienza a contar desde 1.
Por supuesto, si necesita un nivel de retraso personalizado, puede agregar la siguiente configuración a la configuración cargada por el corredor (por ejemplo, el
nivel 1d de 1 día se agrega a continuación). El archivo de configuración está en el directorio conf en el directorio de instalación de RocketMQ.

messageDelayLevel = 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 1d

3 Principio de implementación de mensajes retrasados
Insertar descripción de la imagen aquí
​​El plan de implementación específico es: después de
modificar el
Insertar descripción de la imagen aquí
Productor de mensajes para enviar el mensaje al Broker, el Broker primero escribirá el mensaje en el archivo de registro de confirmación y luego lo distribuirá a la
cola de consumo correspondiente. Sin embargo, antes de la distribución, el sistema primero determinará si el mensaje contiene un nivel de retraso. Si no, se distribuirá
normalmente, si es así, debe pasar por un proceso complicado:
modifique el tema del mensaje a SCHEDULE_TOPIC_XXXX . Según el nivel de retraso, cree el directorio queueId correspondiente y el archivo consumequeue
bajo el tema SCHEDULE_TOPIC_XXXX en el directorio consumequeue (si no existen estos
directorios y archivos).
La relación correspondiente entre el nivel de retraso delayLevel y queueId es queueId = delayLevel -1.
Cabe señalar que al crear el directorio queueId, los directorios correspondientes a todos los niveles de retraso no se crean a la vez,
sino qué directorio se crea según qué nivel de retraso. se utiliza
Insertar descripción de la imagen aquí
Contenido de la unidad de índice de mensajes. La parte HashCode de la etiqueta de mensaje en la unidad de índice almacena originalmente
el valor Hash de la etiqueta del mensaje. Ahora modificado al tiempo de entrega del mensaje. El tiempo de entrega se refiere al tiempo que lleva volver a modificar el mensaje al tema original y luego escribirlo
nuevamente en el registro de confirmación. Tiempo de entrega = tiempo de almacenamiento del mensaje + tiempo del nivel de retraso. El tiempo de almacenamiento del mensaje se refiere a
la marca de tiempo cuando el mensaje se envía al Broker.
Escriba el índice del mensaje en la cola de consumo correspondiente en el tema SCHEDULE_TOPIC_XXXX
¿Cómo se ordenan los mensajes en la cola de cada nivel de retraso en el directorio SCHEDULE_TOPIC_XXXX?
Están ordenados según el tiempo de entrega del mensaje. Todos los mensajes retrasados ​​​​del mismo nivel en un Broker se escribirán en la
misma cola en el directorio SCHEDULE_TOPIC_XXXX en el directorio consumerqueue. Es decir, el tiempo de nivel de retraso del tiempo de entrega de mensajes en una cola
es el mismo. Entonces el tiempo de entrega depende del tiempo de almacenamiento del mensaje.
Es decir, se ordenan según la hora en la que se envían los mensajes al Broker .
Entrega de mensajes retrasados
​​Existe una clase de servicio de mensajes retrasados ​​ScheuleMessageService dentro del Broker, que consume los mensajes en SCHEDULE_TOPIC_XXXX
, es decir, entrega los mensajes retrasados ​​al Tema de destino de acuerdo con el tiempo de entrega de cada mensaje. Sin embargo, antes de la entrega,
el mensaje escrito originalmente se volverá a leer del registro de confirmación y su nivel de retraso original se establecerá en 0, es decir, el mensaje original se convertirá en un mensaje normal sin demora
. Luego entregue el mensaje al tema de destino nuevamente.
Cuando se inicia el Broker, ScheuleMessageService creará e iniciará un temporizador para realizar las
tareas programadas correspondientes. El sistema definirá un número correspondiente de TimerTasks en función del número de niveles de retraso. Cada TimerTask es responsable del consumo y entrega de mensajes de un
nivel de retraso. Cada TimerTask detectará si el primer mensaje de la cola correspondiente ha caducado. Si el
primer mensaje no ha caducado, todos los mensajes posteriores no caducarán (los mensajes se clasifican según el tiempo de entrega); si el primer
mensaje ha caducado, el mensaje se entregará al tema de destino, es decir, el mensaje se consumirá. .información .
Vuelva a escribir el mensaje en el registro de confirmación.
La clase de servicio de mensajes retrasados ​​​​ScheuleMessageService envía el mensaje retrasado al registro de confirmación nuevamente, forma una nueva
entrada de índice de mensajes nuevamente y la distribuye a la cola correspondiente.
En realidad, se trata de un envío de mensajes normal. Es solo que esta vez el Productor de mensajes es la clase de servicio de mensajes retrasados
​​​​ScheuleMessageService.

4 Ejemplo de código
Definición de la clase DelayProducer

public class DelayProducer {
    
    
public static void main(String[] args) throws Exception {
    
    
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketmqOS:9876");
producer.start();
for (int i = 0; i < 10; i++) {
    
    
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("TopicB", "someTag", body);
// 指定消息延迟等级为3级,即延迟10s
// msg.setDelayTimeLevel(3);
SendResult sendResult = producer.send(msg);
// 输出消息被发送的时间
System.out.print(new SimpleDateFormat("mm:ss").format(new
Date()));
System.out.println(" ," + sendResult);
}
producer.shutdown();
}
}

Definir la clase OtroConsumidor

public class OtherConsumer {
    
    
public static void main(String[] args) throws MQClientException {
    
    
DefaultMQPushConsumer consumer = new
DefaultMQPushConsumer("cg");
consumer.setNamesrvAddr("rocketmqOS:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET
);
consumer.subscribe("TopicB", "*");
consumer.registerMessageListener(new
MessageListenerConcurrently() {
    
    
@Override
public ConsumeConcurrentlyStatus
consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
    
    
for (MessageExt msg : msgs) {
    
    
// 输出消息被消费的时间
SimpleDateFormat("mm:ss").format(new Date()));
System.out.println(" ," + msg);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("Consumer Started");
}
}

4. Mensaje de transacción
1 Introducción al problema
Uno de los escenarios de demanda aquí es: el usuario A de ICBC transfiere 10,000 yuanes al usuario B de CCB.
Podemos usar mensajes sincrónicos para manejar este escenario de requisitos:
Insertar descripción de la imagen aquí

  1. El sistema ICBC envía un mensaje de sincronización M al corredor para agregar 10.000 yuanes a B.
  2. Una vez que el corredor recibe exitosamente el mensaje, se envía un ACK exitoso al sistema ICBC.
  3. Después de recibir el ACK exitoso, el sistema ICBC deduce 10.000 yuanes del usuario A.
  4. El sistema CCB obtiene el mensaje M del Broker
  5. El sistema CCB consume el mensaje M, lo que significa agregar 10,000 yuanes al usuario B.
    Hay un problema: si la operación de deducción en el paso 3 falla, el mensaje se envió exitosamente al Broker. Para MQ
    , siempre que el mensaje se escriba correctamente, se puede consumir. En este momento, el usuario B en el sistema CCB ha aumentado en 10.000 yuanes.
    Ha ocurrido un problema de inconsistencia de datos .

2 Idea de solución
La idea de solución es hacer que los pasos 1, 2 y 3 sean atómicos, todos tengan éxito o todos fallen. Es decir, una vez que el mensaje se envía con éxito, se debe
garantizar que la deducción sea exitosa. Si la deducción falla, el mensaje exitoso se revertirá. Esta idea es utilizar mensajes de transacción. Aquí se utiliza
una solución de transacciones distribuidas .
Insertar descripción de la imagen aquí
Utilice mensajes de transacción para manejar este escenario de requisitos:

  1. El administrador de transacciones TM inicia una instrucción al coordinador de transacciones TC para iniciar una transacción global
  2. El sistema ICBC envía un mensaje de transacción M a TC para agregar 10.000 yuanes a B.
  3. TC enviará el mensaje de semitransacción prepareHalf al corredor y enviará previamente el mensaje M al corredor. En este momento, el sistema CCB no puede ver
    el mensaje M en el Broker.
  4. El corredor informará los resultados de la ejecución previa al compromiso a TC.
  5. Si el envío previo falla, TC informará la respuesta de falla del envío previo a TM y la transacción global finaliza; si el envío previo es exitoso, TC llamará a la
    operación de devolución de llamada del sistema ICBC para completar la deducción previa de 10.000 yuanes para el usuario A del ICBC.
  6. El sistema ICBC enviará el resultado de la ejecución de la retención a TC, es decir, el estado de ejecución de la transacción local.
  7. Después de recibir los resultados de la ejecución de la retención, TC informará los resultados a TM.

Existen tres posibilidades para los resultados de la ejecución de la retención:

// 描述本地事务执行状态
public enum LocalTransactionState {
    
    
COMMIT_MESSAGE, // 本地事务执行成功
ROLLBACK_MESSAGE, // 本地事务执行失败
UNKNOW, // 不确定,表示需要进行回查以确定本地事务的执行结果
}
  1. TM emitirá diferentes instrucciones de confirmación a TC según los resultados del informe.
    Si la retención es exitosa (el estado de la transacción local es COMMIT_MESSAGE), el TM enviará la instrucción de compromiso global al TC.
    Si la retención falla (el estado de la transacción local es ROLLBACK_MESSAGE), el TM enviará el comando de confirmación global al TC. Envíe el comando de reversión global.
    Si se desconoce el estado actual (el estado de la transacción local es DESCONOCIDO), se activará la operación de revisión del estado de la transacción local del sistema ICBC. La
    operación de revisión informará el resultado de la revisión, es decir, COMMIT_MESSAGE o ROLLBACK_MESSAGE, a TC. TC informa los resultados
    a TM, y TM enviará la instrucción de confirmación final Global Commit o Global Rollback a TC.
  2. Después de recibir la instrucción, TC enviará una instrucción de confirmación a los sistemas Broker e ICBC
    . Si el TC recibe una instrucción de compromiso global, enviará una instrucción de compromiso de sucursal a los sistemas Broker e ICBC. En este momento,
    el sistema CCB puede ver el mensaje M en el Broker; en este momento, en realidad se confirma la operación de deducción en el usuario ICBC A. Si el TC
    recibe el comando Global Rollback, envía el comando Branch Rollback al Sistemas Broker e ICBC. En este momento,
    el mensaje M en el Broker será revocado y la operación de deducción en el usuario A de ICBC se revertirá. El
    esquema anterior es para garantizar que las operaciones de entrega y deducción del mensaje puedan realizarse en una sola transacción. Si ambas tienen éxito, si uno falla,
    luego retrocede todo.
    La solución anterior no es un modelo XA típico. Porque la transacción de sucursal en modo XA es asíncrona, mientras que
    las operaciones de confirmación previa y deducción previa del mensaje en el esquema de mensajes de transacción son síncronas.

3 Conceptos básicos
Transacciones distribuidas
Para transacciones distribuidas, en términos simples, una operación consta de varias operaciones de sucursal, que pertenecen a diferentes aplicaciones y se distribuyen en
diferentes servidores. Las transacciones distribuidas deben garantizar que todas estas operaciones de sucursales tengan éxito o fracasen. Las transacciones distribuidas son las mismas que las transacciones ordinarias
, para garantizar la coherencia de los resultados de la operación.
Mensajes de transacción
RocketMQ proporciona una función de transacción distribuida similar a X/Open XA, y la coherencia final de las transacciones distribuidas se puede lograr a través de mensajes de transacción. XA
es una solución de transacciones distribuidas, un modelo de procesamiento de transacciones distribuidas.
El mensaje semitransaccional
es un mensaje que no se puede entregar temporalmente. El remitente ha enviado con éxito el mensaje al corredor, pero el corredor no ha recibido la instrucción de confirmación final. En este momento,
el mensaje está marcado como "temporalmente no entregable", es decir. es decir, no puede ser visto por el consumidor. Los mensajes en este estado son mensajes semitransaccionales.
El resultado de la ejecución de la operación de devolución de llamada del productor del estado de la transacción local
es el estado de la transacción local, que se enviará a TC, y luego TC lo enviará a TM.
TM determinará la instrucción de confirmación de transacción global en función del estado de la transacción local enviado por TC .

// 描述本地事务执行状态
public enum LocalTransactionState {
    
    
COMMIT_MESSAGE, // 本地事务执行成功
ROLLBACK_MESSAGE, // 本地事务执行失败
UNKNOW, // 不确定,表示需要进行回查以确定本地事务的执行结果
}

Revisión de mensajes
Insertar descripción de la imagen aquí
La revisión de mensajes significa volver a consultar el estado de ejecución de las transacciones locales. Este ejemplo consiste en volver a la base de datos para verificar si la operación de retención fue exitosa.
Tenga en cuenta que la revisión del mensaje no significa volver a ejecutar la operación de devolución de llamada. La operación de devolución de llamada sirve para realizar la operación de retención, mientras que la devolución de mensaje es para comprobar los resultados de la operación de retención.
Hay dos razones más comunes para activar la revisión de mensajes:
1) La operación de devolución de llamada devuelve UNKNWON
2) TC no recibe la instrucción final de confirmación de transacción global de TM

Configuración de revisión de mensajes en RocketMQ Con respecto a
la revisión de mensajes, existen tres configuraciones de propiedades comunes. Todos están configurados en el archivo de configuración cargado por el corredor, por ejemplo:

transactionTimeout=20, especifica que TM debe enviar el estado de confirmación final a TC dentro de los 20 segundos; de lo contrario, se activará una revisión del mensaje. El valor predeterminado
es 60 segundos
de transacciónCheckMax=5, especificando un máximo de 5 comprobaciones, después de lo cual el mensaje se descartará y se registrará un registro de errores. El valor predeterminado es 15 veces.
transactionCheckInterval=10, especifica que el intervalo de tiempo para múltiples comprobaciones de mensajes es de 10 segundos. El valor predeterminado es 60 segundos.


4 Modo XA Protocolo XA de los Tres Mosqueteros
XA (Unix Transaction) es una solución de transacciones distribuidas, un modo de procesamiento de transacciones distribuidas, basado en el protocolo XA. El protocolo XA fue propuesto por primera vez por
Tuxedo (Transaction for Unix se ha ampliado para operación distribuida, el sistema de transacciones Unix después de la expansión de las operaciones distribuidas ) y se entregó a la organización X/Open como un estándar de interfaz para administradores de recursos y administradores de transacciones. . Hay tres componentes importantes en el modo XA: TC, TM y RM. Coordinador de Transacciones TC , coordinador de transacciones. Mantenga el estado de las transacciones globales y de sucursales e impulse el envío o la reversión de transacciones globales. En RocketMQ, el Broker actúa como TC. TM Transaction Manager, administrador de transacciones. Defina el alcance de una transacción global: inicie una transacción global, confirme o revierta una transacción global. En realidad , es el iniciador de transacciones globales. El Productor de mensajes de transacciones en RocketMQ actúa como TM. RM Resource Manager, administrador de recursos. Administre recursos para el procesamiento de transacciones de sucursales, hable con el TC para registrar transacciones de sucursales e informar el estado de las transacciones de sucursales, e impulse las confirmaciones o reversiones de transacciones de sucursales. El productor y el intermediario de mensajes de transacciones en RocketMQ son ambos RM.













5 Arquitectura del modo XA
Insertar descripción de la imagen aquí
El modo XA es un 2PC típico y su principio de ejecución es el siguiente:

  1. TM inicia una instrucción a TC para iniciar una transacción global.
  2. De acuerdo con los requisitos comerciales, cada RM registrará las transacciones de las sucursales con TC una por una, y luego TC emitirá instrucciones previas a la ejecución a RM una por una.
  3. Cada RM realizará la preejecución de la transacción local después de recibir la instrucción.
  4. RM informa los resultados previos a la ejecución a TC. Por supuesto, este resultado puede ser un éxito o un fracaso.
  5. Después de recibir el informe de cada RM, TC informará los resultados resumidos a TM, y TM emitirá una instrucción de confirmación a TC basada en los resultados resumidos
    .
    Si todos los resultados son respuestas exitosas, el comando de confirmación global se envía al TC.
    Siempre que el resultado sea una respuesta de error, la instrucción de reversión global se envía al TC.
  6. Después de recibir la instrucción, TC envía nuevamente la instrucción de confirmación a RM.
    El esquema de mensajería de transacciones no es un patrón XA típico. Porque las transacciones de sucursal en modo XA son asíncronas, mientras que
    las operaciones de confirmación previa y retención de mensajes en las soluciones de mensajes de transacciones son síncronas.

6 Tenga en cuenta que
los mensajes de transacción no admiten mensajes retrasados.
Para los mensajes de transacción, se debe verificar la idempotencia porque los mensajes de transacción se pueden consumir más de una vez (porque existe una
situación de revertir y luego enviar).
7 Ejemplos de código

Definir el oyente de transacciones ICBC

public class ICBCTransactionListener implements TransactionListener {
    
    
// 回调操作方法
// 消息预提交成功就会触发该方法的执行,用于完成本地事务
@Override
public LocalTransactionState executeLocalTransaction(Message msg,
Object arg) {
    
    
System.out.println("预提交消息成功:" + msg);
// 假设接收到TAGA的消息就表示扣款操作成功,TAGB的消息表示扣款失败,
// TAGC表示扣款结果不清楚,需要执行消息回查
if (StringUtils.equals("TAGA", msg.getTags())) {
    
    
return LocalTransactionState.COMMIT_MESSAGE;
} else if (StringUtils.equals("TAGB", msg.getTags())) {
    
    
return LocalTransactionState.ROLLBACK_MESSAGE;
} else if (StringUtils.equals("TAGC", msg.getTags())) {
    
    
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.UNKNOW;
}
// 消息回查方法
// 引发消息回查的原因最常见的有两个:
// 1)回调操作返回UNKNWON
// 2)TC没有接收到TM的最终全局事务确认指令
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
    
    
System.out.println("执行消息回查" + msg.getTags());
return LocalTransactionState.COMMIT_MESSAGE;
}
}

Definir productor de mensajes de transacción

public class TransactionProducer {
    
    
public static void main(String[] args) throws Exception {
    
    
TransactionMQProducer producer = new
TransactionMQProducer("tpg");
producer.setNamesrvAddr("rocketmqOS:9876");
/**
* 定义一个线程池
* @param corePoolSize 线程池中核心线程数量
* @param maximumPoolSize 线程池中最多线程数
* @param keepAliveTime 这是一个时间。当线程池中线程数量大于核心线程数量
是
* 多余空闲线程的存活时长
* @param unit 时间单位
* @param workQueue 临时存放任务的队列,其参数就是队列的长度
* @param threadFactory 线程工厂
*/
ExecutorService executorService = new ThreadPoolExecutor(2, 5,
100, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2000), new
ThreadFactory() {
    
    
@Override
public Thread newThread(Runnable r) {
    
    
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
});
// 为生产者指定一个线程池
producer.setExecutorService(executorService);
// 为生产者添加事务监听器
producer.setTransactionListener(new ICBCTransactionListener());
producer.start();
String[] tags = {
    
    "TAGA","TAGB","TAGC"};
for (int i = 0; i < 3; i++) {
    
    
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("TTopic", tags[i], body);
// 发送事务消息
// 第二个参数用于指定在执行本地事务时要使用的业务参数
SendResult sendResult =
producer.sendMessageInTransaction(msg,null);
System.out.println("发送结果为:" +
sendResult.getSendStatus());
}
}
}

Para definir consumidores
, simplemente use SomeConsumer de mensajes ordinarios como consumidor.

public class SomeConsumer {
    
    
public static void main(String[] args) throws MQClientException {
    
    
// 定义一个pull消费者
faultLitePullConsumer("cg");
// 定义一个push消费者
DefaultMQPushConsumer consumer = new
DefaultMQPushConsumer("cg");
// 指定nameServer
consumer.setNamesrvAddr("rocketmqOS:9876");
// 指定从第一条消息开始消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET
);
// 指定消费topic与tag
consumer.subscribe("TTopic", "*");
// 指定采用“广播模式”进行消费,默认为“集群模式”
// consumer.setMessageModel(MessageModel.BROADCASTING);
// 注册消息监听器
consumer.registerMessageListener(new
MessageListenerConcurrently() {
    
    
// 一旦broker中有了其订阅的消息就会触发该方法的执行,
// 其返回值为当前consumer消费的状态
@Override
public ConsumeConcurrentlyStatus
consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
    
    
// 逐条消费消息
for (MessageExt msg : msgs) {
    
    
System.out.println(msg);
}
// 返回消费状态:消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 开启消费者消费
consumer.start();
System.out.println("Consumer Started");
}
}

5. Mensajes por lotes
1. Envío de mensajes por lotes
Restricciones de envío
Al enviar mensajes, los productores pueden enviar varios mensajes a la vez, lo que puede mejorar en gran medida la eficiencia de envío del Productor. Sin embargo, debe prestar atención a
los siguientes puntos:
Los mensajes enviados en lotes deben tener el mismo tema.
Los mensajes enviados en lotes deben tener la misma estrategia de vaciado.
Los mensajes enviados en lotes no pueden ser mensajes retrasados ​​ni mensajes de transacción.
El tamaño de envío por lotes
es , de forma predeterminada, se envía en un lote. El tamaño total del mensaje no puede exceder los 4 MB de bytes. Si desea exceder este valor, existen dos soluciones:
Solución 1: dividir el mensaje por lotes en varios conjuntos de mensajes que no superen los 4 millones y enviarlos en lotes varias veces
Solución 2: modificar las propiedades en los lados del Productor y del Broker
** El lado del Productor necesita configurar el atributo maxMessageSize del Productor antes de enviar
** El lado del Broker necesita modificar el atributo maxMessageSize en el archivo de configuración que carga.
El tamaño del mensaje enviado por el productor.
Insertar descripción de la imagen aquí
El Mensaje enviado por el productor a través del envío ( ) El método no es una serialización directa del Mensaje, luego se envía a la red, pero
se genera una cadena y se envía a través de este Mensaje. Esta cadena consta de cuatro partes: tema, cuerpo del mensaje, registro de mensajes
(que ocupa 20 bytes) y un conjunto de atributos clave-valor utilizados para describir el mensaje. Estos atributos incluyen, por ejemplo, la dirección del productor, el tiempo de producción,
el QueueId que se enviará, etc. Los datos que finalmente se escriben en la unidad de mensajes en Broker provienen de estos atributos.

2 Consumir mensajes en lotes.
Modifique el atributo de lote
Insertar descripción de la imagen aquí
. Interfaz de escucha concurrente MessageListener del consumidor. El primer parámetro del método consumeMessage() es la lista de mensajes
, pero de forma predeterminada solo se puede consumir un mensaje a la vez. Para permitirle consumir varios mensajes a la vez, puede
especificarlo modificando la propiedad consumerMessageBatchMaxSize del consumidor. Sin embargo, el valor no puede exceder 32. Porque, de forma predeterminada,
la cantidad máxima de mensajes que los consumidores pueden extraer cada vez es 32. Para modificar el valor máximo de una extracción, puede
especificarlo modificando el atributo pullBatchSize del consumidor.
Problemas:
¿Deberían establecerse los atributos pullBatchSize y consumaMessageBatchMaxSize del consumidor en un valor mayor? Por supuesto que no
.
Cuanto mayor sea el valor de pullBatchSize, más tiempo le tomará al consumidor extraer cada vez y mayor será la posibilidad de que haya problemas de transmisión en la red
. Si ocurre un problema durante el proceso de extracción, todos los mensajes de este lote deben extraerse nuevamente
.
Cuanto mayor sea el valor de consumeMessageBatchMaxSize establecido, menor será la capacidad de consumo de mensajes simultáneos del consumidor y este lote de
mensajes consumidos tendrá el mismo resultado de consumo. Debido a que un lote de mensajes especificado por consumeMessageBatchMaxSize solo será
procesado por un hilo, siempre que haya una excepción de procesamiento de mensajes durante el procesamiento, este lote de mensajes debe consumirse y procesarse nuevamente
.
3 ejemplos de código
El requisito para este envío por lotes no es modificar el valor predeterminado del envío máximo de 4M, sino evitar que los mensajes por lotes enviados excedan el límite de 4M.
Definir divisor de lista de mensajes

// 消息列表分割器:其只会处理每条消息的大小不超4M的情况。
// 若存在某条消息,其本身大小大于4M,这个分割器无法处理,
// 其直接将这条消息构成一个子列表返回。并没有再进行分割
public class MessageListSplitter implements Iterator<List<Message>> {
    
    
// 指定极限值为4M
private final int SIZE_LIMIT = 4 *1024 * 1024;
// 存放所有要发送的消息
private final List<Message> messages;
// 要进行批量发送消息的小集合起始索引
private int currIndex;
public MessageListSplitter(List<Message> messages) {
    
    
this.messages = messages;
}
@Override
public boolean hasNext() {
    
    
// 判断当前开始遍历的消息索引要小于消息总数
return currIndex < messages.size();
}
@Override
public List<Message> next() {
    
    
int nextIndex = currIndex;
// 记录当前要发送的这一小批次消息列表的大小
int totalSize = 0;
for (; nextIndex < messages.size(); nextIndex++) {
    
    
// 获取当前遍历的消息
Message message = messages.get(nextIndex);
// 统计当前遍历的message的大小
int tmpSize = message.getTopic().length() +
message.getBody().length;
Map<String, String> properties = message.getProperties();
for (Map.Entry<String, String> entry :
properties.entrySet()) {
    
    
tmpSize += entry.getKey().length() +
entry.getValue().length();
}
tmpSize = tmpSize + 20;
// 判断当前消息本身是否大于4M
if (tmpSize > SIZE_LIMIT) {
    
    
if (nextIndex - currIndex == 0) {
    
    
nextIndex++;
}
break;
}
if (tmpSize + totalSize > SIZE_LIMIT) {
    
    
break;
} else {
    
    
totalSize += tmpSize;
}
} // end-for
// 获取当前messages列表的子集合[currIndex, nextIndex)
List<Message> subList = messages.subList(currIndex, nextIndex);
// 下次遍历的开始索引
currIndex = nextIndex;
return subList;
}
}

Definir productor de mensajes masivos

public class BatchProducer {
    
    
public static void main(String[] args) throws Exception {
    
    
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("rocketmqOS:9876");
// 指定要发送的消息的最大大小,默认是4M
// 不过,仅修改该属性是不行的,还需要同时修改broker加载的配置文件中的
// maxMessageSize属性
// producer.setMaxMessageSize(8 * 1024 * 1024);
producer.start();
// 定义要发送的消息集合
List<Message> messages = new ArrayList<>();
for (int i = 0; i < 100; i++) {
    
    
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("someTopic", "someTag", body);
messages.add(msg);
}
// 定义消息列表分割器,将消息列表分割为多个不超出4M大小的小列表
MessageListSplitter splitter = new
MessageListSplitter(messages);
while (splitter.hasNext()) {
    
    
try {
    
    
List<Message> listItem = splitter.next();
producer.send(listItem);
} catch (Exception e) {
    
    
e.printStackTrace();
}
}
producer.shutdown();
}
}

Definir consumidor de mensajes masivos

public class BatchConsumer {
    
    
public static void main(String[] args) throws MQClientException {
    
    
DefaultMQPushConsumer consumer = new
DefaultMQPushConsumer("cg");
consumer.setNamesrvAddr("rocketmqOS:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET
);
consumer.subscribe("someTopicA", "*");
// 指定每次可以消费10条消息,默认为1
consumer.setConsumeMessageBatchMaxSize(10);
// 指定每次可以从Broker拉取40条消息,默认为32
consumer.setPullBatchSize(40);
consumer.registerMessageListener(new
MessageListenerConcurrently() {
    
    
@Override
public ConsumeConcurrentlyStatus
consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
    
    
for (MessageExt msg : msgs) {
    
    
System.out.println(msg);
}
// 消费成功的返回结果
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
// 消费异常时的返回结果
// return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
consumer.start();
System.out.println("Consumer Started");
}
}

Supongo que te gusta

Origin blog.csdn.net/weixin_45817985/article/details/133073643
Recomendado
Clasificación