rocketmq介绍和消息队列事务处理机制

RocketMQ介绍
rocketmq是支持发布(Pub)和订阅(Sub),可靠的先进先出、严格顺序、亿级消息堆积能力的分布式消息队列
rocketmq消息队列包含Producer、Name Server、Broker、Consumer等四大块组成
Producer:也就是常说的生产者,生产者的作用就是将消息发送到 MQ,生产者本身既可以产生消息,如读取文本信息,将读取的文本信息发送到 MQ
Name Server:提供轻量级的服务发现和路由信息,每个 NameServer 记录完整的路由信息,提供等效的读写服务,并支持快速存储扩展
Broker:是 RocketMQ 系统的主要角色,就是前面一直说的 MQ。Broker 接收来自生产者的消息,储存以及为消费者拉取消息的请求做好准备
Consumer:也就是常说的消费者,接收 MQ 消息的应用程序就是一个消费者。拥有相同 Consumer Group 的消费者称为一个消费者集群
生产者走向: Producer--->Name Server--->Broker

消费者走向: Customer--->Name Server--->Broker

以下是其他相关支持组件
Topic:主题是对消息的逻辑分类,比如说有订单类相关的消息,也有支付类相关的消息,那么就需要进行分类。
Tag:标签可以被认为是对主题的进一步细化,可以理解为二级分类,一般在相同业务模块中通过引入标签来标记不同用途,同时消费者也可以根据不同的标签进行消息的过滤。
如下所示:
Message msg = new Message("TopicTest",// topic
                            "TagA",                     // tag
                            "ORDER",                    // key
                            ("Hello").getBytes()); // body

SendResult sendResult = producer.send(msg);

部署Broker
部署方式包含单Master、多Master、多Master多Slave(集群采用异步复制方式)、多Master多Slave(集群采用同步双写方式)等四大类组成
单Master: 优点:除了配置简单没什么优点
          缺点:不可靠,该机器重启或宕机,将导致整个服务不可用
多Master:优点:配置简单,性能最高
缺点:可能会有少量消息丢失(配置相关),单台机器重启或宕机期间,该机器下未被消费的消息在机器恢复前不可订阅,影响消息实时性
多Master多Slave(集群采用异步复制方式):   
优点:性能同多Master几乎一样,实时性高,主备间切换对应用透明,不需人工干预
        缺点:Master宕机或磁盘损坏时会有少量消息丢失
多Master多Slave(集群采用同步双写方式):
       优点:服务可用性与数据可用性非常高
       缺点:性能比异步集群略低,当前版本主宕备不能自动切换为主

消息队列分布式事务

一般用法比较广泛的消息队列有rabbitMQ、rocketMQ、kafka等消息中间件,但是rabbitMQ和kafka这两个消息中间件没有事务,接下来我们以rocketMQ举例子,

我们以一个转帐的场景为例来说明这个问题:Bob向Smith转账100块。

在开发中我们将大事务拆分成=小事务+异步

事务流程:

第一步:首先生产者发送Prepared消息给MQ,状态为不可消费状态(就是消费者无法消息)   ---该消息包含Smith姓名以及转账金额和状态等等

第二步:执行本地事务   ---该事务就是从Bob账户扣款100块

第三部:如果本地事务执行失败就回滚,并将Prepared消息删除,如果本地事务执行成功,确认消息发送(就是修改Prepared消息状态为可消费状态)

第四步:消费端开始消费队列。如果成功,给MQ返回回执。表示成功,生产者可以订阅该消息或者监听该消息,表示整个消息队列是成功的  --比如:支付包账户余额转到银行卡。到账状态和时间都是靠生产者监听和订阅MQ实现转账成功的逻辑。

如果消息端出现消费失败(这里有好多原因:程序报错,账户被冻结了等等),那么MQ可以重发消息。可以设定6次,超过次数可人工处理。

------------------------------------------------------------------------------事务消息实例案例-------------------------------------------------------------------------------------

什么是事务消息?

事务消息就是将发送消息和本地数据库操作融合为同一个事务二者要么都成功,要么都失败,不能出现一个操作成功另一操作失败的情况

以用户注册成功时向用户发送欢迎邮件为例。有新用户注册时,Producer 向MQ发送新用户信息,消费者消费消息发送欢迎邮件。

此时 Producer 的操作可以简化为两步: 将新用户插入到数据库 发送MQ消息

一般情况下,我们会将发送消息的操作写在数据库的事务里,尤其是将发送消息的操作放在最后一步,这样当消息发送异常时可以回滚数据库事务。向下面这样:

@Transactional
public void saveUser(User user){
    // ....
    // 将用户保存到数据库
    // ...
    // 发送MQ消息
}
复制代码

这样看似没什么问题,如果插入数据库失败也不会发送消息,发送消息失败整个事务也会回滚。

但是有一种情况会导致二者不一致,就是当插入数据库成功,消息也发送成功,但是由于网络等原因,Producer超时未收到 Broker 的确认(rocketMQ 同步发送方式需要接收 Broker 的确认),此时 Producer 会抛出异常,认为消息发送失败,进而导致本地事务回滚。导致的最终结果就是消息被消费,但是数据库中却没有用户的信息。

这种方式还有一种缺陷,当消息发送成功后会立即被消费者消费,但是此时 Producer 本地的数据库事务可能还没有提交,即使我们将发送消息的操作放在最后一步,我们也不能保证消费者拿到消息时 Producer 的本地事务已经提交。如果消费业务依赖于 Producer 的本地事务,此时消费者就不能从数据库中获取到 Producer 保存的数据。

RocketMQ 事务消息

RocketMQ 事务消息的原理

RocketMQ 事务消息将消息的发送分解为两个阶段:

第一阶段发送消息,Producer把消息发送到 Broker ,但是此时该消息还不能被投递给消费者,此时消息的状态被称为半消息(Half Message) 。

第二阶段提交消息。类似于数据库事务的提交,当对半消息进行二次确认,对消息进行提交或者回滚,成功提交的消息才可以被投递给消费者,回滚的消息会被删除。

事务状态回查

一般情况下,Producer 根据本地事务的执行结果,主动对半消息发送二次确认(commit 或者 rollback),但是可能由于网络或者程序代码问题等原因 Broker 未收到二次确认的消息。此时 Broker 会主动向 Producer 发起事务状态回查,根据回查的结果决定消息的去留

Tips:若未收到来自 Producer 的二次确认,Broker默认每隔 1分钟 回查一次,最多回查 15 次,若达到最大次数后仍未提交或者回滚,消息会被删除

代码示例

以Spring Boot 整合 RocketMQ 为例,业务:新用户注册时发送MQ消息

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.0.4</version>
</dependency>
复制代码
rocketmq.name-server=127.0.0.1:9876
复制代码

UserService

提供保存以及查询用户的数据库操作方法。

@Service
public class UserService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
	
    @Transactional
    public void addUser(User user){
        String sql = "INSERT INTO t_user (username, password, email) VALUES (?, ?, ?)";
        jdbcTemplate.update(sql, user.getUsername(), user.getPassword(), user.getEmail());
    }

     public User getByUsername(String username){
        String sql = "SELECT * FROM t_user WHERE username = ?";
        return jdbcTemplate.queryForObject(sql, new RowMapper<User>() {
            @Override
            public User mapRow(ResultSet resultSet, int i) throws SQLException {
                User user = new User();
                user.setUsername(resultSet.getString("username"));
                return user;
            }
        }, username);
    }
}
复制代码

实现 RocketMQLocalTransactionListener

@Slf4j
@Component
@RocketMQTransactionListener(txProducerGroup = "user_tx_producer_group")
public class UserTransactionListener implements RocketMQLocalTransactionListener {
    @Autowired
    private UserService userService;

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        log.info("开始执行本地事务");
        try {
            User user = (User) arg;
            userService.addUser(user);
        }catch (Exception e){
            log.error("本地事务执行异常, 回滚消息");
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        log.info("本地事务执行成功, 提交消息");
        return RocketMQLocalTransactionState.COMMIT;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        log.info("开始本地事务状态回查");
        RocketMQLocalTransactionState localTransactionState;
        if (userService.getByUsername(((User) msg.getPayload()).getUsername()) != null) {
            localTransactionState = RocketMQLocalTransactionState.COMMIT;
        }else {
            localTransactionState = RocketMQLocalTransactionState.UNKNOWN;
        }
        log.info("本地事务状态回查结果:{}", localTransactionState);
        return localTransactionState;
    }

}
复制代码

RocketMQLocalTransactionListener 接口定义了两个接口。分别是半消息发送成功后的本地事务回调方法,和事务状态回查方法。

其实现类要使用 @RocketMQTransactionListener 注解,并定义其 txProducerGroup 属性值,该属性值可以看作是Listener的标识,发送消息时需要指定该标识,然后才能找到对于的 RocketMQLocalTransactionListener 实现类。

UserController 接口

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @PostMapping("/add")
    public String addUser(@RequestBody User user){
        Message<User> message = MessageBuilder.withPayload(user).build();
        rocketMQTemplate.sendMessageInTransaction("user_tx_producer_group", "user-topic", message, user);
        return "success";
    }
}
复制代码

发送事务消息使用 rocketMQTemplate.sendMessageInTransaction()方法,传递的四个参数从左至右依次为

① 本地事务回调的实现类标识(即UserTransactionListener上面的@RocketMQTransactionListener(txProducerGroup = "user_tx_producer_group"))、

② 消息的topic

③ 消息体

④ 额外参数,回调本地事务时会传递该参数。即executeLocalTransaction(Message msg, Object arg)方法的第二个参数。

使用postman调用用户接口

事务状态回查模拟

我们先在执行本地事务时打上断点,在返回事务状态前一直阻塞Producer程序,来模拟发送二次确认失败的情况,从而触发Broker的事务状态回查。

使用postman调用接口,当程序执行到断点位置处时阻塞,半消息和本地事务都已执行成功但还未发送二次确认,Broker等到60s的时间间隔后就会触发事务回查。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值