深入浅出 RocketMQ 与 Kafka 的持久化机制全解析

大家好!作为开发者,工作中无法避免会遇到消息中间件的使用,作为一名在消息中间件这个"泥坑"里摸爬滚打多年的开发者。

今天想和大家一起聊聊两大消息中间件巨头 RocketMQ 和 Kafka 在持久化方面的那些事儿。

持久化机制一直是高可用架构的命脉所在,值得每个开发者好好研究一番!

为什么持久化这么重要?

先问大家一个问题,假设你的消息系统突然崩溃了,数据会怎样?是灰飞烟灭还是死而复生?

没错,持久化就是解决这个问题的关键!它让我们的消息系统具备了"记忆"能力,即使遭遇重启、崩溃,消息数据依然安然无恙。

接下来,我们就一起揭开 RocketMQ 和 Kafka 持久化机制的神秘面纱,看看它们各自的绝技!

RocketMQ 的持久化机制

CommitLog + ConsumeQueue 的二级存储设计

RocketMQ 采用了一种非常聪明的二级存储结构,包括 CommitLog 和 ConsumeQueue 两部分。

在这里插入图片描述

这种存储结构很巧妙,让我们来拆解一下

  1. CommitLog 是一种磁盘上的顺序写日志,所有的主题(Topic)消息都写在这一个文件里,实现了"写入永远是顺序的"这一高性能准则。

  2. ConsumeQueue 则是一个逻辑消费队列,相当于 CommitLog 的索引文件,保存了指向 CommitLog 的物理偏移量,按照 Topic + MessageQueue 组织,方便消费者快速定位消息。

想象一下,CommitLog 就像一本流水账,记录所有的交易,而 ConsumeQueue 则像是一个分类目录,帮你快速找到特定类型的交易记录。

文件存储结构

RocketMQ 的文件保存在磁盘上是如何组织的呢?

/rocketmq_store
  /commitlog
    00000000000000000000  (1G文件大小)
    00000000001073741824  (文件名代表起始偏移量)
    00000000002147483648
    ...
  /consumequeue
    /topic_1
      /0   (队列ID0)
        00000000000000000000  (6个字节/索引 × 30万索引)
        00000000000006000000
        ...
      /1   (队列ID1)
        00000000000000000000
        ...
    /topic_2
      /0
        ...
  /index
    /20230101  (索引文件按照日期命名)
    /20230102
    ...
  /config
    consumerOffset.json  (消费者组的消费位置)
    delayOffset.json     (延迟消费队列进度)
    topics.json          (主题配置信息)
    subscriptionGroup.json (订阅组配置)
    ...
CommitLog 的特点
  1. 分片存储
    每个文件大小固定为1GB,当一个文件写满后,会创建新文件继续写入。

  2. 文件命名
    文件名使用20位十进制数字,表示当前文件的起始偏移量,如 00000000000000000000 表示从0开始的文件。

  3. 消息格式
    每条消息包含消息长度、物理偏移量、消息体、主题、队列ID等信息。

看看一个典型的 RocketMQ 消息在 CommitLog 中的存储格式

在这里插入图片描述

ConsumeQueue 的结构

ConsumeQueue 文件包含若干条定长条目,每个条目固定20字节,由三部分组成

  1. CommitLog 物理偏移量(8字节)
    指向消息在 CommitLog 中的位置

  2. 消息长度(4字节)
    消息在 CommitLog 中占用的字节数

  3. 消息 Tag 的哈希码(8字节)
    用于消息过滤

看起来有点抽象?没关系,我们用代码来理解一下 RocketMQ 存储时的关键流程

// 简化版的消息存储过程,展示核心逻辑
public class RocketMQStorageSimulator {
    
    // 模拟CommitLog将消息追加到日志文件
    public PutMessageResult appendToCommitLog(Message msg) {
        // 1. 获取CommitLog文件,如果当前文件已满则创建新文件
        MappedFile mappedFile = getAvailableMappedFile();
        
        // 2. 计算消息长度并检查剩余空间
        int msgLen = calculateMessageLength(msg);
        if (mappedFile.getFileSize() - mappedFile.getWritePosition() < msgLen) {
            // 当前文件不足以容纳消息,需要创建新文件
            mappedFile = createNewMappedFile();
        }
        
        // 3. 构建消息存储格式
        ByteBuffer buffer = buildMessageBuffer(msg, msgLen);
        
        // 4. 追加写入CommitLog文件
        long offset = mappedFile.getFileFromOffset() + mappedFile.getWritePosition();
        mappedFile.appendMessage(buffer);
        
        // 5. 构建返回结果包含消息在CommitLog中的物理偏移量
        PutMessageResult result = new PutMessageResult(PutMessageStatus.OK, offset, msgLen);
        
        // 6. 返回结果,供后续更新ConsumeQueue索引用
        return result;
    }
    
    // 构建消息的存储格式
    private ByteBuffer buildMessageBuffer(Message msg, int msgLen) {
        ByteBuffer buffer = ByteBuffer.allocate(msgLen);
        
        // 写入消息总长度
        buffer.putInt(msgLen);
        
        // 写入物理偏移量(后面补充)
        buffer.putLong(0);
        
        // 写入魔数(用于文件校验)
        buffer.putInt(0xAABBCCDD);
        
        // 写入存储时间戳
        buffer.putLong(System.currentTimeMillis());
        
        // 写入消息ID
        buffer.putLong(generateMessageId());
        
        // 写入Topic长度和内容
        byte[] topicBytes = msg.getTopic().getBytes();
        buffer.put((byte) topicBytes.length);
        buffer.put(topicBytes);
        
        // 写入属性信息
        byte[] propBytes = serializeProperties(msg.getProperties());
        buffer.putInt(propBytes.length);
        buffer.put(propBytes);
        
        // 写入消息体
        buffer.put(msg.getBody());
        
        // 计算并写入CRC校验码
        int crc = calculateCRC(buffer.array());
        buffer.putInt(crc);
        
        buffer.flip();
        return buffer;
    }
    
    // 消息写入CommitLog后,更新ConsumeQueue
    public void updateConsumeQueue(Message msg, long commitLogOffset, int msgSize) {
        String topic = msg.getTopic();
        int queueId = msg.getQueueId();
        
        // 1. 找到对应的ConsumeQueue
        ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);
        
        // 2. 创建ConsumeQueue索引条目
        ByteBuffer indexBuffer = ByteBuffer.allocate(20);
        indexBuffer.putLong(commitLogOffset);     // CommitLog物理偏移量
        indexBuffer.putInt(msgSize);              // 消息大小
        indexBuffer.putLong(msg.getTagsHashCode()); // Tag的哈希码
        indexBuffer.flip();
        
        // 3. 将索引追加到ConsumeQueue文件
        consumeQueue.appendIndex(indexBuffer);
    }
    
    // 主存储流程,将消息写入CommitLog并更新ConsumeQueue
    public void putMessage(Message msg) {
        // 1. 写入CommitLog
        PutMessageResult result = appendToCommitLog(msg);
        
        // 2. 写入成功后,更新ConsumeQueue索引
        if (result.getStatus() == PutMessageStatus.OK) {
            updateConsumeQueue(msg, result.getOffset(), result.getSize());
        }
        
        // 3. 如果配置了消息索引服务,还会更新索引文件(略)
    }
    
    // 读取消息的流程
    public Message getMessage(String topic, int queueId, long offset) {
        // 1. 找到对应的ConsumeQueue
        ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);
        
        // 2. 从ConsumeQueue读取索引信息
        IndexItem indexItem = consumeQueue.getIndexItem(offset);
        
        // 3. 根据索引信息中的物理偏移量,从CommitLog读取消息
        long physicalOffset = indexItem.getPhysicalOffset();
        int size = indexItem.getSize();
        
        // 4. 从CommitLog读取消息并解析
        ByteBuffer messageBuf = readFromCommitLog(physicalOffset, size);
        Message message = decodeMessage(messageBuf);
        
        return message;
    }
    
    // 其他辅助方法略...
}

刷盘机制

RocketMQ 持久化最关键的环节就是刷盘,这决定了消息安全性与性能的平衡点。它提供了两种刷盘方式

  1. 同步刷盘
    Producer 发送消息后,必须等待消息完全持久化到磁盘才返回成功。安全性最高,但性能较低。

  2. 异步刷盘
    消息先写入 PageCache,立即返回成功响应,后台线程定时将数据刷入磁盘。性能高但有丢失风险。
    在这里插入图片描述

高可用机制

RocketMQ 通过主从复制来保证数据的高可用。每个 Broker 分为 Master 和 Slave,它们之间的数据复制有两种方式:

  1. 同步复制:Master 和 Slave 都写成功才反馈给客户端成功。

  2. 异步复制:Master 写成功后立即返回成功,Slave 异步从 Master 拉取数据。

复制和刷盘可以组合使用,根据业务需求选择不同策略:

  • 同步复制 + 同步刷盘:最高可靠,性能最低
  • 同步复制 + 异步刷盘:较高可靠,性能中等
  • 异步复制 + 异步刷盘:可靠性一般,性能最高

Kafka 的持久化机制

切换话题,让我们来看看 Kafka 的持久化是如何实现的。

基于日志的分区存储

Kafka 的核心存储单元是日志(Log),它将消息组织成一个个分区(Partition),每个分区是一个有序的、不可变的消息序列。

文件存储结构

Kafka 的文件存储有其独特特点,我们来看看它的文件是如何组织的:

/kafka-logs
  /my-topic-0  (主题名称-分区ID)
    /00000000000000000000.log     (数据文件)
    /00000000000000000000.index   (偏移量索引文件)
    /00000000000000000000.timeindex  (时间索引文件)
    /00000000000000367001.log
    /00000000000000367001.index
    /00000000000000367001.timeindex
    /00000000000000734002.log
    /00000000000000734002.index
    /00000000000000734002.timeindex
    ...
  /my-topic-1
    /00000000000000000000.log
    /00000000000000000000.index
    /00000000000000000000.timeindex
    ...
  /my-topic-2
    ...
  /__consumer_offsets-0  (内部主题,存储消费者位移)
    /00000000000000000000.log
    /00000000000000000000.index
    ...
  /__consumer_offsets-1
    ...

Kafka 的存储结构有以下特点:

  1. 分区目录:每个分区有单独的目录,命名为"主题名-分区号"。

  2. 日志分段:每个分区由多个日志分段(LogSegment)组成,每个日志分段包含以下文件:

    • *.log:实际存储消息的数据文件
    • *.index:偏移量索引文件,用于根据偏移量快速定位消息
    • *.timeindex:时间戳索引文件,用于根据时间戳快速定位消息
  3. 文件命名:文件名是该段中第一条消息的偏移量,如 00000000000000000000.log 表示从偏移量 0 开始的日志段。

消息格式与索引

Kafka 的消息格式相对简单,主要包含消息内容和一些元数据。

在这里插入图片描述

索引文件的结构简洁高效,包含两种类型:

  1. 偏移量索引(.index)
    记录消息偏移量到物理文件位置的映射,每个索引项占用8字节(4字节相对偏移量 + 4字节物理位置)。

  2. 时间戳索引(.timeindex)
    记录时间戳到偏移量的映射,每个索引项占用12字节(8字节时间戳 + 4字节相对偏移量)。

页缓存与零拷贝

Kafka 的高性能秘诀之一是充分利用操作系统的页缓存(PageCache)和零拷贝技术。
让我继续完成关于 Kafka 持久化机制的内容:

在这里插入图片描述

页缓存的优势

Kafka 巧妙地利用了操作系统的页缓存,而不是像许多数据库那样管理自己的应用缓存。这有几个显著优势:

  1. 避免了双重缓冲:不需要在JVM堆内存中维护一份数据的副本,节省内存。

  2. 自动内存管理:操作系统会自动管理页缓存,在内存压力大时回收。

  3. 预读与后写:操作系统会自动进行预读和批量写入,提高性能。

  4. 空闲内存利用:即使Kafka进程只分配了较少的JVM堆内存,也能利用系统全部可用内存。

零拷贝技术

在消费消息时,Kafka使用了sendfile系统调用,实现了零拷贝,避免了不必要的数据复制操作,如下代码所示:

// 简化版的Kafka零拷贝实现代码
public class KafkaNetworkSend {
    // FileChannel的transferTo方法底层会使用操作系统的零拷贝功能(如sendfile)
    public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
        // 核心调用,直接从文件通道传输到网络通道,避免用户空间的数据拷贝
        return fileChannel.transferTo(position, count, this.socketChannel);
    }
    
    // Kafka中NetworkSend简化实现
    public boolean writeTo(GatheringByteChannel channel) throws IOException {
        long bytesTransferred = transferFrom(fileChannel, position, count);
        
        // 更新位置信息
        position += bytesTransferred;
        remaining -= bytesTransferred;
        
        // 判断是否传输完成
        return remaining <= 0;
    }
}

// 消费者获取消息时的数据传输过程
public class KafkaDataFetchSimulator {
    public void fetchMessages(String topic, int partition, long offset, int maxBytes) {
        try {
            // 1. 查找到对应的日志分段
            LogSegment segment = findSegmentForOffset(topic, partition, offset);
            
            // 2. 通过索引文件找到消息在数据文件中的位置
            long physicalPosition = segment.translateOffset(offset);
            
            // 3. 打开文件通道,准备读取数据
            FileChannel fileChannel = segment.fileChannel();
            
            // 4. 设置读取的起始位置
            fileChannel.position(physicalPosition);
            
            // 5. 使用零拷贝技术直接传输数据到Socket通道
            // 这一步避免了将数据先读入应用程序内存再写出
            long bytesTransferred = fileChannel.transferTo(
                physicalPosition,
                Math.min(maxBytes, segment.size() - physicalPosition),
                socketChannel
            );
            
            System.out.println("Zero-copy transferred " + bytesTransferred + " bytes");
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    // 其他辅助方法略...
}

持久化策略

Kafka 的持久化策略主要由以下几个参数控制:

  1. log.flush.interval.messages:积累多少条消息后将数据刷新到磁盘。

  2. log.flush.interval.ms:多长时间后将数据刷新到磁盘。

  3. log.retention.hours/minutes/ms:数据保留的时间,默认为7天。

  4. log.retention.bytes:分区数据文件的最大大小,超过则删除旧数据。

  5. log.segment.bytes:单个日志分段的最大大小,默认为1GB。

Kafka同样提供了不同级别的持久化保证,可以通过acks参数进行控制:

  • acks=0:生产者发送消息后不等待确认,最快但可能丢数据
  • acks=1:生产者等待leader副本确认,较快且较可靠
  • acks=all:生产者等待所有同步副本确认,最慢但最可靠

副本同步机制

Kafka通过分区副本(Replica)机制实现高可用。每个分区有一个Leader副本和多个Follower副本。

在这里插入图片描述

Kafka的副本同步机制有几个关键概念:

  1. LEO (Log End Offset):每个副本最后一条消息的偏移量,即副本当前的写入位置。

  2. HW (High Watermark):所有同步副本都已经复制完的位置,消费者只能消费到HW位置的数据。

  3. ISR (In-Sync Replicas):与Leader保持同步的副本集合,包括Leader自己。

副本同步的过程是:

  1. Producer将消息发送给Leader副本
  2. Leader将消息写入本地日志
  3. Follower向Leader拉取消息(采用拉模式而非推模式)
  4. Leader根据各Follower的同步进度更新HW
  5. 只有写入ISR中所有副本的消息才对消费者可见

RocketMQ 与 Kafka 持久化机制对比

现在我们已经了解了两个消息系统的持久化机制,让我们做个全面对比:

对比维度RocketMQKafka
存储结构一个CommitLog顺序存储所有消息,ConsumeQueue作为索引按Topic分区组织,每个分区有独立的日志段(LogSegment)
文件组织CommitLog固定1GB,ConsumeQueue固定存储格式LogSegment默认1GB,文件名是起始偏移量
索引方式ConsumeQueue包含消息在CommitLog中的偏移量、大小、Tag哈希码偏移量索引(.index)和时间戳索引(.timeindex)
刷盘机制支持同步刷盘和异步刷盘主要依赖操作系统页缓存,异步刷盘
消息查询支持按时间、Key、消息ID、Tag等多维度查询主要通过偏移量和时间戳查询
高可用主从结构,支持同步复制和异步复制分区副本机制,基于ISR模型
性能优化顺序写提升写入性能,预分配MapedFile零拷贝技术、页缓存、批量传输
消息保留默认按时间保留(3天),支持按大小保留支持按时间和大小保留策略
磁盘使用同一消息存两份(CommitLog和ConsumeQueue)每个分区只存一份数据
消息过滤支持在服务端按Tag过滤主要在客户端过滤
延时消息原生支持延时消息需要额外的时间轮或专门的延时主题
事务消息原生支持分布式事务消息支持但实现复杂度高
适用场景金融支付、交易系统等对一致性要求高的场景日志收集、流处理等大数据量、高吞吐场景

架构设计理念对比

两种消息系统在持久化设计上的理念有很大不同:

RocketMQ 的设计思路

RocketMQ 采用了"写一次,读多次"的设计模式:

  1. 统一存储:所有消息都写在一个CommitLog中,避免了随机写入,最大化顺序写性能。

  2. 逻辑分离:通过ConsumeQueue索引实现按主题、队列的逻辑分离,加速查找。

  3. 均衡权衡:牺牲一部分存储空间(同样的消息存两份),换取更好的写入性能和更灵活的功能。

这种设计让RocketMQ在支持各种复杂业务场景时更加灵活,比如延时消息、事务消息等。

Kafka 的设计思路

Kafka则追求的是高吞吐、简单高效:

  1. 分区物理隔离:每个分区都有独立的文件,物理上完全分离。

  2. 页缓存依赖:充分信任和利用操作系统的页缓存,简化设计。

  3. 零拷贝优化:通过sendfile系统调用实现零拷贝,减少CPU消耗。

Kafka的设计简洁明了,特别适合大数据时代的日志收集和流处理场景。

适用场景分析

根据持久化机制的差异,两者在不同场景下各有优势:

RocketMQ 适合的场景
  1. 金融交易:可以配置同步刷盘+同步双写,确保数据不丢失。

  2. 复杂业务逻辑:延时消息、事务消息都有很好的支持。

  3. 灵活查询需求:支持多维度消息查询,方便问题排查。

  4. 丰富的过滤:服务端支持Tag过滤,减少无效消息传输。

  5. 海量主题:由于所有消息集中存储,支持数万级主题数量。

Kafka 适合的场景
  1. 日志收集:高吞吐、低延迟、支持大量分区。

  2. 流数据处理:与Spark、Flink等流处理框架无缝集成。

  3. 数据管道:作为数据流转的高速通道。

  4. 高性能要求:零拷贝技术在大数据量传输时优势明显。

  5. 长时间数据保存:可以配置较长的数据保留时间,如数月甚至数年。

实战应用建议

选择哪个消息系统,并不是简单的"二选一"问题。在实际工作中,可以根据具体业务需求进行更细致的配置或混合使用:

  1. 针对性配置

    • RocketMQ:根据重要性调整刷盘策略,重要业务用同步刷盘,一般业务用异步刷盘。
    • Kafka:调整副本数量和acks参数,平衡性能和可靠性。
  2. 分场景使用

    • 对于交易、支付等核心业务,使用RocketMQ保证可靠性。
    • 对于日志、监控等高吞吐场景,使用Kafka提升性能。
  3. 混合架构

    • 两种消息系统并行使用,通过桥接工具实现数据流转。
      让我继续完成 RocketMQ 和 Kafka 持久化机制的对比分析:

性能测试与调优建议

理论上的分析需要结合实际性能测试才能更好地指导实践。下面分享一些性能测试数据和调优建议:
在这里插入图片描述

RocketMQ 调优要点

  1. 刷盘策略选择

    在实际应用中,通常我们会根据业务重要性来选择不同的刷盘策略。对于核心交易业务,可能会选择同步刷盘,而对于一般的消息通知场景,异步刷盘就足够了。

  2. 文件存储空间管理

    RocketMQ 的 CommitLog 和 ConsumeQueue 文件会随着消息的积累而增长,需要合理设置保留策略。可以通过以下参数进行调优:

    # 文件保留时间,默认72小时
    fileReservedTime=72
    
    # 删除文件间隔时间,默认100ms
    deleteWhen=04
    
    # 单个CommitLog文件大小,默认1G
    mapedFileSizeCommitLog=1073741824
    
    # 单个ConsumeQueue文件大小,默认30万条索引
    mapedFileSizeConsumeQueue=300000
    
  3. 内存管理

    适当增加系统内存,尤其是直接内存的大小可以提升性能:

    # 设置JVM直接内存大小
    -XX:MaxDirectMemorySize=8g
    
  4. 批量处理

    启用消息批量处理可以极大提升吞吐量:

    // 生产者批量发送
    List<Message> messages = new ArrayList<>();
    // 添加消息...
    producer.send(messages);
    

Kafka 调优要点

  1. 页缓存利用

    Kafka 严重依赖操作系统的页缓存,因此应该尽可能给系统留出足够的空闲内存:

    # Kafka进程本身的堆内存可以适当控制
    KAFKA_HEAP_OPTS="-Xmx4G -Xms4G"
    
  2. 批量设置

    Kafka 在批量处理方面表现优异,可以通过以下参数提升性能:

    # 生产者批次大小,默认16KB
    batch.size=131072
    
    # 批量延迟时间,默认0ms
    linger.ms=10
    
    # 消费者批量拉取大小
    fetch.min.bytes=1024
    fetch.max.bytes=52428800
    
  3. 分区设计

    合理设置分区数量,可以提高并行处理能力:

    # 分区数 = min(磁盘吞吐量 / 单分区吞吐量, 消费者线程数)
    
  4. 日志清理配置

    配置合理的日志保留策略,避免磁盘空间不足:

    # 数据保留时间
    log.retention.hours=168
    
    # 基于大小的保留策略
    log.retention.bytes=1073741824
    
    # 段文件最大大小
    log.segment.bytes=1073741824
    

两种设计的深层思考

为什么设计差异这么大?

RocketMQ 和 Kafka 之所以在持久化设计上有如此大的差异,主要源于它们的设计初衷和诞生背景:

  1. Kafka 诞生于 LinkedIn,最初是为了解决海量日志处理问题,因此其设计理念是"简单高效、高吞吐优先"。页缓存 + 零拷贝的设计正是这一理念的体现,简单明了但功能相对单一。

  2. RocketMQ 则源自阿里巴巴的电商业务,需要应对复杂的业务场景和极端的可靠性要求。CommitLog + ConsumeQueue 的存储设计虽然复杂一些,但能更好地支持多种高级特性。

工程设计中的权衡思想

从这两种消息系统的设计中,我们可以学到一些通用的工程设计思想:

  1. 没有完美的方案,只有最适合的选择
    无论是 RocketMQ 还是 Kafka,都不是完美无缺的,它们只是在各自的场景中做出了最合理的权衡。

  2. 简单 vs 灵活的永恒权衡
    Kafka 选择了简单性,这让它在扩展性和维护性上具有优势;而 RocketMQ 选择了灵活性,能支持更多复杂的业务场景。

  3. 利用已有 vs 重新设计
    Kafka 更多地利用操作系统已有的能力(如页缓存),而 RocketMQ 则选择了更多自主设计的组件,各有优劣。

未来演进趋势

消息系统的持久化机制还在不断演进:

  1. RocketMQ 的优化方向

    • 引入类似 Kafka 的零拷贝技术减少内存消耗
    • 优化 CommitLog 的存储效率,减少冗余
    • 增强分布式事务功能
  2. Kafka 的增强方向

    • KRaft 模式取代 ZooKeeper,简化架构
    • 增强可靠性保证,缩小与 RocketMQ 在金融场景的差距
    • 增加更多可观测性和管理功能

实战总结与建议

作为一名经历过无数消息系统血泪史的架构师,我想给大家一些切实可行的建议:

  1. 混合使用是明智选择
    别被"二选一"的思维限制,在企业架构中,RocketMQ 和 Kafka 完全可以共存,各自承担最适合的业务场景。

  2. 持久化配置因地制宜
    不要盲目追求极致的持久化方案,应该根据业务的实际需求和可靠性要求来配置持久化策略。

  3. 性能测试必不可少
    在实际部署前,一定要在真实环境下做全面的性能测试,特别是要测试极限场景下的表现。

在这里插入图片描述

结语

这篇文章,我们深入剖析了 RocketMQ 和 Kafka 在持久化机制上的异同。它们各自的设计蕴含着深刻的工程智慧,针对不同的场景做出了不同的权衡。

在实际应用中,不要陷入"哪个更好"的争论,而是应该思考"哪个更适合我的业务场景"。

有时候,最佳方案可能是两者结合,取长补短。

如果你有关于消息系统持久化的问题或经验,欢迎在评论区留言交流!我们一起探讨,一起成长。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

慢德

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值