RocketMQ的事务消息


前言

有种场景,方法上即便是加了 @Transactional(rollbackFor = Exception.class) 也会出现分布式事务的问题。
例如:public void fun(){
update1();
MQsend();
update2();
}
update2();出现异常,update1();可以回滚,但是MQ消息已经发出去了,撤不回来了。
针对与这种情况,RocketMQ 实现了事务消息。

在这里插入图片描述


一、RocketMQ 事务消息是什么?

1.本地方法执行发送MQ事务消息,这个消息实际被标注了"不可投递"
2.本地方法执行完成后会发送"二次确认:提交\回滚",此时MQ消息就知道这个消息是要投递还是要丢弃了。
3.如果MQ没有收到"二次确认",也就是断网或者异常了,则MQ会进行"回查"检查本地事务状态,根据回查的结果进行投递或者丢弃。

二、使用步骤

例如:文章审核功能,审核员在看完一篇文章之后点击审核通过,传给后台论文id和审核信息。

1.生产者

private final RocketMQTemplate rocketMQTemplate;


public Share auditById(Integer id, ShareAuditDTO auditDTO) {
        Share share = this.shareMapper.selectByPrimaryKey(id);
        if (share == null) {
            throw new IllegalArgumentException("参数非法!该分享不存在!");
        }
        if (!Objects.equals("NOT_YET", share.getAuditStatus())) {
            throw new IllegalArgumentException("参数非法!该分享已审核通过或审核不通过!");
        }
        //如果是PASS,那么发送消息给rocketmq,让用户中心去消费,并为发布人添加积分
        if (AuditStatusEnum.PASS.equals(auditDTO.getAuditStatusEnum())) {
            //把需要与MQ消息做一致事务的业务逻辑 挪到MQ的TransactionListener里面
            String uuid = UUID.randomUUID().toString();
            //sendMessageInTransaction(txGroup,Topic,Message,Ojb args)
            //txGroup:要与TransactionListener的@RocketMQTransactionListener的txProducerGroup一致
            //Topic要与消费者的Listener的@RocketMQMessageListener的topic一致
            //Message 以及 Message的Header 和args 都是用来传参数的,以变执行业务逻辑
            this.rocketMQTemplate.sendMessageInTransaction(
                    "tx_add_bonus_group",
                    "add_bouns",
                     MessageBuilder.withPayload(UserAddBonusMsgDTO.builder().userId(share.getUserId()).bonus(50).build()).setHeader(RocketMQHeaders.TRANSACTION_ID,uuid).setHeader("share_id",id).build(),
                    auditDTO
            );
            //TODO 其他业务逻辑
        }else{
            this.auditByIdInDB(id, auditDTO);
        }
        return share;
    }

    @Transactional(rollbackFor = Exception.class)
    public void auditByIdInDB(Integer id, ShareAuditDTO auditDTO) {
        Share share = Share.builder()
                .id(id)
                .auditStatus(auditDTO.getAuditStatusEnum().toString())
                .reason(auditDTO.getReason())
                .build();
        this.shareMapper.updateByPrimaryKeySelective(share);

    }
    @Transactional(rollbackFor = Exception.class)
    public void auditByIdWithRocketMqLog(Integer id, ShareAuditDTO auditDTO, String transactionId) {
        this.auditByIdInDB(id, auditDTO);

        this.rocketmqTransactionLogMapper.insertSelective(
                RocketmqTransactionLog.builder()
                        .transactionId(transactionId)
                        .log("审核分享...")
                        .build()
        );
    }

2.生产者TransactionListener


import com.itmuch.contentcenter.dao.RocketmqTransactionLog.RocketmqTransactionLogMapper;
import com.itmuch.contentcenter.domain.dto.content.ShareAuditDTO;
import com.itmuch.contentcenter.domain.entity.RocketmqTransactionLog.RocketmqTransactionLog;
import com.itmuch.contentcenter.service.content.ShareService;
import lombok.RequiredArgsConstructor;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;


@RocketMQTransactionListener(txProducerGroup = "tx_add_bonus_group")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AddBonusTransactionListener implements RocketMQLocalTransactionListener {

    private final ShareService shareService;
    private final RocketmqTransactionLogMapper rocketmqTransactionLogMapper;


    @Override
    //执行本地事务
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object args) {

        MessageHeaders headers = message.getHeaders();

        String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
        Integer shareId = Integer.valueOf((String) headers.get("share_id"));
        ShareAuditDTO auditDTO = (ShareAuditDTO)args;
        try{
            //执行本地的业务逻辑,并存MQ消息到LOG表
            //存LOG表是为了实现'本地事务的检查接口',遇到长时间不给MQ发二次确认消息的本地事务,MQ会通过检查接口去查LOG表,查到了数据则返回COMMIT,没有查到返回ROLLBACK
            this.shareService.auditByIdWithRocketMqLog(shareId,auditDTO,transactionId);
            return RocketMQLocalTransactionState.COMMIT;
        }catch (Exception e){
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    @Override
    //本地事务的检查接口
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        MessageHeaders headers = message.getHeaders();

        String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
        RocketmqTransactionLog rocketmqTransactionLog = this.rocketmqTransactionLogMapper.selectOne(RocketmqTransactionLog.builder().transactionId(transactionId).build());
        if(rocketmqTransactionLog !=null){
            return RocketMQLocalTransactionState.COMMIT;
        }
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}

3.消费者


import com.itmuch.usercenter.dao.user.BonusEventLogMapper;
import com.itmuch.usercenter.dao.user.UserMapper;
import com.itmuch.usercenter.domain.entity.dto.massaging.UserAddBonusMsgDTO;
import com.itmuch.usercenter.domain.entity.user.BonusEventLog;
import com.itmuch.usercenter.domain.entity.user.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;

@Slf4j
@RocketMQMessageListener(consumerGroup = "consumer-group",topic = "add_bouns")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Service
public class AddBonusListener implements RocketMQListener<UserAddBonusMsgDTO> {

    private final UserMapper userMapper;
    private final BonusEventLogMapper  bonusEventLogMapper;
    @Override
    public void onMessage(UserAddBonusMsgDTO userAddBonusMsgDTO) {

        User user = userMapper.selectByPrimaryKey(userAddBonusMsgDTO.getUserId());
        user.setBonus(user.getBonus()+userAddBonusMsgDTO.getBonus());
        userMapper.updateByPrimaryKey(user);
        bonusEventLogMapper.insert(BonusEventLog.builder().userId(user.getId()).event("UPDATE").createTime(new Date()).description("加积分").value(userAddBonusMsgDTO.getBonus()).build());

    }
}

三. 基于Spring Cloud Stream + RocketMQ实现分布式事务

1.生产者、消费者 加依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>

2. 生产者加配置

spring:
  cloud:
    stream:
      rocketmq:
        binder:
          name-server: localhost:9876
          bindings:
            output:
              producer:
                transational: true
                group: tx_add_bonus_group #source.output().send 的group
      bindings:
        output:  #也叫channel
          destination: stream-topic #指定 source.output().send的topic

3.生产者加注解

启动类上加注解

@EnableBinding(Source.class)

4.生产者

private final Source source;


public Share auditById(Integer id, ShareAuditDTO auditDTO) {
        Share share = this.shareMapper.selectByPrimaryKey(id);
        if (share == null) {
            throw new IllegalArgumentException("参数非法!该分享不存在!");
        }
        if (!Objects.equals("NOT_YET", share.getAuditStatus())) {
            throw new IllegalArgumentException("参数非法!该分享已审核通过或审核不通过!");
        }
        //如果是PASS,那么发送消息给rocketmq,让用户中心去消费,并为发布人添加积分
        if (AuditStatusEnum.PASS.equals(auditDTO.getAuditStatusEnum())) {
            //把需要与MQ消息做一致事务的业务逻辑 挪到MQ的TransactionListener里面
            String uuid = UUID.randomUUID().toString();
            //Message 以及 Message的Header 都是用来传参数的,以变执行业务逻辑
            //source的send(),只有一个Message参数,没有args,所以原来args的参数需要放到header里
            //send的topic和group都在配置文件里指定了
            this.source.output().send(
                    MessageBuilder.withPayload(
                            UserAddBonusMsgDTO.builder().userId(share.getUserId()).bonus(50).build()).setHeader(RocketMQHeaders.TRANSACTION_ID,uuid
                    ).setHeader("share_id",id).setHeader("dto",JSON.toJSONString(auditDTO)).build());
            //TODO 其他业务逻辑
        }else{
            this.auditByIdInDB(id, auditDTO);
        }
        return share;
    }

5.生产者TransactionListener


import com.alibaba.fastjson.JSON;
import com.itmuch.contentcenter.dao.RocketmqTransactionLog.RocketmqTransactionLogMapper;
import com.itmuch.contentcenter.domain.dto.content.ShareAuditDTO;
import com.itmuch.contentcenter.domain.entity.RocketmqTransactionLog.RocketmqTransactionLog;
import com.itmuch.contentcenter.service.content.ShareService;
import lombok.RequiredArgsConstructor;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;


@RocketMQTransactionListener(txProducerGroup = "tx_add_bonus_group")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AddBonusTransactionListener implements RocketMQLocalTransactionListener {

    private final ShareService shareService;
    private final RocketmqTransactionLogMapper rocketmqTransactionLogMapper;


    @Override
    //执行本地事务
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object args) {

        MessageHeaders headers = message.getHeaders();

        String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
        Integer shareId = Integer.valueOf((String) headers.get("share_id"));
        //从headers里取到的都是String
        String dtoString = (String) headers.get("dto");
        ShareAuditDTO auditDTO = JSON.parseObject(dtoString, ShareAuditDTO.class);
        try{
            //执行本地的业务逻辑,并存MQ消息到LOG表
            //存LOG表是为了实现'本地事务的检查接口',遇到长时间不给MQ发二次确认消息的本地事务,MQ会通过检查接口去查LOG表,查到了数据则返回COMMIT,没有查到返回ROLLBACK
            this.shareService.auditByIdWithRocketMqLog(shareId,auditDTO,transactionId);
            return RocketMQLocalTransactionState.COMMIT;
        }catch (Exception e){
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    @Override
    //本地事务的检查接口
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        MessageHeaders headers = message.getHeaders();

        String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
        RocketmqTransactionLog rocketmqTransactionLog = this.rocketmqTransactionLogMapper.selectOne(RocketmqTransactionLog.builder().transactionId(transactionId).build());
        if(rocketmqTransactionLog !=null){
            return RocketMQLocalTransactionState.COMMIT;
        }
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}

7.消费者加配置

spring:
  cloud:
    stream:
      rocketmq:
        binder:
          name-server: localhost:9876
      bindings:
        input:
          destination: stream-topic #消费哪个topic
          group: stream-group #一定要指定,否则启动不了

8.消费者加注解

@EnableBinding(Sink.class)

9.消费者


import com.itmuch.usercenter.domain.entity.dto.massaging.UserAddBonusMsgDTO;
import com.itmuch.usercenter.service.user.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.stereotype.Service;


@Service
@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AddBonusStreamListener {

    private final UserService userService;

    @StreamListener(Sink.INPUT)
    public void receive(UserAddBonusMsgDTO userAddBonusMsgDTO){
        log.info("AddBonusStreamListener.receive.userAddBonusMsgDTO:"+userAddBonusMsgDTO.toString());
        //业务逻辑
        userService.addBonus(userAddBonusMsgDTO);
    }


    @StreamListener("errorChannel")
    public void error(Message<?> message){
        ErrorMessage errorMessage = (ErrorMessage)message;
        log.warn("出现异常:{}",errorMessage);

    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值