从源码角度分析RocketMQ同步刷盘与异步刷盘的异同

同步刷盘:当生产者发送消息到broker端的时候,需要等待broker端把消息进行落盘之后,才会返回响应结果给生产者

异步刷盘:当生产者发送消息到broker端的时候,此时无需等待broker端对消息进行落盘的过程,直接就可以返回响应结果给生产者

按照正常逻辑去理解,我们都会以为同步刷盘和异步刷盘的实现不同之处肯定就在于同步刷盘是生产者线程同步执行刷盘逻辑完成的,异步刷盘是由一个子线程去完成刷盘的操作的,那么事实真的是如此吗?下面我们通过源码的角度去探讨下两者之间的异同

同步刷盘

final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
if (messageExt.isWaitStoreMsgOK()) {
    // 构造一个同步刷盘请求对象
    GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
    // 唤醒同步刷盘线程
    service.putRequest(request);
    CompletableFuture<PutMessageStatus> flushOkFuture = request.future();
    PutMessageStatus flushStatus = null;
    try {
        // 生产者发送消息的主线程会在这里阻塞,直到同步刷盘的异步线程调用GroupCommitRequest.wakeupCustomer()方法
        flushStatus = flushOkFuture.get(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout(),
                TimeUnit.MILLISECONDS);
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
        //flushOK=false;
    }
    if (flushStatus != PutMessageStatus.PUT_OK) {
        log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags()
            + " client address: " + messageExt.getBornHostString());
        putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
    }
} else {
    service.wakeup();
}

可以看到首先构造出一个同步刷盘请求对象,然后把这个对象叫给了一个叫GroupCommitService的类,接着下面就没有了刷盘相关的逻辑了,这不是同步刷盘吗,这里不应该是同步执行刷盘的逻辑吗?其实RocketMQ的同步刷盘机制中的“同步”并不是指生产者发送过来的线程同步执行刷盘操作,而是交由一个子线程去执行刷盘,生产者线程与刷盘子线程之间通过CompletableFuture进行同步,当刷盘子线程在刷盘的时候,生产者线程通过CompletableFuture.get()去阻塞自己,直到刷盘子线程完成刷盘才接触阻塞,以此来达到同步的效果。具体代码如下:

public static class GroupCommitRequest {

    /**
     * 下一次消息的开始写入位点
     */
    private final long nextOffset;

    /**
     * 通过该feature对象可以阻塞生产者线程达到同步写入消息的效果
     */
    private CompletableFuture<PutMessageStatus> flushOKFuture = new CompletableFuture<>();

    private final long startTimestamp = System.currentTimeMillis();

    private long timeoutMillis = Long.MAX_VALUE;

    public GroupCommitRequest(long nextOffset, long timeoutMillis) {
        this.nextOffset = nextOffset;
        this.timeoutMillis = timeoutMillis;
    }

    public GroupCommitRequest(long nextOffset) {
        this.nextOffset = nextOffset;
    }


    public long getNextOffset() {
        return nextOffset;
    }

    public void wakeupCustomer(final PutMessageStatus putMessageStatus) {
        this.flushOKFuture.complete(putMessageStatus);
    }

    public CompletableFuture<PutMessageStatus> future() {
        return flushOKFuture;
    }

}
/**
 * commitlog同步刷盘的异步线程,该线程不断地去轮询刷盘请求,只要有一个commitlog刷盘请求就会立刻进行刷盘
 */
class GroupCommitService extends FlushCommitLogService {

    /**
     * 外部生产者线程会把GroupCommitRequest对象放到requestsWrite集合中
     */
    private volatile List<GroupCommitRequest> requestsWrite = new ArrayList<GroupCommitRequest>();

    /**
     * 异步线程每一次执行刷盘之前都会把requestsWrite与requestsWrite进行交换
     */
    private volatile List<GroupCommitRequest> requestsRead = new ArrayList<GroupCommitRequest>();

    /**
     * 放入commitlog的刷盘请求,并且唤醒刷盘线程
     * @param request
     */
    public synchronized void putRequest(final GroupCommitRequest request) {
        synchronized (this.requestsWrite) {
            this.requestsWrite.add(request);
        }
        // 调用该方法之后,刷盘线程能够立刻执行doCommit()方法进行刷盘
        this.wakeup();
    }

    /**
     * 每一次执行doCommit()方法之前,都会把requestsWrite和requestsRead的元素进行交换
     * 在doCommit()方法中是通过遍历requestsRead中的GroupCommitRequest进行刷盘的
     */
    private void swapRequests() {
        List<GroupCommitRequest> tmp = this.requestsWrite;
        this.requestsWrite = this.requestsRead;
        this.requestsRead = tmp;
    }

    private void doCommit() {
        //加锁
        synchronized (this.requestsRead) {
            if (!this.requestsRead.isEmpty()) {
                for (GroupCommitRequest req : this.requestsRead) {
                    boolean flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
                    // 这里因为需要刷盘的消息可能跨文件了,满了又生成了一个,所以这里要刷两次
                    for (int i = 0; i < 2 && !flushOK; i++) {
                        // 强制对commitlog进行刷盘,只要有脏数据就刷盘
                        CommitLog.this.mappedFileQueue.flush(0);
                        // 刷盘指针 >= 写入指针,代表刷盘成功
                        flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
                    }
                    // 唤醒发送消息客户端
                    req.wakeupCustomer(flushOK ? PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_DISK_TIMEOUT);
                }
                // 获取最后一条commitlog的写入时间,保存在checkpoint文件
                long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
                if (storeTimestamp > 0) {
                    CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
                }
                // 清除本次所有的刷盘请求
                this.requestsRead.clear();
            } else {
                // Because of individual messages is set to not sync flush, it
                // will come to this process
                CommitLog.this.mappedFileQueue.flush(0);
            }
        }
    }

    public void run() {
        CommitLog.log.info(this.getServiceName() + " service started");

        while (!this.isStopped()) {
            try {
                // 该线程每次循环都会阻塞10毫秒(防止cup被占满),当外部调用了wakeup()方法的时候下一次循环将不会阻塞
                this.waitForRunning(10);
                // 刷盘
                this.doCommit();
            } catch (Exception e) {
                CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
            }
        }

        // Under normal circumstances shutdown, wait for the arrival of the
        // request, and then flush
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            CommitLog.log.warn("GroupCommitService Exception, ", e);
        }

        synchronized (this) {
            this.swapRequests();
        }

        this.doCommit();

        CommitLog.log.info(this.getServiceName() + " service end");
    }

    @Override
    protected void onWaitEnd() {
        this.swapRequests();
    }

    @Override
    public String getServiceName() {
        return GroupCommitService.class.getSimpleName();
    }

    @Override
    public long getJointime() {
        return 1000 * 60 * 5;
    }
}

刷盘子线程循环地执行doCommit()方法,只要当mappedFile存在脏页的情况下在doCommit()方法中就会被落盘,而在落盘的过程中,RocketMQ是如何保证生产者线程与落盘的异步线程同步的呢?

try {
    // 生产者发送消息的主线程会在这里阻塞,直到同步刷盘的异步线程调用GroupCommitRequest.wakeupCustomer()方法
    flushStatus = flushOkFuture.get(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout(),
            TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
    //flushOK=false;
}

构造的GroupCommitRequest对象中都有一个CompletableFuture对象,在生产者线程中会通过CompletableFuture.get()方法进行线程阻塞,那么什么时候会接触阻塞呢?

for (GroupCommitRequest req : this.requestsRead) {
    boolean flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
    // 这里因为需要刷盘的消息可能跨文件了,满了又生成了一个,所以这里要刷两次
    for (int i = 0; i < 2 && !flushOK; i++) {
        // 强制对commitlog进行刷盘,只要有脏数据就刷盘
        CommitLog.this.mappedFileQueue.flush(0);
        // 刷盘指针 >= 写入指针,代表刷盘成功
        flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
    }
    // 唤醒发送消息客户端
    req.wakeupCustomer(flushOK ? PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_DISK_TIMEOUT);
}
public void wakeupCustomer(final PutMessageStatus putMessageStatus) {
    this.flushOKFuture.complete(putMessageStatus);
}

在子线程完成刷盘之后,就会调用GroupCommitRequest#wakeupCustomer方法,在这个方法中就会把刷盘结果放到CompletableFuture中,此时生产者线程也就会解除阻塞并且拿到子线程的刷盘结果了

异步刷盘

if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
    flushCommitLogService.wakeup();
} else {
    commitLogService.wakeup();
}

相对同步刷盘来说,异步刷盘的在生产者线程这里的处理就简单多了,异步刷盘不用多说,肯定是通过子线程去异步刷盘的,而这个子线程就是FlushRealTimeService

/**
 * commitlog异步刷盘线程,该线程通过不断地判断需要刷盘的commitlog脏页数量和判断强制刷盘的时间间隔去进行刷盘,所以说当我们写入了commitlog是并不会立刻刷盘的
 */
class FlushRealTimeService extends FlushCommitLogService {

    /**
     * 上一次强制刷盘的时间
     */
    private long lastFlushTimestamp = 0;

    private long printTimes = 0;

    public void run() {
        CommitLog.log.info(this.getServiceName() + " service started");

        while (!this.isStopped()) {
            // 异步刷盘线程是否固定阻塞休眠一段时间,默认是false,表示能够通过wakeup()方法立刻唤醒线程
            boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed();
            // 异步刷盘线程的刷盘时间间隔
            int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();
            // 刷盘脏页阈值
            int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();
            // 强制刷盘的时间间隔
            int flushPhysicQueueThoroughInterval =
                CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();

            boolean printFlushProgress = false;

            // 如果当前时间 - 上一次强制刷盘时间 > 强制刷盘的时间间隔, 那么本次就需要强制刷盘
            long currentTimeMillis = System.currentTimeMillis();
            if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
                this.lastFlushTimestamp = currentTimeMillis;
                flushPhysicQueueLeastPages = 0;
                printFlushProgress = (printTimes++ % 10) == 0;
            }

            try {
                // flushCommitLogTimed == true,固定线程会休眠一段时间
                if (flushCommitLogTimed) {
                    Thread.sleep(interval);
                }
                // flushCommitLogTimed == false, 线程可以通过wakeup()立刻唤醒线程
                else {
                    this.waitForRunning(interval);
                }

                if (printFlushProgress) {
                    this.printFlushProgress();
                }

                long begin = System.currentTimeMillis();
                // commitlog开始刷盘
                CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages);
                long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
                if (storeTimestamp > 0) {
                    CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
                }
                long past = System.currentTimeMillis() - begin;
                if (past > 500) {
                    log.info("Flush data to disk costs {} ms", past);
                }
            } catch (Throwable e) {
                CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
                this.printFlushProgress();
            }
        }

        // Normal shutdown, to ensure that all the flush before exit
        boolean result = false;
        for (int i = 0; i < RETRY_TIMES_OVER && !result; i++) {
            result = CommitLog.this.mappedFileQueue.flush(0);
            CommitLog.log.info(this.getServiceName() + " service shutdown, retry " + (i + 1) + " times " + (result ? "OK" : "Not OK"));
        }

        this.printFlushProgress();

        CommitLog.log.info(this.getServiceName() + " service end");
    }

    @Override
    public String getServiceName() {
        return FlushRealTimeService.class.getSimpleName();
    }

    private void printFlushProgress() {
        // CommitLog.log.info("how much disk fall behind memory, "
        // + CommitLog.this.mappedFileQueue.howMuchFallBehind());
    }

    @Override
    public long getJointime() {
        return 1000 * 60 * 5;
    }
}

在异步刷盘线程中,有几个配置值这里要说一下:

1.flushCommitLogTimed:该值表示异步刷盘线程是否固定阻塞休眠一段时间,默认是false表示能够通过wakeup()方法立刻唤醒线程

2.flushIntervalCommitLog:该值表示异步刷盘线程的刷盘时间间隔

3.flushCommitLogLeastPages:该值表示刷盘脏页阈值

4.flushPhysicQueueThoroughInterval:该值表示强制刷盘的时间间隔

异步刷盘线程通过while循环不断地去对脏页进行刷盘,与同步刷盘不同的是,异步刷盘有刷盘的脏页阈值限制,默认是脏页数达到4页的时候才会把脏页进行刷盘,那么如果脏页数量一直不到阈值怎么办呢?异步刷盘线程还有一个强制刷盘时间间隔的设置,如果刷盘的时间间隔超过了这个强制刷盘时间间隔,那么只要有脏页就会对其进行刷盘。并且异步刷盘线程可以设置是否固定休眠一段时间,如果固定休眠,就调用sleep方法使线程休眠,在休眠期间外部不能进行唤醒和中断,如果非固定休眠,那么就调用waitForRunning方法阻塞一段时间,而在阻塞的过程中,外部是可以通过wakeup方法对其进行唤醒的

总结

相同点:同步刷盘与异步刷盘的工作原理都是通过子线程去达成目的的,同步刷盘虽然说是同步,但是并不是一个线程同步去干一件事,而是把刷盘的操作交给子线程去处理,在子线程处理过程中生产线程会阻塞,子线程处理完之后再去通知生产线程以此来达到同步刷盘的效果

不同点:同步刷盘都是强制刷盘,异步刷盘有刷盘脏页阈值以及强制刷盘时间间隔等限制,并且异步刷盘线程无需与生产者线程之间产生任何关系,而同步刷盘线程需要通过GroupCommitGroup对象才能与生产线程打交道

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值