【RocketMQ】消息的刷盘机制

本文深入探讨RocketMQ的消息刷盘策略,包括同步刷盘和异步刷盘。同步刷盘确保消息立即写入磁盘,而异步刷盘允许消息先写入内存,存在数据丢失风险。文章详细解析了刷盘请求的处理过程,包括线程启动、刷盘请求的添加、刷盘线程的唤醒、刷盘操作以及刷盘超时监控,同时分析了同步和异步刷盘的区别与实现原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

刷盘策略

CommitLog 的 asyncPutMessage 方法中可以看到在写入消息之后,调用了 submitFlushRequest 方法执行刷盘策略:

public class CommitLog {
    public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
        // ...
        try {
            // 获取上一次写入的文件
            MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
            // ...
            // 写入消息
            result = mappedFile.appendMessage(msg, this.appendMessageCallback, putMessageContext);
            // ...
        } finally {
            beginTimeInLock = 0;
            putMessageLock.unlock();
        }
        // ...
        // 执行刷盘
        CompletableFuture<PutMessageStatus> flushResultFuture = submitFlushRequest(result, msg);
        // ...
    }
}

刷盘有两种策略:

  • 同步刷盘,表示消息写入到内存之后需要立刻刷到磁盘文件中。

    同步刷盘会构建 GroupCommitRequest 组提交请求并设置本次刷盘后的位置偏移量的值(写入位置偏移量+写入数据字节数),然后将请求添加到 flushDiskWatcher 和 GroupCommitService 中进行刷盘。

  • 异步刷盘,表示消息写入内存成功之后就返回,由MQ定时将数据刷入到磁盘中,会有一定的数据丢失风险。

public class CommitLog {
    // 监控刷盘
    private final FlushDiskWatcher flushDiskWatcher;
    public CompletableFuture<PutMessageStatus> submitFlushRequest(AppendMessageResult result, MessageExt messageExt) {
        // 是否是同步刷盘
        if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
            // 获取GroupCommitService
            final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
            // 是否等待
            if (messageExt.isWaitStoreMsgOK()) {
                // 构建组提交请求,传入本次刷盘后位置的偏移量:写入位置偏移量+写入数据字节数
                GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes(),
                        this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
                // 添加到wather中
                flushDiskWatcher.add(request);
                // 添加到service
                service.putRequest(request);
                // 返回
                return request.future();
            } else {
                service.wakeup();
                return CompletableFuture.completedFuture(PutMessageStatus.PUT_OK);
            }
        }
        // 如果是异步刷盘
        else {
            if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
                flushCommitLogService.wakeup();
            } else  {
                commitLogService.wakeup();
            }
            return CompletableFuture.completedFuture(PutMessageStatus.PUT_OK);
        }
    }
}

同步刷盘

如果使用的是同步刷盘,首先获取了 GroupCommitService ,然后构建 GroupCommitRequest 组提交请求,将请求添加到 flushDiskWatcher 和 GroupCommitService 中,其中flushDiskWatcher用于监控刷盘是否超时,GroupCommitService用于提交刷盘数据。

构建GroupCommitRequest提交请求

GroupCommitRequest 是 CommitLog 的内部类:

  • nextOffset :写入位置偏移量+写入数据字节数,也就是本次刷盘成功后应该对应的flush偏移量
  • flushOKFuture :刷盘结果
  • deadLine :刷盘的限定时间,值为当前时间 + 传入的超时时间,超过限定时间还未刷盘完毕会被认为超时
public class CommitLog {
    public static class GroupCommitRequest {
        private final long nextOffset;
        // 刷盘状态
        private CompletableFuture<PutMessageStatus> flushOKFuture = new CompletableFuture<>();
        private final long deadLine;// 刷盘的限定时间,超过限定时间还未刷盘完毕会被认为超时

        public GroupCommitRequest(long nextOffset, long timeoutMillis) {
            this.nextOffset = nextOffset;
            // 设置限定时间:当前时间 + 超时时间
            this.deadLine = System.nanoTime() + (timeoutMillis * 1_000_000);
        }

        public void wakeupCustomer(final PutMessageStatus putMessageStatus) {
            // 结束刷盘,设置刷盘状态
            this.flushOKFuture.complete(putMessageStatus);
        }

        public CompletableFuture<PutMessageStatus> future() {
            // 返回刷盘状态
            return flushOKFuture;
        }

    }
}

GroupCommitService处理刷盘

GroupCommitService 是 CommitLog 的内部类,从继承关系中可知它实现了Runnable接口,在run方法调用 waitForRunning 等待刷盘请求的提交,然后处理刷盘,不过这个线程是在什么时候启动的呢?

public class CommitLog {
    /**
     * GroupCommit Service
     */
    class GroupCommitService extends FlushCommitLogService {
        // ...
        // run方法
        public void run() {
            CommitLog.log.info(this.getServiceName() + " service started");
            while (!this.isStopped()) {
                try {
                    // 等待刷盘请求的到来
                    this.waitForRunning(10);
                    // 处理刷盘
                    this.doCommit();
                } catch (Exception e) {
                    CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
                }
            }
            // ...
        }
    }
}

刷盘线程的启动

在BrokerController的启动方法中,可以看到调用了 messageStore 的start方法,前面可知使用的是 DefaultMessageStore ,进入到 DefaultMessageStore 的start方法,它又调用了 commitLog 的start方法,在 CommitLog 的 start 方法中,启动了刷盘的线程和监控刷盘的线程:

public class BrokerController {
    public void start() throws Exception {
        if (this.messageStore != null) {
            // 启动
            this.messageStore.start();
        }
        // ...
    }
}

public class DefaultMessageStore implements MessageStore {
   /**
     * @throws Exception
     */
    public void start() throws Exception {
        // ...
        this.flushConsumeQueueService.start();
        // 调用CommitLog的启动方法
        this.commitLog.start();
        this.storeStatsService.start();
        // ...
    }
}

public class CommitLog {
    private final FlushCommitLogService flushCommitLogService; // 刷盘
    private final FlushDiskWatcher flushDiskWatcher; // 监控刷盘
    private final FlushCommitLogService commitLogService; // commitLogService
    public void start() {
        // 启动刷盘的线程
        this.flushCommitLogService.start();
        flushDiskWatcher.setDaemon(true);
        // 启动监控刷盘的线程
        flushDiskWatcher.start();
        if (defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
            this.commitLogService.start();
        }
    }
}

刷盘请求的处理

既然知道了线程在何时启动的,接下来详细看一下 GroupCommitService 是如何处理刷盘提交请求的。

前面知道在 GroupCommitService 的run方法中,调用了 waitForRunning 方法等待刷盘请求, waitForRunning 在 GroupCommitService 父类 ServiceThread 中实现。 ServiceThread 是一个抽象类,实现了Runnable接口, 里面使用了CountDownLatch进行线程间的通信 ,大小设为1。

waitForRunning 方法在进入的时候先判断 hasNotified 是否为true(已通知),并尝试将其更新为false(未通知),由于hasNotified的初始化值为false,所以首次进入的时候条件不成立,不会进入到这个处理逻辑,会继续执行后面的代码。

接着调用 waitPoint的reset方法将其重置为1,并调用waitPoint的await方法进行等待:

// ServiceThread
public abstract class ServiceThread implements Runnable {
    // 是否通知,初始化为false
    protected volatile AtomicBoolean hasNotified = new AtomicBoolean(false);
  
    // CountDownLatch用于线程间的通信
   
### RocketMQ 刷盘机制详解 #### 文件的重要性 为了确保消息的持久性和可靠性,RocketMQ采用了高效的文件刷盘机制来保存消息。这不仅能够防止数据丢失,还能提高系统的稳定性和性能。 #### 同步双写模式 同步双写意味着每条消息都会立即写入磁并等待确认返回。这种方式虽然最安全,但是会对吞吐量造成一定影响[^1]。 ```python def sync_flush(): # 将消息立刻写入磁并等待确认 message.flush_to_disk() ``` #### 异步批量 异步批量则是指消息先存放在内存缓冲区中,在达到设定条件(如数量或时间间隔)后再统一写入磁。这种方法可以在保证可靠性的前提下显著提升性能。 ```python import threading class AsyncFlush(threading.Thread): def __init__(self, interval=1000): # 默认每隔一秒执行一次 super().__init__() self.interval = interval def run(self): while True: time.sleep(self.interval / 1000.0) batch_messages.flush_to_disk() # 批量新至磁 ``` #### 半同步半异步策略 实际应用中,RocketMQ采用了一种介于两者之间的方案——即大部分情况下采取异步方式快速响应请求;而在特定场景下(比如系统重启前),则切换成同步模式以确保所有未处理的数据都能及时落地。 通过上述多种手段相结合的方式,RocketMQ实现了既保持较高效率又能有效避免因意外断电等原因造成的潜在风险的目标。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值