Spring Boot 整合 RabbitMQ 訊息事務(消費者)

Jason207010發表於2024-10-11
  • 1. Spring Boot 整合 RabbitMQ 訊息事務(消費者)
    • 1.1. 版本說明
    • 1.2. 概覽
      • 1.2.1. 最大努力單階段提交模式
      • 1.2.2. 成功的業務流程
      • 1.2.3. 失敗的業務流程
    • 1.3. 新建資料庫表
    • 1.4. Spring 配置
    • 1.5. 定義常量
    • 1.6. 配置交換機和佇列
    • 1.7. 定義 RabbitMQ 訊息事務管理器
    • 1.8. 配置 SimpleMessageListenerContainer
    • 1.9. 定義資料庫事務管理器
    • 1.10. 測試
    • 1.11. 參考資料

1. Spring Boot 整合 RabbitMQ 訊息事務(消費者)

1.1. 版本說明

構件 版本
spring-boot 2.7.18
spring-boot-starter-amqp 2.7.18
spring-boot-starter-jdbc 2.7.18

1.2. 概覽

這裡模擬一個常見的業務流程,消費者接收到一條 RabbitMQ 訊息,此時消費者再發布一條 RabbitMQ 訊息,同時更新資料庫,更新失敗時回滾資料庫事務,同時拒絕先前接收到的訊息,被拒絕的訊息將被轉發到死信佇列,然後回滾訊息事務,消費者傳送出去的訊息將被回滾,並不會真正釋出到 RabbitMQ。這裡涉及到分散式事務,本案例採用最大努力單階段提交模式來實現事務管理。

sequenceDiagram participant producer as Spring Boot 應用(生產者) box lightYellow RabbitMQ participant exchange as 交換機 participant queue as 佇列 participant deadLetterExchange as 死信交換機 participant deadLetterQueue as 死信佇列 end participant consumer as Spring Boot 應用(消費者) participant mysql as 資料庫 producer ->> exchange: 1. 傳送訊息 exchange ->> queue: 2. 轉發訊息 consumer ->> consumer: 3. 開啟訊息事務 queue ->> consumer: 4. 接收訊息 consumer ->> consumer: 5. 開啟資料庫事務 consumer ->> exchange: 6. 釋出訊息 consumer ->> mysql: 7. 更新資料庫 mysql ->> consumer: 8. 更新失敗 consumer ->> consumer: 9. 回滾資料庫事務,撤回第 7 步更新操作 consumer ->> consumer: 10. 回滾訊息事務,撤回第 6 步釋出操作 consumer ->> queue: 11. 拒絕訊息 queue ->> deadLetterExchange: 12. 轉發訊息 deadLetterExchange ->> deadLetterQueue: 13. 轉發訊息

1.2.1. 最大努力單階段提交模式

最大努力單階段提交模式是相當普遍的,但在開發人員必須注意的某些情況下可能會失敗。這是一種非 XA 模式,涉及了許多資源的同步單階段提交。因為沒有使用二階段提交,它絕不會像 XA 事務那樣安全,但是如果參與者意識到妥協,通常就足夠了。許多高容量,高吞吐量的事務處理系統透過設定這種方式以達到提高效能的目的。

1.2.2. 成功的業務流程

flowchart TB 1["1. 開始訊息事務"] --> 2["2. 接收訊息"] --> 3["3. 開始資料庫事務"] --> 4["4. 釋出訊息"] --> 5["5. 更新資料庫"] --> 6["6. 提交資料庫事務"] --> 7["7. 提交訊息事務"]

1.2.3. 失敗的業務流程

flowchart TB 1["1. 開始訊息事務"] --> 2["2. 接收訊息"] --> 3["3. 開始資料庫事務"] --> 4["4. 釋出訊息"] --> 5["5. 更新資料庫失敗"] --> 6["6. 回滾資料庫事務"] --> 7["7. 回滾訊息事務"]

1.3. 新建資料庫表

create table t_user
(
    id   int auto_increment primary key,
    name varchar(20) not null
);

1.4. Spring 配置

spring:
  application:
    name: spring-rabbit-transaction-consumer-demo
  rabbitmq:
    addresses: 127.0.0.1:5672
    username: admin
    password: admin
    virtual-host: /
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/demo
    username: root
    password: root

1.5. 定義常量

public class RabbitTransactionConsumerConstants {
    public static final String QUEUE = "spring-rabbit-transaction-consumer-demo-queue";
    public static final String EXCHANGE = "spring-rabbit-transaction-consumer-demo-exchange";
    public static final String DEAD_LETTER_QUEUE = "spring-rabbit-transaction-consumer-demo-dead-latter-queue";
    public static final String DEAD_LETTER_EXCHANGE = "spring-rabbit-transaction-consumer-demo-latter-exchange";
    public static final String ROLLBACK_QUEUE = "spring-rabbit-transaction-consumer-demo-rollback-queue";
    public static final String ROLLBACK_EXCHANGE = "spring-rabbit-transaction-consumer-demo-rollback-exchange";
}

1.6. 配置交換機和佇列

@Bean
public Queue queue() {
    return QueueBuilder.durable(QUEUE)
            .deadLetterExchange(DEAD_LETTER_EXCHANGE)
            .build();
}

@Bean
public FanoutExchange exchange() {
    return ExchangeBuilder.fanoutExchange(EXCHANGE).durable(true).build();
}

@Bean
public Binding binding() {
    return BindingBuilder.bind(queue()).to(exchange());
}

@Bean
public FanoutExchange deadLetterExchange() {
    return ExchangeBuilder.fanoutExchange(DEAD_LETTER_EXCHANGE).durable(true).build();
}

@Bean
public Queue deadLetterQueue() {
    return QueueBuilder.durable(DEAD_LETTER_QUEUE)
            .build();
}

@Bean
public Binding deadLetterBinding() {
    return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange());
}

@Bean
public FanoutExchange rollbackExchange() {
    return ExchangeBuilder.fanoutExchange(ROLLBACK_EXCHANGE).durable(true).build();
}

@Bean
public Queue rollbackQueue() {
    return QueueBuilder.durable(ROLLBACK_QUEUE)
            .build();
}

@Bean
public Binding rollbackBinding() {
    return BindingBuilder.bind(rollbackQueue()).to(rollbackExchange());
}

1.7. 定義 RabbitMQ 訊息事務管理器

@Bean(name = "rabbitTransactionManager")
public RabbitTransactionManager rabbitTransactionManager(ConnectionFactory connectionFactory) {
    return new RabbitTransactionManager(connectionFactory);
}

1.8. 配置 SimpleMessageListenerContainer

@Bean
public ContainerCustomizer<SimpleMessageListenerContainer> simpleMessageListenerContainerCustomizer(RabbitTransactionManager rabbitTransactionManager) {
    return container -> {
        //Channel 開啟事務
        // 由於訊息事務只適用於釋出(publish)和確認(ack)
        // 在消費端 Spring 預設會自動 ack
        // 因此,只有消費訊息的情況下,並不需要開啟事務
        // 只有在消費訊息的同時還發布訊息出去,才需要配置開啟訊息事務
        container.setChannelTransacted(true);
        //配置事務管理器
        container.setTransactionManager(rabbitTransactionManager);
        //設定拒絕訊息的行為
        //值為 true 時,將丟擲 AmqpRejectAndDontRequeueException 異常並重新消費該訊息
        //值為 false 時,將訊息轉發到死信佇列中
        container.setDefaultRequeueRejected(false);
    };
}

1.9. 定義資料庫事務管理器

@Bean(name = "dataSourceTransactionManager")
@Primary
DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource, ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
    DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
    transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(dataSourceTransactionManager));
    return dataSourceTransactionManager;
}

1.10. 測試

@Component
@Slf4j
public class SpringRabbitTransactionConsumerDemo implements ApplicationRunner {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        rabbitTemplate.convertAndSend(EXCHANGE, null, "Jason");
    }

    //Spring 資料庫事務,指定事務管理器為 DataSourceTransactionManager
    @Transactional(rollbackFor = Throwable.class, transactionManager = "dataSourceTransactionManager")
    @RabbitListener(queues = {QUEUE})
    public void listen(Channel channel, Message<String> message) throws Throwable {
        //由於 Channel 已配置為開啟事務,因此這裡傳送出去的訊息將會在出現異常時回滾
        channel.basicPublish(ROLLBACK_EXCHANGE, "", null, "rollback".getBytes());
        //往資料庫表插入兩條主鍵 id 一樣的資料,引起主鍵 id 重複異常
        jdbcTemplate.update("INSERT INTO t_user (id, name) VALUES (1, ?)", ps -> ps.setString(1, message.getPayload()));
        jdbcTemplate.update("INSERT INTO t_user (id, name) VALUES (1, ?)", ps -> ps.setString(1, message.getPayload()));
    }
}

1.11. 參考資料

  • Spring 官方文件
  • Distributed transactions in Spring, with and without XA

相關文章