Java最全RocketMQ架构原理解析:消息存储,40张图文详解,我就不信你还参透不了并发编程

架构学习资料

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 1、拿到或创建文件操作对象MappedFile此处涉及点较多,我们在文件写入大节详细展开

  • 2、二次整理要落盘的消息格式

  • 之前已经整理过消息协议了,为什么此处还要进行二次整理?因为之前一些消息协议在没有加锁的时候,还无法确定。主要是以下三项内容:

  • a、queueOffset 队列偏移量,此值需要最终返回,且需要保证严格递增,所以需要在锁内进行

  • b、physicalOffset 物理偏移量,也就是全局文件的位置,注:此位置是全局文件的偏移量,不是当前文件的偏移量,所以其值可能会大于1G

  • c、storeTimestamp 存储时间戳,此处在锁内进行,主要是为了保证消息投递的时间严格保序

  • 3、记录写入信息

  • 记录当前文件写入情况:比如已写入字节数、存储时间等

三、文件开辟及写入

=========

3.1 文件开辟

========

文件的开辟是异步进行,有独立的线程专门负责开辟文件。我们可以先看下文件开辟的简单模型

RocketMQ架构原理解析:消息存储

也就是putMsg的线程会将开辟文件的请求委托给allocate file线程,然后进入阻塞,待allocate file线程将文件开辟完毕后,再唤醒putMsg线程

那此处我们便产生了2点疑问:

  • 1、putMsg把开辟文件的请求交给了allocate file线程,直到allocate file线程开辟完毕后才会唤醒putMsg线程,其实并没有起到异步开辟节省时间的目的,直接在putMsg线程中开辟文件不好吗?

  • 2、创建文件本身感觉并不耗时,不管是拿到文件的FileChannnel还是MappedByteBuffer,都是一件很快的操作,费尽周章的异步开辟真的有必要吗?

这两个疑问将逐步说明

3.1.1 开启堆外缓冲池

=============

至此我们要引入一个非常重要的配置变量transientStorePoolEnable,该配置项只在异步刷盘(FlushDiskType == AsyncFlush)的场景下,才会生效

如果配置项中,将transientStorePoolEnable置为false,便称为“开启堆外缓冲池”。那么这个变量到底起到什么作用呢?

RocketMQ架构原理解析:消息存储

系统启动时,会默认开辟5个(参数transientStorePoolSize控制)堆外内存DirectByteBuffer,循环利用。写消息时,消息都暂存至此,通过线程CommitRealTimeService将数据定时刷到page cache,当数据flush到disk后,再将DirectByteBuffer归还给缓冲池

而开辟过程是在broker启动时进行的;如上图所示,空间一旦开辟完毕后,文件都是预先创建好的,使用时直接返回文件引用即可,相当高效。但首次启动需要大量开辟堆外内存空间,会拉长broker的启动时长。我们看一下这块开辟的源码

/**

  • It’s a heavy init method.

*/

public void init() {

for (int i = 0; i < poolSize; i++) {

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(fileSize);

availableBuffers.offer(byteBuffer);

}

}

注释中也标识了这是个重量级的方法,主要耗时点在ByteBuffer.allocateDirect(fileSize),其实开辟内存并不耗时,耗时集中在为内存区域赋0操作,以下是JDK中DirectByteBuffer源码:

DirectByteBuffer(int cap) { // package-private

super(-1, 0, cap, cap);

long base = 0;

try {

base = unsafe.allocateMemory(size);

} catch (OutOfMemoryError x) {

Bits.unreserveMemory(size, cap);

throw x;

}

unsafe.setMemory(base, size, (byte) 0);

}

我们发现在开辟完内存后,开始执行了赋0操作unsafe.setMemory(base, size, 0)。其实可以利用反射巧妙地绕过这个耗时点

private static Field addr;

private static Field capacity;

static {

try {

addr = Buffer.class.getDeclaredField(“address”);

addr.setAccessible(true);

capacity = Buffer.class.getDeclaredField(“capacity”);

capacity.setAccessible(true);

} catch (NoSuchFieldException e) {

e.printStackTrace();

}

}

public static ByteBuffer newFastByteBuffer(int cap) {

long address = unsafe.allocateMemory(cap);

ByteBuffer bb = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());

try {

addr.setLong(bb, address);

capacity.setInt(bb, cap);

} catch (IllegalAccessException e) {

return null;

}

bb.clear();

return bb;

}

3.1.2 关闭堆外缓冲池

=============

关闭堆外内存池的话,就会启动MappedByteBuffer

RocketMQ架构原理解析:消息存储

  • a、首次启动

  • 第一次启动的时候,allocate线程会先后创建2个文件,第一个文件创建完毕后,便会返回putMsg线程并唤醒它,然后allocate线程进而继续异步创建下一个文件

  • b、后续启动

  • 后续请求allocate线程都会将已经创建好的文件直接返回给putMsg线程,然后继续异步创建下一个文件,这样便真正实现了异步创建文件的效果

3.1.3 文件预热

==========

我们再回顾一下本章刚开始提出的2个疑问:

  • 1、putMsg把开辟文件的请求交给了allocate file线程,直到allocate file线程开辟完毕后才会唤醒putMsg线程,其实并没有起到异步开辟节省时间的目的,直接在putMsg线程中开辟文件不好吗?

  • 2、创建文件本身感觉并不耗时,不管是拿到文件的FileChannnel还是MappedByteBuffer,都是一件很快的操作,费尽周章的异步开辟真的有必要吗?

第一个问题已经迎刃而解,即allocate线程通过异步创建下一个文件的方式,实现真正异步

本节讨论的便是第二个问题,其实如果只是单纯创建文件的话,的确是非常快的,不至于再使用异步操作。但RocketMQ对于新建文件有个文件预热(通过配置warmMapedFileEnable启停)功能,当然目的是为了磁盘提速,我么先看下源码

org.apache.rocketmq.store.MappedFile#warmMappedFile

for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {

byteBuffer.put(i, (byte) 0);

// force flush when flush disk type is sync

if (type == FlushDiskType.SYNC_FLUSH) {

if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {

flush = i;

mappedByteBuffer.force();

}

}

}

简单来说,就是将MappedByteBuffer每隔4K就写入一个0 byte,然后将整个文件撑满;如果刷盘策略是同步刷盘的话,还需要调用mappedByteBuffer.force(),当然这个操作是相当相当耗时的,所以也就需要我们进行异步处理。这样也就解释了第二个问题

但文件预热真的有效吗?我们不妨做个简单的基准测试

public class FileWriteCompare {

private static String filePath = “/Users/likangning/test/index3.data”;

private static int fileSize = 1024 * 1024 * 1024;

private static boolean warmFile = true;

private static int batchSize = 4096;

@Test

public void test() throws Exception {

File file = new File(filePath);

if (file.exists()) {

file.delete();

}

file.createNewFile();

FileChannel fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.WRITE, StandardOpenOption.READ);

MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize);

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(batchSize);

long beginTime = System.currentTimeMillis();

mappedByteBuffer.position(0);

while (mappedByteBuffer.remaining() >= batchSize) {

byteBuffer.position(batchSize);

byteBuffer.flip();

mappedByteBuffer.put(byteBuffer);

}

System.out.println("time cost is : " + (System.currentTimeMillis() - beginTime));

}

}

简单来说就是通过MappedByteBuffer写入1G文件,在我本地电脑上,平均耗时在 550ms 左右

然后在MappedByteBuffer写文件前加入预热操作

private void warmFile(MappedByteBuffer mappedByteBuffer) {

if (!warmFile) {

return;

}

int pageSize = 4096;

long begin = System.currentTimeMillis();

for (int i = 0, j = 0; i < fileSize; i += pageSize, j++) {

mappedByteBuffer.put(i, (byte) 0);

}

System.out.println("warm file time cost " + (System.currentTimeMillis() - begin));

}

耗时情况如下:

warm file time cost 492

time cost is : 125

预热后,写文件的耗时缩短了很多,但预热本身的耗时也几乎等同于文件写入的耗时了

以上是没有强制刷盘的测试效果,如果强制刷盘(#force)的话,个人经验是文件预热一定会带来性能的提升。从前两天结束的第二届中间件性能挑战赛来看,文件预热至少带来10%以上的提升。但是同非强制刷盘一样,文件预热操作实在是太重了

整体来看,文件预热后的写入操作,确实能带来性能上的提升,但是如果在系统压力较大、磁盘吞吐紧张的场景下,势必导致broker抖动,甚至请求超时,反而得不偿失。明白了此层概念后,再通过大量benchmark来决定是否开启此配置,做到有的放矢

3.2 文件写入

========

经过以上整理分析后,文件写入将变得非常轻;不论是DirectByteBuffer还是MappedByteBuffer都可以抽象为ByteBuffer,进而直接调用ByteBuffer.write()

四、刷盘策略

======

4.1 异步刷盘

========

RocketMQ架构原理解析:消息存储

4.1.1 异步+关闭写缓冲

==============

对应如下配置

FlushDiskType == AsyncFlush && transientStorePoolEnable == false

异步刷盘,且关闭缓冲池,对应的异步刷盘线程是FlushRealTimeService

上文可知,次策略是通过MappedByteBuffer写入的数据,所以此时数据已经在 page cache 中了

我们总结一下刷盘的策略:

  • 1、固定频率刷盘

总结

我个人认为,如果你想靠着背面试题来获得心仪的offer,用癞蛤蟆想吃天鹅肉形容完全不过分。想必大家能感受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。

这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。

大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

道面试官会问多广,多深,避免面试的时候一问三不知。

大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:

[外链图片转存中…(img-FFuW5bGz-1715323120102)]

[外链图片转存中…(img-tdRP3lwL-1715323120103)]

[外链图片转存中…(img-Opo6hPd1-1715323120103)]

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 19
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,让我来为您解答这个问题。 在一个大事务下进行新增、修改、查询等操作是非常常见的。但是,由于多个事务同时进行,可能会出现数据读取、写入不一致的情况。因此,数据库引入了事务隔离级别,来解决这些问题。 事务隔离级别是指多个事务之间的数据隔离程度,分为四个级别:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、串行化(Serializable)。 - 读未提交(Read Uncommitted):一个事务可以读取另一个事务未提交的数据。这个级别会导致脏读、不可重复读和幻读等问题。 - 读已提交(Read Committed):一个事务只能读取另一个事务已经提交的数据。这个级别能够解决脏读问题,但是可能会出现不可重复读和幻读问题。 - 可重复读(Repeatable Read):一个事务在执行过程中,多次读取同一数据,读取的结果是一致的。这个级别能够解决脏读和不可重复读问题,但是可能会出现幻读问题。 - 串行化(Serializable):最高的隔离级别,所有的事务串行执行,避免了脏读、不可重复读和幻读问题,但是并发性能非常差。 在Java中,我们可以使用JDBC事务来控制事务隔离级别。具体来说,可以使用Connection对象的setTransactionIsolation()方法来设置隔离级别。例如: ```java Connection conn = DriverManager.getConnection(url, username, password); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // 设置隔离级别为读已提交 ``` 除了使用JDBC事务,还可以使用Spring的声明式事务管理来控制事务隔离级别。具体来说,可以在@Transactional注解中设置isolation属性来指定隔离级别。例如: ```java @Transactional(isolation = Isolation.READ_COMMITTED) // 设置隔离级别为读已提交 public void doSomething() { // ... } ``` 总之,事务隔离级别是保证数据一致性的重要手段,不同的隔离级别适用于不同的场景。在实际开发中,需要根据具体情况选择合适的隔离级别。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值