Use RocketMQ (rocketmq-spring-boot-starter) en Springboot para resolver problemas de transacciones distribuidas

Prefacio
El artículo anterior presentó el proceso de uso de RocketMQ-spring-boot-starter.Este artículo presenta cómo usar RocketMQ para resolver problemas de transacciones en un entorno distribuido.

Si no está familiarizado con cohetemq-spring-boot-starter, se recomienda leer primero mi último artículo: Tutorial de integración de Springboot RocketMQ

1: simulación de escena

Escenario : supongamos que tenemos un negocio de este tipo ahora: los usuarios obtendrán puntos por recargar tarifas de red, y 1 yuan = 1 punto, recargarán 100 yuanes en el servicio de usuario y agregarán 100 puntos al usuario en el servicio de puntos

Análisis : para tales operaciones entre servicios y entre bibliotecas, debemos asegurarnos de que estas dos operaciones tengan éxito juntas o fracasen juntas.La solución RocketMQ es: mensaje de transacción RocketMQ + transacción local + consumo de monitoreo para lograr la consistencia final

Antes de implementar, presentemos la transacción de RocketMQ

Dos: Introducción a la transacción RocketMQ

1. Conceptos básicos
(1) Medio Mensaje: también llamado Preparar Mensaje, traducido como "medio mensaje" o "mensaje de preparación", que hace referencia a un mensaje que no se puede entregar temporalmente, es decir, el mensaje se envía con éxito al servidor MQ y no puede ser entregado al consumidor por el momento Para el consumo, solo cuando el servidor recibe la segunda confirmación del productor, puede ser consumido por el consumidor (2
) Verificación del estado del mensaje: verifique el estado del mensaje. El segundo reconocimiento del mensaje transaccional puede perderse debido a la desconexión de la red o al reinicio de la aplicación productora. Cuando el servidor MQ descubre que un mensaje permanece en el estado de medio mensaje durante mucho tiempo, enviará una solicitud al productor del mensaje. para comprobar el estado final del mensaje ("commit" o "rollback")

2. Diagrama de flujo de ejecución (herramienta ProcessOn)
Proceso de transacción de RocketMQ

  1. El productor envía medio mensaje al servidor MQ, que no se puede entregar temporalmente y no se consumirá
  2. Después de que el medio mensaje se envía con éxito, el productor ejecuta la transacción local
  3. El productor envía un mensaje de confirmación o reversión al servidor MQ para una confirmación secundaria basada en el resultado de la ejecución de la transacción local.
  4. Si el servidor MQ recibe la confirmación, marcará la mitad del mensaje como entregable y el consumidor puede consumir en este momento; si recibe la reversión, descartará la mitad del mensaje directamente y no consumirá
  5. Si el servidor MQ no recibe el segundo mensaje de confirmación, el servidor MQ enviará periódicamente (predeterminado 1 minuto) un mensaje de respuesta al productor para verificar el estado de la transacción local, y luego el productor enviará una confirmación o un envío al servidor MQ. nuevamente de acuerdo con el resultado del mensaje de reversión de verificación de transacción local

Tres: Implementación del código comercial

1. Crear tabla
(1) tabla de usuario

CREATE TABLE `t_user` (
   `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户表',
   `name` varchar(16) NOT NULL COMMENT '姓名',
   `id_card` varchar(32) NOT NULL COMMENT '身份证号',
   `balance` int(11) NOT NULL DEFAULT '0' COMMENT '余额',
   `state` tinyint(1) DEFAULT NULL COMMENT '状态(1在线,0离线)',
   `vip_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'VIP用户标识(1是,0否)',
   `create_time` datetime NOT NULL COMMENT '创建时间',
   `last_login_time` datetime DEFAULT NULL COMMENT '最后一次登录时间',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4

(2) tabla de puntos

CREATE TABLE `t_credit` (
   `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '积分表',
   `user_id` int(11) NOT NULL COMMENT '用户id',
   `username` varchar(16) NOT NULL COMMENT '用户姓名',
   `integration` int(11) NOT NULL DEFAULT '0' COMMENT '积分',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4

(3) Tabla de registro de transacciones

CREATE TABLE `t_mq_transaction_log` (
   `transaction_id` varchar(64) NOT NULL COMMENT '事务id',
   `log` varchar(64) NOT NULL COMMENT '日志',
   PRIMARY KEY (`transaction_id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
  1. Soy una simulación y la puse en una biblioteca aquí. En cuanto a por qué se crea una tabla de registro de transacciones , lo sabrá más adelante.
  2. Después de crear la tabla, para su comodidad, primero escriba manualmente un dato en la tabla de usuario y la tabla de puntos
  3. La estructura del proyecto se omite aquí, incluidas las clases de entidad, las interfaces del mapeador, etc.

2. Cree un nuevo productor de transacciones MQ: MQTXProducerService

@Slf4j
@Component
public class MQTXProducerService {
    
    

    private static final String Topic = "RLT_TEST_TOPIC";
    private static final String Tag = "charge";
    private static final String Tx_Charge_Group = "Tx_Charge_Group";

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 先向MQ Server发送半消息
     * @param userCharge 用户充值信息
     */
    public TransactionSendResult sendHalfMsg(UserCharge userCharge) {
    
    
        // 生成生产事务id
        String transactionId = UUID.randomUUID().toString().replace("-", "");
        log.info("【发送半消息】transactionId={}", transactionId);

        // 发送事务消息(参1:生产者所在事务组,参2:topic+tag,参3:消息体(可以传参),参4:发送参数)
        TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(
                Tx_Charge_Group, Topic + ":" + Tag,
                MessageBuilder.withPayload(userCharge).setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId).build(),
                userCharge);
        log.info("【发送半消息】sendResult={}", JSON.toJSONString(sendResult));
        return sendResult;
    }
}
  1. Aquí uso UUID para generar la identificación de la transacción, que es la identificación de la tabla de registro de transacciones anterior (la situación real puede usar el algoritmo de copo de nieve o definir la identificación de acuerdo con el negocio)
  2. El parámetro de método userCharge, agregado adicionalmente, puede entenderse como dto, solo dos campos: ID de usuario, monto de cargo, que representa la identificación del usuario y el monto de la recarga.
  3. Tenga en cuenta aquí: hay dos parámetros en el método de envío de un medio mensaje, la referencia 3 y la referencia 4. Aquellos que hayan leído el tutorial de integración anterior deben saber que este parámetro 3 es para consumidores y este parámetro 4 es para asuntos locales. Soy Es lo mismo que la simulación, el negocio real puede ser diferente

3. Cree un nuevo escucha de transacciones local: MQTXLocalService

@Slf4j
@RocketMQTransactionListener(txProducerGroup = "Tx_Charge_Group") // 这里的txProducerGroup的值要与发送半消息时保持一致
public class MQTXLocalService implements RocketMQLocalTransactionListener {
    
    

    @Autowired
    private UserService userService;
    @Autowired
    private MQTransactionLogMapper mqTransactionLogMapper;

    /**
     * 用于执行本地事务的方法
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object obj) {
    
    
        // 获取消息体里参数
        MessageHeaders messageHeaders = message.getHeaders();
        String transactionId = (String) messageHeaders.get(RocketMQHeaders.TRANSACTION_ID);
        log.info("【执行本地事务】消息体参数:transactionId={}", transactionId);

        // 执行带有事务注解的本地方法:增加用户余额+保存mq日志
        try {
    
    
            UserCharge userCharge = (UserCharge) obj;
            userService.addBalance(userCharge, transactionId);
            return RocketMQLocalTransactionState.COMMIT; // 正常:向MQ Server发送commit消息
        } catch (Exception e) {
    
    
            log.error("【执行本地事务】发生异常,消息将被回滚", e);
            return RocketMQLocalTransactionState.ROLLBACK; // 异常:向MQ Server发送rollback消息
        }
    }

    /**
     * 用于回查本地事务执行结果的方法
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
    
    
        MessageHeaders headers = message.getHeaders();
        String transactionId = headers.get(RocketMQHeaders.TRANSACTION_ID, String.class);
        log.info("【回查本地事务】transactionId={}", transactionId);

        // 根据事务id查询事务日志表
        MQTransactionLog mqTransactionLog = mqTransactionLogMapper.selectByPrimaryKey(transactionId);
        if (null == mqTransactionLog) {
    
     // 没查到表明本地事务执行失败,通知回滚
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        return RocketMQLocalTransactionState.COMMIT; // 查到表明本地事务执行成功,提交
    }
}
@Service
public class UserService {
    
    

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private MQTransactionLogMapper mqTransactionLogMapper;

    /**
     * 用户增加余额+事务日志
     */
    @Transactional(rollbackFor = Exception.class)
    public void addBalance(UserCharge userCharge, String transactionId) {
    
    
        // 1. 增加余额
        userMapper.addBalance(userCharge.getUserId(), userCharge.getChargeAmount());
        // 2. 写入mq事务日志
        saveMQTransactionLog(transactionId, userCharge);
    }

    @Transactional(rollbackFor = Exception.class)
    public void saveMQTransactionLog(String transactionId, UserCharge userCharge) {
    
    
        MQTransactionLog transactionLog = new MQTransactionLog();
        transactionLog.setTransactionId(transactionId);
        transactionLog.setLog(JSON.toJSONString(userCharge));
        mqTransactionLogMapper.insertSelective(transactionLog);
    }

}
  1. El código aquí es el punto clave principal. La transacción local es agregar el saldo al usuario y luego insertar el registro de transacciones mq. Solo cuando estas dos operaciones tengan éxito, volverá a COMMIT, y si falla anormalmente, volverá para RETROCEDER
  2. Es posible que el método backcheck no se ejecute, pero debe ser. Backcheck es consultar la tabla de registro de transacciones en función de la identificación de transacción (transactionId) que generamos y pasamos antes. La ventaja de esto es que no importa cuántas tablas haya . involucrado en el negocio. Mi tabla de registro También está vinculada a su transacción local . Solo necesito consultar esta tabla de transacciones. Si puedo encontrarla, significa que la transacción local se ha ejecutado con éxito.
  3. Aquí hay un punto: el método addBalance anterior es defectuoso. Si el método saveMQTransactionLog es anormal, aunque la anotación @Transactional se agrega al método addBalance, la transacción no tendrá efecto. Esto involucra el principio del mecanismo de transacción de Spring (esencialmente implementado a través de Proxy dinámico AOP+), pero también estoy aquí para la simulación, así que filtro este detalle

4. Cree un nuevo consumidor de mensajes de transacción: MQTXConsumerService

@Slf4j
@Component
@RocketMQMessageListener(topic = "RLT_TEST_TOPIC", selectorExpression = "charge", consumerGroup = "Con_Group_Four") // topic、tag保持一致
public class MQTXConsumerService implements RocketMQListener<UserCharge> {
    
    

    @Autowired
    private CreditMapper creditMapper;

    @Override
    public void onMessage(UserCharge userCharge) {
    
    
        // 一般真实环境这里消费前,得做幂等性判断,防止重复消费
        // 方法一:如果你的业务中有某个字段是唯一的,有标识性,如订单号,那就可以用此字段来判断
        // 方法二:新建一张消费记录表t_mq_consumer_log,字段consumer_key是唯一性,能插入则表明该消息还未消费,往下走,否则停止消费
        // 我个人建议用方法二,根据你的项目业务来定义key,这里我就不做幂等判断了,因为此案例只是模拟,重在分布式事务

        // 给用户增加积分
        int i = creditMapper.addNumber(userCharge.getUserId(), userCharge.getChargeAmount());
        if (1 == i) {
    
    
            log.info("【MQ消费】用户增加积分成功,userCharge={}", JSONObject.toJSONString(userCharge));
        } else {
    
    
            log.error("【MQ消费】用户充值增加积分消费失败,userCharge={}", JSONObject.toJSONString(userCharge));
        }
    }
}
  1. Los consumidores son en realidad relativamente simples, similares a los consumidores comunes, solo preste atención a la configuración de atributos
  2. Aquí puede cuestionar que no haya nada de malo con el envío anterior y las transacciones locales, ya sea compromiso o reversión, pero ¿qué pasa si el consumo falla aquí? De hecho, casi no hay posibilidad de problemas aquí. En primer lugar, RocketMQ tiene una alta disponibilidad. Si su sistema es realmente enorme, puede agruparlo. Además, ya sea que el consumo aquí sea exitoso o no, el código fuente ha sido procesado. internamente, siempre que no haya anormalidad, consumirá, y también tiene un mecanismo de reintento, finalmente, puede expandir la lógica de consumo aquí. Cuando el consumo no es exitoso, puede guardar el registro, recordarlo regularmente o procesar manualmente él

Cuatro: Prueba

Añadir a RocketMQController:

@PostMapping("/charge")
public Result<TransactionSendResult> charge(UserCharge userCharge) {
    
    
    TransactionSendResult sendResult = mqtxProducerService.sendHalfMsg(userCharge);
    return Result.success(sendResult);
}

Use el cartero para llamar a: http://localhost:8080/rocketmq/charge
prueba
consola
y verifique que sea normal, luego vaya a la base de datos y descubra que se agregaron 100 del saldo y los puntos, y también se registra la tabla de registro de transacciones, ¡éxito!

Resumen: De hecho, después de comprender el proceso de implementación de transacciones, encontrará que es bastante simple usar RocketMQ para resolver transacciones distribuidas.Después de todo, MQ es muy amigable y MQ tiene muchos usos, y cada proyecto puede tenerlo. Por supuesto, ahora hay otras soluciones de transacciones distribuidas populares y profesionales, por lo que se debe mencionar a Seata, pero si su proyecto no necesita específicamente a Seata, si MQ puede resolverlo, entonces puede introducir un componente menos de Seata. Bueno, ¿por qué no hacerlo? ¿él?

Aquí hay una pequeña opinión personal: la elección de una solución de transacciones distribuidas debe considerar y adaptarse a su negocio real, entonces mi sugerencia es: si el negocio es un usuario que realiza una determinada operación, definitivamente se caerá, incluso si algo sucede El problema también está al revés, por lo que no hay problema en usar RocketMQ al 100%. Permítame darle el ejemplo más simple: el usuario pagó el dinero, pero hubo un problema con el programa de seguimiento. Es imposible que usted se comunique con el usuario para decirle que hay un problema con el sistema. ¿Qué tal si le devuelvo el dinero? el dinero a usted primero, y puede hacer un pedido para comprarlo de nuevo más tarde. Si es así, creo que nadie usará su sistema más tarde. En resumen, en la producción real, debe usarse de manera flexible de acuerdo con el negocio.

Supongo que te gusta

Origin blog.csdn.net/qq_36737803/article/details/112360609
Recomendado
Clasificación