RocketMQ --- 实战篇

本文深入探讨了电商场景下基于RocketMQ的下单和支付业务,包括业务分析、技术选型(SpringBoot、Dubbo、Zookeeper、RocketMQ、MySQL),详细讲解了SpringBoot整合RocketMQ的生产者和消费者的配置与使用,以及如何处理失败的补偿机制。同时,还介绍了环境搭建、数据库设计和整体联调的过程。
摘要由CSDN通过智能技术生成

一、案例介绍

1.1、业务分析

模拟电商网站购物场景中的【下单】和【支付】业务

1.1.1、下单

在这里插入图片描述
流程

  1. 用户请求订单系统下单

  2. 订单系统通过RPC调用订单服务下单

  3. 订单服务调用优惠券服务,扣减优惠券

  4. 订单服务调用调用库存服务,校验并扣减库存

  5. 订单服务调用用户服务,扣减用户余额

  6. 订单服务完成确认订单

1.1.2、支付

在这里插入图片描述
流程

  1. 用户请求支付系统
  2. 支付系统调用第三方支付平台API进行发起支付流程
  3. 用户通过第三方支付平台支付成功后,第三方支付平台回调通知支付系统
  4. 支付系统调用订单服务修改订单状态
  5. 支付系统调用积分服务添加积分
  6. 支付系统调用日志服务记录日志

1.2、问题分析

1.2.1、问题1

用户提交订单后,扣减库存成功、扣减优惠券成功、使用余额成功,但是在确认订单操作失败,需要对库存、库存、余额进行回退。

如何保证数据的完整性?

在这里插入图片描述

使用MQ保证在下单失败后系统数据的完整性

在这里插入图片描述

1.2.2、问题2

用户通过第三方支付平台(支付宝、微信)支付成功后,第三方支付平台要通过回调API异步通知商家支付系统用户支付结果,支付系统根据支付结果修改订单状态、记录支付日志和给用户增加积分。

商家支付系统如何保证在收到第三方支付平台的异步通知时,如何快速给第三方支付凭条做出回应?

在这里插入图片描述

通过MQ进行数据分发,提高系统处理性能

在这里插入图片描述

二.、技术分析

2.1、技术选型

  • SpringBoot
  • Dubbo
  • Zookeeper
  • RocketMQ
  • Mysql

在这里插入图片描述

2.2、SpringBoot整合RocketMQ

下载rocketmq-spring项目

将rocketmq-spring安装到本地仓库

mvn install -Dmaven.skip.test=true

2.2.1、消息生产者

2.2.1.1、添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>rocketmq-producer</artifactId>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <spring-boot-version>2.3.2.RELEASE</spring-boot-version>
    </properties>

    <dependencies>
        <!-- 配置web启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!-- rocketMQ -->
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!-- lombok插件 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot-version}</version>
            </plugin>
        </plugins>
    </build>
</project>
2.2.1.2、配置文件
server:
  port: 8094
spring:
  application:
    name: rocketmq-producer

rocketmq:
  name-server: 127.0.0.1:9876          # rocketMQ的名称服务器,格式为:' host:port;host:port '。
  # 生产端配置
  producer:
    group: ${
   spring.application.name}  # 生产着组名
    #access-key: access-key            # rocketMQ服务端配置acl授权信息,没有则不需要
    #secret-key: secret-key            # rocketMQ服务端配置acl授权信息,没有则不需要
  # 消费端配置
#  consumer:
#    access-key: access-key            #如果开启了acl,一定要配置。否则集群模式下会正常,广播模式消费端会失效!
#    secret-key: secret-key            #如果开启了acl,一定要配置。否则集群模式下会正常,广播模式消费端会失效!
2.2.1.3、消息发送
/**
 * <p>
 * RocektMQ 事务消息监听器
 * </p>
 *
 **/
@Slf4j
@RocketMQTransactionListener
public class TransactionListenerImpl implements RocketMQLocalTransactionListener {
   


    /**
     * 检测半消息,在该方法中,执行本地事务
     *
     * @param msg 发送消息
     * @param arg 外部参数
     * @return commit:提交事务,它允许消费者消费此消息。bollback:回滚事务,它代表该消息将被删除,不允许被消费。 unknown:中间状态,它代表需要检查消息队列来确定状态(checkLocalTransaction方法)。
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
   
        log.info(">>>> MQ事务执行器,执行本地事务 message={},args={} <<<<", msg, arg);

        try {
   
            String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
            OrderPaidEvent payload = JSON.parseObject(jsonString, OrderPaidEvent.class);

            //模拟业务操作,当paidMoney >5 则提交,否则等事务会查
            if (payload.getPaidMoney().compareTo(new BigDecimal("5")) > 0) {
   
                //提交事务
                log.info("MQ提交事务啦!payload ={} ", payload);
                return RocketMQLocalTransactionState.COMMIT;
            }

            //不知道状态,转 checkLocalTransaction 回查执行
            log.info("MQ无法确定,等回查!payload ={} ", payload);
            return RocketMQLocalTransactionState.UNKNOWN;
        } catch (Exception e) {
   
            log.error("事务消息出错啦~ e:{}", e.getMessage(), e);
            //回滚
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }


    /**
     * 该方法时MQ进行消息事务状态回查、
     * <p>
     *
     * @param msg
     * @return bollback, commit or unknown
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
   
        log.info(">>>> MQ事务执行器,事务状态回查 message={} <<<<", msg);
        try {
   
            String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
            OrderPaidEvent payload = JSON.parseObject(jsonString, OrderPaidEvent.class);

            log.info("事务回查:checkLocalTransaction提交事务啦!payload ={} ", payload);
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
   
            log.error("回调的事务出错啦~ e:{}", e.getMessage(), e);
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
}
/**
 * <p>
 * RocektMQ生产者常用发送消息方法
 * 最佳实践:https://github.com/apache/rocketmq/blob/master/docs/cn/best_practice.md
 * </p>
 *
 **/
public interface IRocketMQService {
   

    /**
     * 发送同步消息(这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。)
     * <p>
     * (send消息方法只要不抛异常,就代表发送成功。但不保证100%可靠投递(所有消息都一样,后面不在叙述)。
     * 要确保不会丢失任何消息,还应启用同步Master服务器或同步刷盘,即SYNC_MASTER或SYNC_FLUSH。
     * 解析看:https://github.com/apache/rocketmq/blob/master/docs/cn/best_practice.md
     * )
     *
     * @param destination 主题名:标签 topicName:tags
     * @param msg         发送对象
     * @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
     */
    SendResult sendMessage(String destination, Object msg);

    /**
     * 发送同步消息(这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。)
     *
     * @param topicName 主题名 topicName
     * @param tags      标签 tags
     * @param msg       发送对象
     * @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
     */
    SendResult sendMessage(String topicName, String tags, Object msg);

    /**
     * 发送同步消息(这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。)
     *
     * @param topicName 主题名 topicName
     * @param tags      标签 tags
     * @param key       唯一标识码要设置到keys字段,方便将来定位消息丢失问题
     * @param msg       发送对象
     * @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
     */
    SendResult sendMessage(String topicName, String tags, String key, Object msg);

    /**
     * 发送同步消息-SQL92模式
     * 需要配置RocketMQ服务器  vim conf/broker.conf  ##支持sql语句过滤  enablePropertyFilter=true
     * 在console控制台查看集群状态  enablePropertyFilter=true 才正常
     *
     * @param topicName 主题名 topicName
     * @param map       自定义属性
     * @param msg       发送对象
     * @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
     */
    SendResult sendMessageBySql(String topicName, Map<String, Object> map, Object msg);


    /**
     * 发送同步消息-SQL92模式
     * 需要配置RocketMQ服务器  vim conf/broker.conf  ##支持sql语句过滤  enablePropertyFilter=true
     * 在console控制台查看集群状态  enablePropertyFilter=true 才正常
     *
     * @param topicName 主题名 topicName
     * @param map       自定义属性
     * @param key       唯一标识码要设置到keys字段,方便将来定位消息丢失问题
     * @param msg       发送对象
     * @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
     */
    SendResult sendMessageBySql(String topicName, Map<String, Object> map, String key, Object msg);


    /**
     * 发生异步消息(异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。)
     *
     * @param destination  主题名:标签 topicName:tags
     * @param msg          发送对象
     * @param sendCallback 异步回调函数
     */
    void sendAsyncMessage(String destination, Object msg, SendCallback sendCallback);

    /**
     * 发送单向消息(这种方式主要用在不特别关心发送结果的场景,例如日志发送。)
     *
     * @param destination 主题名:标签 topicName:tags
     * @param msg         发送对象
     */
    void sendOneway(String destination, Object msg);

    /**
     * 发送批量消息(发送超过1MB,做了自动分割,超时时间设置30s(默认3s)),注:默认最大是4MB,为了避免ListSplitter.calcMessageSize计算不精确及大批量数据发送超时才设置1MB
     *
     * @param destination 主题名:标签 topicName:tags
     * @param list        批量消息
     */
    void sendBatchMessage(String destination, List<?> list);


    /**
     * 发送批量消息(发送超过1MB,做了自动分割。),注:默认最大是4MB,为了避免ListSplitter.calcMessageSize计算不精确及大批量数据发送超时才设置1MB
     *
     * @param topicName 主题名 topicName
     * @param tags      标签 tags
     * @param timeout   超时时间,空则默认设为30s
     * @param list      批量消息
     */
    void sendBatchMessage(String topicName, String tags, Long timeout, List<?> list);

    /**
     * 发送延时消息(超时时间,设置30s(默认3s))
     * 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
     * 1  2  3   4   5  6  7  8  9  10 11 12 13 14  15  16  17 18
     *
     * @param destination    主题名:标签 topicName:tags
     * @param msg            发送对象
     * @param delayTimeLevel 延时等级(从1开始)
     * @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
     */
    SendResult sendDelayLevel(String destination, Object msg, int delayTimeLevel);

    /**
     * 发送延时消息
     * 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
     * 1  2  3   4   5  6  7  8  9  10 11 12 13 14  15  16  17 18
     *
     * @param destination    主题名:标签 topicName:tags
     * @param msg            发送对象
     * @param timeout        超时时间(单位毫秒)
     * @param delayTimeLevel 延时等级(从1开始)
     * @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
     */
    SendResult sendDelayLevel(String destination, Object msg, int timeout, int delayTimeLevel);


    /**
     * 发送顺序消息(分区有序,多个queue参与,即相对每个queue,消息都是有序的。)
     *
     * @param destination 主题名:标签 topicName:tags
     * @param msg         发送对象
     * @param hashKey     根据其哈希值取模后确定发送到哪一个queue队列
     * @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
     */
    SendResult sendInOrder(String destination, Object msg, String hashKey);


    /**
     * 发送事务消息
     * 事务消息使用上的限制
     * 1:事务消息不支持延时消息和批量消息。
     * 2:为了避免单个消息被检查太多次而导致半队列消息累积,我们默认将单个消息的检查次数限制为 15 次,但是用户可以通过 Broker 配置文件的 transactionCheckMax参数来修改此限制。如果已经检查某条消息超过 N 次的话( N = transactionCheckMax ) 则 Broker 将丢弃此消息,并在默认情况下同时打印错误日志。用户可以通过重写 AbstractTransactionalMessageCheckListener 类来修改这个行为。
     * 3:事务消息将在 Broker 配置文件中的参数 transactionTimeout 这样的特定时间长度之后被检查。当发送事务消息时,用户还可以通过设置用户属性 CHECK_IMMUNITY_TIME_IN_SECONDS 来改变这个限制,该参数优先于 transactionTimeout 参数。
     * 4:事务性消息可能不止一次被检查或消费。
     * 5:提交给用户的目标主题消息可能会失败,目前这依日志的记录而定。它的高可用性通过 RocketMQ 本身的高可用性机制来保证,如果希望确保事务消息不丢失、并且事务完整性得到保证,建议使用同步的双重写入机制。
     * 6:事务消息的生产者 ID 不能与其他类型消息的生产者 ID 共享。与其他类型的消息不同,事务消息允许反向查询、MQ服务器能通过它们的生产者 ID 查询到消费者。
     *
     * @param destination 主题名:标签 topicName:tags
     * @param msg         发送对象
     * @param arg         arg
     * @return 发送结果,只有为SEND_OK且同步Master服务器或同步刷盘才保证100%投递成功
     */
    SendResult sendMessageInTransaction(String destination, Object msg, Object arg);
}
/**
 * <p>
 * RocektMQ生产者常用发送消息方法
 * 最佳实践:https://github.com/apache/rocketmq/blob/master/docs/cn/best_practice.md
 * </p>
 *
 */
@Slf4j
@Service
public class RocketMQServiceImpl implements IRocketMQService {
   

    @Autowired
    private RocketMQTemplate rocketMqTemplate;

    /**
     * 原生的producer
     */
    @Autowired
    private DefaultMQProducer producer;

    @Override
    public SendResult sendMessage(String destination, Object msg) {
   
        String[] split = destination.split(":");
        if (split.length == 2) {
   
            return this.sendMessage(split[0], split[1], msg);
        }
        return this.sendMessage(destination, null, msg);
    }

    @Override
    public SendResult sendMessage(String topicName, String tags, Object msg) {
   
        return this.sendMessage(topicName, tags, null, msg);
    }

    @Override
    public SendResult sendMessage(String topicName, String tags, String key, Object msg) {
   
        MessageBuilder<?> messageBuilder = MessageBuilder.withPayload(msg);
        //设置key,唯一标识码要设置到keys字段,方便将来定位消息丢失问题
        if (StringUtils.isNotBlank(key)) {
   
            messageBuilder.setHeader(MessageConst.PROPERTY_KEYS, key);
        }
        Message<?> message = messageBuilder.build();
        SendResult sendResult = this.rocketMqTemplate.syncSend(StringUtils.isBlank(tags) ? topicName : (topicName + ":" + tags), message);
        if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
   
            log.info("MQ发送同步消息成功,topicName={},tags={},msg={},sendResult={}", topicName, tags, msg, sendResult);
        } else {
   
            log.warn("MQ发送同步消息不一定成功,topicName={},tags={},msg={},sendResult={}", topicName, tags, msg, sendResult);
        }
        return sendResult;
    }

    @Override
    public SendResult sendMessageBySql(String topicName, Map<String, Object> map, Object msg) {
   
        return this.sendMessageBySql(topicName, map, null, msg);
    }

    @Override
    public SendResult sendMessageBySql(String topicName, Map<String, Object> map, String key, Object msg) {
   
        MessageBuilder<?> messageBuilder = MessageBuilder.withPayload(msg);
        //设置key,唯一标识码要设置到keys字段,方便将来定位消息丢失问题
        if (StringUtils.isNotBlank(key)) {
   
            messageBuilder.setHeader(MessageConst.PROPERTY_KEYS, key);
        }
        //设置自定义属性
        if (map != null && !map.isEmpty()) {
   
            for (Map.Entry<String, Object> entry : map.entrySet()) {
   
                messageBuilder.setHeader(entry.getKey(), entry.getValue());
            }
        }
        Message<?> message = messageBuilder.build();
        SendResult sendResult = this.rocketMqTemplate.syncSend(topicName, message);
        if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
   
            log.info("发送同步消息-SQL92模式成功,topicName={},map={},msg={},sendResult={}", topicName, map, msg, sendResult);
        } else {
   
            log.warn("发送同步消息-SQL92模式不一定成功,topicName={},map={},msg={},sendResult={}", topicName, map, msg, sendResult);
        }
        return sendResult;
    }

    @Override
    public void sendAsyncMessage(String destination, Object msg, SendCallback sendCallback) {
   
        this.rocketMqTemplate.asyncSend(destination, msg, sendCallback);
        log.info("MQ发送异步消息,destination={} msg={}", destination, msg);
    }

    @Override
    public void sendOneway(String destination, Object msg) {
   
        this.rocketMqTemplate.sendOneWay(destination, msg);
        log.info("MQ发送单向消息,destination={} msg={}", destination, msg);
    }

    @Override
    public void sendBatchMessage(String destination, List<?> list) {
   
        String topicName = destination;
        String tags = "";

        String[] split = destination.split(":");
        if (split.length == 2) {
   
            topicName = split[0];
            tags = split[1];
        }
        this.sendBatchMessage(topicName, tags, 30000L, list);
    }

    @Override
    public void sendBatchMessage(String topicName, String tags, Long timeout, List<?> list) {
   
        //转为message
        List<org.apache.rocketmq.common.message.Message> messages = list.stream().map(x ->
                new org.apache.rocketmq.common.message.Message(topicName, tags,
                        //String类型不需要转JSON,其它类型都要转为JSON模式
                        x instanceof String ? ((String) x).getBytes(StandardCharsets.UTF_8) : JSON.toJSONBytes(x))
        ).collect(Collectors.toList());

        //自动分割发送,把大的消息分裂成若干个小的消息(每次发送最大只能4MB)
        ListSplitter splitter = new ListSplitter(messages);

        while (splitter.hasNext()) {
   
            try {
   
                List<org.apache.rocketmq.common.message.Message> listItem = splitter.next();
                SendResult sendResult = producer.send(listItem, timeout == null ? 30000L : timeout);
                if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
   
                    log.info("MQ发送批量消息成功,topicName={}  tags={}, size={},sendResult={}", topicName, tags, listItem.size(), sendResult);
                } else {
   
                    log.warn("MQ发送批量消息不一定成功,topicName={}  tags={}, size={},sendResult={}", topicName, tags, listItem.size(), sendResult);
                }
            } catch (Exception e) {
   
                //处理error
                log.error("MQ发送批量消息失败,topicName={}  tags={},,errorMessage={}", topicName, tags, e.getMessage(), e);
                throw new RuntimeException("MQ发送批量消息失败,原因:" + e.getMessage());
            }
        }
    }

    @Override
    public SendResult sendDelayLevel(String destination, Object msg, int delayTimeLevel) {
   
        return this.sendDelayLevel(destination, msg, 30000, delayTimeLevel);
    }

    @Override
    public SendResult sendDelayLevel(String destination, Object msg, int timeout, int delayTimeLevel) {
   
        Message<?> message = MessageBuilder
                .withPayload(msg)
                .build();
        SendResult sendResult = this.rocketMqTemplate.syncSend(destination, message, timeout, delayTimeLevel);
        if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
   
            log.info("MQ发送延时消息成功,destination={} msg={} sendResult={}", destination, message, sendResult);
        } else {
   
            log.warn("MQ发送延时消息不一定成功,destination={} msg={} sendResult={}", destination, message, sendResult);
        }
        return sendResult;
    }

    @Override
    public SendResult sendInOrder(String destination, Object msg, String hashKey) {
   
        Message<?> message = MessageBuilder
                .withPayload(msg)
                .build();
        //hashKey:  根据其哈希值取模后确定发送到哪一个队列
        SendResult sendResult = this.rocketMqTemplate.syncSendOrderly(destination, message, hashKey);
        if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
   
            log.info("MQ发送顺序消息成功,destination={} msg={} sendResult={}", destination, message, sendResult);
        } else {
   
            log.warn("MQ发送顺序消息不一定成功,destination={} msg={} sendResult={}", destination, message, sendResult);
        }
        return sendResult;
    }

    @Override
    public SendResult sendMessageInTransaction(String destination, Object msg, Object arg) {
   
        Message<?> message = MessageBuilder
                //转为JSON格式
                .withPayload(msg instanceof String ? msg : JSON.toJSONString(msg))
                .build();

        TransactionSendResult sendResult = rocketMqTemplate.sendMessageInTransaction(destination, message, arg);

        if (SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
   
            log.info("MQ发送事务消息成功,destination={} msg={} sendResult={}", destination, message, sendResult);
        } else {
   
            log.warn("MQ发送事务消息不一定成功,destination={} msg={} sendResult={}", destination, message, sendResult);
        }
        return sendResult;
    }
}
/**
 * <p>
 * 消息列表分割,复杂度只有当你发送大批量时才会增长,你可能不确定它是否超过了大小限制(4MB)。这时候你最好把你的消息列表分割一下:
 * </p>
 *
 **/
public class ListSplitter implements Iterator<List<Message>> {
   
    /**
     * 最大4MB,这里每次只发送1MB。(为了避免ListSplitter.calcMessageSize计算不精确及大批量数据发送超时才设置1MB)
     */
    private final int SIZE_LIMIT = 1024 * 1024 * 1;
    private final List<Message> messages;
    private int currIndex;

    public ListSplitter(List<Message> messages) {
   
        this.messages = messages;
    }

    @Override
    public boolean hasNext() {
   
        return currIndex < messages.size();
    }

    @Override
    public List<Message> next() {
   
        int startIndex = getStartIndex();
        int nextIndex = startIndex;
        int totalSize = 0;
        for (; nextIndex < messages.size(); nextIndex++) {
   
            Message message = messages.get(nextIndex);
            int tmpSize = calcMessageSize(message);
            if (tmpSize + totalSize > SIZE_LIMIT) {
   
                break;
            } else {
   
                totalSize += tmpSize;
            }
        }
        List<Message> subList = messages.subList(startIndex, nextIndex);
        currIndex = nextIndex;
        return subList;
    }

    private int getStartIndex() {
   
        Message currMessage = messages.get(currIndex);
        int tmpSize = calcMessageSize(currMessage);
        while (tmpSize >
  • 24
    点赞
  • 91
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值