首页 文章 精选 留言 我的

精选列表

搜索[热点技术征文第8期最强语言],共10000篇文章
优秀的个人博客,低调大师

【JVM技术探索】全流程化分析Java对象的创建过程

前言概要 对应过程则是:对象创建、对象内存布局、对象访问定位的三个过程。 对象的创建过程 对象的创建方式 java中对象的创建方式有很多种,常见的是通过new关键字和反射这两种方式来创建。除此之外,还有clone、反序列化等方式创建。 通过new关键字创建 // Person zhangsan = new Person(id, height, weight) Person zhangsan = new Person(); 通过反射创建 反射创建对象,可以通过class.newInstance()调用无参的构造器创建对象,也可以使用构造器来创建constructor.newInstance()。 //Class clz = Class.forName("Person类的全限定类名") Class clz = Person.class; Person zhangsan = clz.newInstance() // 使用构造器创建 Constructor<Person> cons = clz.getConstructor() // 也可以指定参数类型获取有参构造器 Person zhangsan1 = cons.newInstance() 通过clone创建对象 当类实现了Cloneable接口时,可以使用clone()方法复制一个对象。需要留意是clone方法是浅拷贝。 Person libo = new Person(name: "李博", age:12, ...) Person Livonor = new Person(name: "Livorno", age:32, ...) libo.setFather(Livonor) Person zhangsi = libo.clone() // 此时,张四和张三的名字、父亲在内存中都引用了相同的对象 反序列化创建 通过读取IO数据流创建,非本节重点 对象的创建过程 检查类加载(包含是否初始化、是否被加载、是否被解析) 对于new和反射两种创建方式而言,需要检查创建对象所使用的参数是否已完成类加载(比如它的类型和参数类型)。如果没有,要先完成类加载过程。 分配内存空间 虚拟机为对象分配内存,即起始地址和偏移量。 对象所需要的空间在创建前就可以确定,但是起始地址需要在分配时去内存中找到一块足够大的空间。地址的分配有两种方式:指针碰撞和空闲列表。 指针碰撞 指针碰撞的方式是假设内存空间是规整的,被使用的和空闲的内存被分割成了两整块,通过一个指针记录分界点。在给对象分配内存的时候,将指针空闲区域移动一段与对象大小相等的距离即可。 空闲列表 如果内存不规整,那么就需要维护一张表,来记录内存中那些地址是空闲的。分配对象时,通过空闲列表去找到一块足够大的空闲内存分配给对象并更新空闲列表。 多个线程创建的对象内存的冲突 举个例子,线程1和2同时要创建两个对象,指针是同一个。它们各自将指针加载到了cpu缓存,然后去执行分配地址空间的指令。结果就导致,后分配的哪一个,可能将先分配的那个对象的地址给覆盖了。 解决的办法有两种,一种是对分配内存的动作进行同步处理,即采用CAS加失败重试的方式,保证更新操作的原子性。 // 伪代码表示CAS+失败重试 while(true){ oldPtr = ptr //读取共享指针 newPtr = oldPtr + sizeOfInstance if(compareAndSet(oldPtr, ptr, newPtr)){break} } 另一种是使用TLAB的方式将线程的分配空间在堆内存中隔离开,在堆中为每个线程预先分配一小块不同的空间,每个线程创建对象都在自己对应的空间中完成。 即每个线程在 Java堆中预先分配一小块内存(本地线程分配缓冲(Thread Local Allocation Buffer ,TLAB)),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。 分配完内存之后,对象就已经存在于虚拟机的堆中了,此时虚拟机要将分配的内存空间初始化为零值(对象头例外)。 设置对象头 对象头包含了两种信息:MarkWord和类型指针。 MarkWord:存放对象本身的运行时状态数据(如HashCode, GC分代年龄、锁状态、是否偏向信息等) KlassPointer:类型指针指向它的类型的元数据。对象头在对象的内存布局中细讲。 即对象指向它的类元数据的指针 虚拟机通过这个指针来确定这个对象是哪个类的实例 数据长度:如果对象 是 数组,那么在对象头中还必须有一块用于记录数组长度的数据 因为虚拟机可以通过普通Java对象的元数据信息确定对象的大小,但是从数组的元数据中却无法确定数组的大小。 执行对象实例构造函数 首先递归的执行父类的构造函数,然后收集本类中为实例变量赋值的语句并执行,最后执行构造方法中的语句。 public class AddA { public static void main(String[] args) { Father guy = new Son(30); guy.saySomething(); System.out.println(guy.age); } } class Father{ int age = 60; public Father() { saySomething(); } public Father(int age) { this.age = age; } public void saySomething(){ System.out.println("I am the father, " + age + "years old"); } } class Son extends Father{ int age = 20; public Son(int age) { // super(); 不写则隐式调用方法,写则必须在子类构造方法的第一句 saySomething(); this.age = age; saySomething(); } public void saySomething(){ System.out.println("I am the son, " + age + " years old"); } } 因为它涉及到了多态与方法的动态分派。在这里先简单描述一下它的执行过程,用来掌握构造方法的执行还是ok的。 首先,创建一个Son对象,然后调用其有参构造方法Son(int age)。 在有参构造方法中隐式调用了父类的无参构造方法,然后父类的构造方法继续调Object的构造方法。接下来收集为父类成员变量赋值的语句并执行。 由于多态中子类的成员变量会覆盖父类的成员变量,因此子类对象的age仍然是0。 同时无参构造方法中的saySomething()此时是被子类对象调用的,因此打印了第一句I am the son, 0 years old。 然后,super()方法出栈,回到子类构造方法中。此时应该收集为子类成员变量赋值的语句并执行。对象的age=20,saySomething()打印出第二句I am the son, 20 years old。 然后执行构造方法中的赋值语句int age = age;saySomething(); 第三句话被打印I am the son, 30 years old。 子类对象创建完成,回到main方法。此时使用多态,将对象转成Father对象。 由于多态的规则:被重写的方法使用动态分派,查找(vtable)方法表,该方法实际是属于子类对象的。 因此guy.saySomething()实际调用的是子类对象的方法,打印出第四句话,I am the son, 30 years old。 最后,输出guy.age. 成员变量不具备多态性,因此打印出父类对象的age 60. I am the son, 0 years oldI am the son, 20 years oldI am the son, 30 years oldI am the son, 30 years old 对象的内存布局 对象在堆中的存储布局划分为三个部分:对象头、实例数据和对其填充(padding)。 对象的内存布局 对象头 对象头中包含markword(标记字段)和类型指针【数组长度】。 markword markword存储与对象自身定义数据无关的信息,用来表示对象的运行时状态。包括了HashCode,GC年龄,锁状态等信息。在一个32位的虚拟机中,markword用一个32位的bitmap表示,bitmap最后两位存放锁状态信息,如下图。 markword 普通状态下,状态为01,存储hashcode,分代年龄,偏向锁状态为0。 偏向锁状态下,状态为01,存储持有偏向锁的线程和重入次数,分代年龄,偏向锁状态为1。此时hashcode没了,但是,hashcode可以通过Object的hashcode()方法计算出来,只要没有重写该方法,那么得到的哈希码始终是一致的。 轻量级锁,状态为00。通过cas方式将对象的markword信息原子性地交换到了持有该对象锁的线程中,存储在lockRecord内,并同时将lockRecord的指针存放在对象头Markword的前30位。 重量级锁状态下,前30位存放指向锁控制器Monitor的指针,锁状态为10. 对象被标记为待回收状态时,最后两位状态为11. KlassPointer(类型指针) 指向类型元数据,从而可以通过对象来访问到它的类型信息。 数组长度(array length) 主要记录数组的长度信息一般为4字节(根据int的范围来考虑) 实例数据 实例数据中存放了对象的字段信息。无论是从父类继承的,还是在子类中定义的,都保存在实例数据中。 按照一定顺序存放,在满足这个顺序的条件下,父类定义的字段又会出现在子类定义的变量之前。 即代码中定义的字段内容 注:这部分数据的存储顺序会受到虚拟机分配参数(FieldAllocationStyle)和字段在Java源码中定义顺序的影响。 // HotSpot虚拟机默认的分配策略如下: longs/doubles、ints、shorts/chars、bytes/booleans、oop(Ordinary Object Pointers) // 从分配策略中可以看出,相同宽度的字段总是被分配到一起 // 在满足这个前提的条件下,父类中定义的变量会出现在子类之前 CompactFields = true; // 如果 CompactFields 参数值为true,那么子类之中较窄的变量也可能会插入到父类变量的空隙之中。 对齐填充 如果对象的实例数据占用空间不是8的整数倍,则填充0值让对象的占用空间位8的整数倍。 对象的访问定位 常见的有两种方式,句柄访问和直接指针访问。 句柄访问 使用句柄访问的话,对象的引用(如zhangsan),指向的是句柄池中的某个句柄,该句柄存放了指向实际实例对象的指针和指向方法区数据类型的指针。 其好处是,当对象被移动的时候(比如垃圾回收时,整理内存空间需要大量移动对象),不需要频繁的修改引用,只需要修改句柄中实例数据指针。 通过指针访问,则是对象的引用直接指向了该对象。其好处是,通过引用访问对象时,不需要多一次的指针定位,使得访问速度更快。

优秀的个人博客,低调大师

技术干货 | 源码解析 Github 上 14.1k Star 的 RocketMQ

站在发送方视角,通过源码,来分析在事务消息发送中 RocketMQ 是如何工作的。 前言 Apache RocketMQ 作为广为人知的开源消息中间件,诞生于阿里巴巴,于 2016 年捐赠给了 Apache。从 RocketMQ 4.0 到如今最新的 v4.7.1,不论是在阿里巴巴内部还是外部社区,都赢得了广泛的关注和好评。 本文将站在发送方视角,通过阅读 RocketMQ Producer 源码,来分析在事务消息发送中 RocketMQ 是如何工作的。 需要说明的是,本文所贴代码,均来自 4.7.1 版本的 RocketMQ 源码。本文中所讨论的发送,仅指从 Producer 发送到 Broker 的过程,并不包含 Broker 将消息投递到 Consumer 的过程。宏观概览 RocketMQ 事务消息发送流程: 结合源码来看,RocketMQ 的事务消息 TransactionMQProducer 的 sendMessageInTransaction 方法,实际调用了 DefaultMQProducerImpl 的 sendMessageInTransaction 方法。我们进入 sendMessageInTransaction 方法,整个事务消息的发送流程清晰可见。 首先,做发送前检查,并填入必要参数,包括设 prepare 事务消息。 源码清单-1 publicTransactionSendResultsendMessageInTransaction(finalMessagemsg, finalLocalTransactionExecuterlocalTransactionExecuter,finalObjectarg) throwsMQClientException{ TransactionListenertransactionListener=getCheckListener(); if(null==localTransactionExecuter&&null==transactionListener){ thrownewMQClientException("tranExecutorisnull",null); } //ignoreDelayTimeLevelparameter if(msg.getDelayTimeLevel()!=0){ MessageAccessor.clearProperty(msg,MessageConst.PROPERTY_DELAY_TIME_LEVEL); } Validators.checkMessage(msg,this.defaultMQProducer); SendResultsendResult=null; MessageAccessor.putProperty(msg,MessageConst.PROPERTY_TRANSACTION_PREPARED,"true"); MessageAccessor.putProperty(msg,MessageConst.PROPERTY_PRODUCER_GROUP,this.defaultMQProducer.getProducerGroup()); 进入发送处理流程: 源码清单-2 try{ sendResult=this.send(msg); }catch(Exceptione){ thrownewMQClientException("sendmessageException",e); } 根据 broker 返回的处理结果决策本地事务是否执行,半消息发送成功则开始本地事务执行: 源码清单-3 LocalTransactionStatelocalTransactionState=LocalTransactionState.UNKNOW; ThrowablelocalException=null; switch(sendResult.getSendStatus()){ caseSEND_OK:{ try{ if(sendResult.getTransactionId()!=null){ msg.putUserProperty("__transactionId__",sendResult.getTransactionId()); } StringtransactionId=msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX); if(null!=transactionId&&!"".equals(transactionId)){ msg.setTransactionId(transactionId); } if(null!=localTransactionExecuter){ localTransactionState=localTransactionExecuter.executeLocalTransactionBranch(msg,arg); }elseif(transactionListener!=null){ log.debug("UsednewtransactionAPI"); localTransactionState=transactionListener.executeLocalTransaction(msg,arg); } if(null==localTransactionState){ localTransactionState=LocalTransactionState.UNKNOW; } if(localTransactionState!=LocalTransactionState.COMMIT_MESSAGE){ log.info("executeLocalTransactionBranchreturn{}",localTransactionState); log.info(msg.toString()); } }catch(Throwablee){ log.info("executeLocalTransactionBranchexception",e); log.info(msg.toString()); localException=e; } } break; caseFLUSH_DISK_TIMEOUT: caseFLUSH_SLAVE_TIMEOUT: caseSLAVE_NOT_AVAILABLE://当备broker状态不可用时,半消息要回滚,不执行本地事务 localTransactionState=LocalTransactionState.ROLLBACK_MESSAGE; break; default: break; } 本地事务执行结束,根据本地事务状态进行二阶段处理: 源码清单-4 try{ this.endTransaction(sendResult,localTransactionState,localException); }catch(Exceptione){ log.warn("localtransactionexecute"+localTransactionState+",butendbrokertransactionfailed",e); } //组装发送结果 //... returntransactionSendResult; } 接下来,我们深入每个阶段代码分析。深扒内幕 Ⅰ阶段发送 重点分析 send 方法。进入 send 方法后,我们发现,RocketMQ 的事务消息的一阶段,使用了 SYNC 同步模式: 源码清单-5 publicSendResultsend(Messagemsg, longtimeout)throwsMQClientException,RemotingException,MQBrokerException,InterruptedException{ returnthis.sendDefaultImpl(msg,CommunicationMode.SYNC,null,timeout); } 这一点很容易理解,毕竟事务消息是要根据一阶段发送结果来决定要不要执行本地事务的,所以一定要阻塞等待 broker 的 ack。 我们进入 DefaultMQProducerImpl.java 中去看 sendDefaultImpl 方法的实现,通过读这个方法的代码,来尝试了解在事务消息的一阶段发送过程中 producer 的行为。 值得注意的是,这个方法并非为事务消息定制,甚至不是为 SYNC 同步模式定制的,因此读懂了这段代码,基本可以对 RocketMQ 的消息发送机制有了一个较为全面的认识。 这段代码逻辑非常通畅,不忍切片。为了节省篇幅,将代码中较为繁杂但信息量不大的部分以注释代替,尽可能保留流程的完整性。个人认为较为重要或是容易被忽略的部分,以注释标出,后文还有部分细节的详细解读。 源码清单-6 privateSendResultsendDefaultImpl( Messagemsg, finalCommunicationModecommunicationMode, finalSendCallbacksendCallback, finallongtimeout )throwsMQClientException,RemotingException,MQBrokerException,InterruptedException{ this.makeSureStateOK(); //一、消息有效性校验。见后文 Validators.checkMessage(msg,this.defaultMQProducer); finallonginvokeID=random.nextLong(); longbeginTimestampFirst=System.currentTimeMillis(); longbeginTimestampPrev=beginTimestampFirst; longendTimestamp=beginTimestampFirst; //获取当前topic的发送路由信息,主要是要broker,如果没找到则从namesrv获取 TopicPublishInfotopicPublishInfo=this.tryToFindTopicPublishInfo(msg.getTopic()); if(topicPublishInfo!=null&&topicPublishInfo.ok()){ booleancallTimeout=false; MessageQueuemq=null; Exceptionexception=null; SendResultsendResult=null; //二、发送重试机制。见后文 inttimesTotal=communicationMode==CommunicationMode.SYNC?1+this.defaultMQProducer.getRetryTimesWhenSendFailed():1; inttimes=0; String[]brokersSent=newString[timesTotal]; for(;times<timesTotal;times++){ //第一次发送是mq==null,之后都是有broker信息的 StringlastBrokerName=null==mq?null:mq.getBrokerName(); //三、rocketmq发送消息时如何选择队列?——broker异常规避机制 MessageQueuemqSelected=this.selectOneMessageQueue(topicPublishInfo,lastBrokerName); if(mqSelected!=null){ mq=mqSelected; brokersSent[times]=mq.getBrokerName(); try{ beginTimestampPrev=System.currentTimeMillis(); if(times>0){ //Resettopicwithnamespaceduringresend. msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic())); } longcostTime=beginTimestampPrev-beginTimestampFirst; if(timeout<costTime){ callTimeout=true; break; } //发送核心代码 sendResult=this.sendKernelImpl(msg,mq,communicationMode,sendCallback,topicPublishInfo,timeout-costTime); endTimestamp=System.currentTimeMillis(); //rocketmq选择broker时的规避机制,开启sendLatencyFaultEnable==true才生效 this.updateFaultItem(mq.getBrokerName(),endTimestamp-beginTimestampPrev,false); switch(communicationMode){ //四、RocketMQ的三种CommunicationMode。见后文 caseASYNC://异步模式 returnnull; caseONEWAY://单向模式 returnnull; caseSYNC://同步模式 if(sendResult.getSendStatus()!=SendStatus.SEND_OK){ if(this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()){ continue; } } returnsendResult; default: break; } }catch(RemotingExceptione){ //... //自动重试 }catch(MQClientExceptione){ //... //自动重试 }catch(MQBrokerExceptione){ //... //仅返回码==NOT_IN_CURRENT_UNIT==205时自动重试 //其他情况不重试,抛异常 }catch(InterruptedExceptione){ //... //不重试,抛异常 } }else{ break; } } if(sendResult!=null){ returnsendResult; } //组装返回的info信息,最后以MQClientException抛出 //...... //超时场景抛RemotingTooMuchRequestException if(callTimeout){ thrownewRemotingTooMuchRequestException("sendDefaultImplcalltimeout"); } //填充MQClientException异常信息 //... } validateNameServerSetting(); thrownewMQClientException("Norouteinfoofthistopic:"+msg.getTopic()+FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO), null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION); } 一、消息有效性校验 源码清单-7 Validators.checkMessage(msg,this.defaultMQProducer); 在此方法中校验消息的有效性,包括对 topic 和消息体的校验。topic 的命名必须符合规范,且避免使用内置的系统消息 TOPIC。消息体长度 > 0 && 消息体长度 <= 102410244 = 4M 。 源码清单-8 publicstaticvoidcheckMessage(Messagemsg,DefaultMQProducerdefaultMQProducer) throwsMQClientException{ if(null==msg){ thrownewMQClientException(ResponseCode.MESSAGE_ILLEGAL,"themessageisnull"); } //topic Validators.checkTopic(msg.getTopic()); Validators.isNotAllowedSendTopic(msg.getTopic()); //body if(null==msg.getBody()){ thrownewMQClientException(ResponseCode.MESSAGE_ILLEGAL,"themessagebodyisnull"); } if(0==msg.getBody().length){ thrownewMQClientException(ResponseCode.MESSAGE_ILLEGAL,"themessagebodylengthiszero"); } if(msg.getBody().length>defaultMQProducer.getMaxMessageSize()){ thrownewMQClientException(ResponseCode.MESSAGE_ILLEGAL, "themessagebodysizeovermaxvalue,MAX:"+defaultMQProducer.getMaxMessageSize()); } } 二、发送重试机制 Producer 在消息发送不成功时,会自动重试,最多发送次数 = retryTimesWhenSendFailed + 1 = 3 次 。 值得注意的是,并非所有异常情况都会重试,从以上源码中可以提取到的信息告诉我们,在以下三种情况下,会自动重试: 发生 RemotingException,MQClientException 两种异常之一时 发生 MQBrokerException 异常,且 ResponseCode 是 NOT_IN_CURRENT_UNIT = 205 时 SYNC 模式下,未发生异常且发送结果状态非 SEND_OK 在每次发送消息之前,会先检查是否在前面这两步就已经耗时超长(超时时长默认 3000ms),若是,则不再继续发送并且直接返回超时,不再重试。这里说明了 2 个问题: producer 内部自动重试对业务应用而言是无感知的,应用看到的发送耗时是包含所有重试的耗时在内的; 一旦超时意味着本次消息发送已经以失败告终,原因是超时。这个信息最后会以 RemotingTooMuchRequestException 的形式抛出。 这里需要指出的是,在 RocketMQ 官方文档中指出,发送超时时长是 10s,即 10000ms,网上许多人对 rocketMQ 的超时时间解读也认为是 10s。然而代码中却明明白白写着 3000ms,最终我 debug 之后确认,默认超时时间确实是 3000ms。 三、broker 的异常规避机制 源码清单-9 MessageQueuemqSelected=this.selectOneMessageQueue(topicPublishInfo,lastBrokerName); 这行代码是发送前选择 queue 的过程。 这里涉及 RocketMQ 消息发送高可用的的一个核心机制,latencyFaultTolerance。这个机制是 Producer 负载均衡的一部分,通过 sendLatencyFaultEnable 的值来控制,默认是 false 关闭状态,不启动 broker 故障延迟机制,值为 true 时启用 broker 故障延迟机制,可由 Producer 主动打开。 选择队列时,开启异常规避机制,则根据 broker 的工作状态避免选择当前状态不佳的 broker 代理,不健康的 broker 会在一段时间内被规避,不开启异常规避机制时,则按顺序选取下一个队列,但在重试场景下会尽量选择不同于上次发送 broker 的 queue。每次消息发送都会通过 updateFaultItem 方法来维护 broker 的状态信息。 源码清单-10 publicvoidupdateFaultItem(finalStringbrokerName,finallongcurrentLatency,booleanisolation){ if(this.sendLatencyFaultEnable){ //计算延迟多久,isolation表示是否需要隔离该broker,若是,则从30s往前找第一个比30s小的延迟值,再按下标判断规避的周期,若30s,则是10min规避; //否则,按上一次发送耗时来决定规避时长; longduration=computeNotAvailableDuration(isolation?30000:currentLatency); this.latencyFaultTolerance.updateFaultItem(brokerName,currentLatency,duration); } } 深入到 selectOneMessageQueue 方法内部一探究竟: 源码清单-11 publicMessageQueueselectOneMessageQueue(finalTopicPublishInfotpInfo,finalStringlastBrokerName){ if(this.sendLatencyFaultEnable){ //开启异常规避 try{ intindex=tpInfo.getSendWhichQueue().getAndIncrement(); for(inti=0;i<tpInfo.getMessageQueueList().size();i++){ intpos=Math.abs(index++)%tpInfo.getMessageQueueList().size(); if(pos<0) pos=0; //按顺序取下一个messagequeue作为发送的queue MessageQueuemq=tpInfo.getMessageQueueList().get(pos); //当前queue所在的broker可用,且与上一个queue的broker相同, //或者第一次发送,则使用这个queue if(latencyFaultTolerance.isAvailable(mq.getBrokerName())){ if(null==lastBrokerName||mq.getBrokerName().equals(lastBrokerName)) returnmq; } } finalStringnotBestBroker=latencyFaultTolerance.pickOneAtLeast(); intwriteQueueNums=tpInfo.getQueueIdByBroker(notBestBroker); if(writeQueueNums>0){ finalMessageQueuemq=tpInfo.selectOneMessageQueue(); if(notBestBroker!=null){ mq.setBrokerName(notBestBroker); mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement()%writeQueueNums); } returnmq; }else{ latencyFaultTolerance.remove(notBestBroker); } }catch(Exceptione){ log.error("Erroroccurredwhenselectingmessagequeue",e); } returntpInfo.selectOneMessageQueue(); } //不开启异常规避,则随机自增选择Queue returntpInfo.selectOneMessageQueue(lastBrokerName); } 四、RocketMQ 的三种 CommunicationMode 源码清单-12 publicenumCommunicationMode{ SYNC, ASYNC, ONEWAY, } 以上三种模式指的都是消息从发送方到达 broker 的阶段,不包含 broker 将消息投递给订阅方的过程。三种模式的发送方式的差异: 单向模式:ONEWAY。消息发送方只管发送,并不关心 broker 处理的结果如何。这种模式下,由于处理流程少,发送耗时非常小,吞吐量大,但不能保证消息可靠不丢,常用于流量巨大但不重要的消息场景,例如心跳发送等。 异步模式:ASYNC。消息发送方发送消息到 broker 后,无需等待 broker 处理,拿到的是 null 的返回值,而由一个异步的线程来做消息处理,处理完成后以回调的形式告诉发送方发送结果。异步处理时如有异常,返回发送方失败结果之前,会经过内部重试(默认 3 次,发送方不感知)。这种模式下,发送方等待时长较小,吞吐量较大,消息可靠,用于流量大但重要的消息场景。 同步模式:SYNC。消息发送方需等待 broker 处理完成并明确返回成功或失败,在消息发送方拿到消息发送失败的结果之前,也会经历过内部重试(默认 3 次,发送方不感知)这种模式下,发送方会阻塞等待消息处理结果,等待时长较长,消息可靠,用于流量不大但重要的消息场景。需要强调的是,事务消息的一阶段半事务消息的处理是同步模式。 在 sendKernelImpl 方法中也可以看到具体的实现差异。ONEWAY 模式最为简单,不做任何处理。负责发送的 sendMessage 方法参数中,相比同步模式,异步模式多了回调方法、包含 topic 发送路由元信息的 topicPublishInfo、包含发送 broker 信息的 instance、包含发送队列信息的 producer、重试次数。另外,异步模式下,会对有压缩的消息先做 copy。 源码清单-13 switch(communicationMode){ caseASYNC: MessagetmpMessage=msg; booleanmessageCloned=false; if(msgBodyCompressed){ //Ifmsgbodywascompressed,msgbodyshouldberesetusingprevBody. //Clonenewmessageusingcommpressedmessagebodyandrecoveroriginmassage. //Fixbug:https://github.com/apache/rocketmq-externals/issues/66 tmpMessage=MessageAccessor.cloneMessage(msg); messageCloned=true; msg.setBody(prevBody); } if(topicWithNamespace){ if(!messageCloned){ tmpMessage=MessageAccessor.cloneMessage(msg); messageCloned=true; } msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(),this.defaultMQProducer.getNamespace())); } longcostTimeAsync=System.currentTimeMillis()-beginStartTime; if(timeout<costTimeAsync){ thrownewRemotingTooMuchRequestException("sendKernelImplcalltimeout"); } sendResult=this.mQClientFactory.getMQClientAPIImpl().sendMessage( brokerAddr, mq.getBrokerName(), tmpMessage, requestHeader, timeout-costTimeAsync, communicationMode, sendCallback, topicPublishInfo, this.mQClientFactory, this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(), context, this); break; caseONEWAY: caseSYNC: longcostTimeSync=System.currentTimeMillis()-beginStartTime; if(timeout<costTimeSync){ thrownewRemotingTooMuchRequestException("sendKernelImplcalltimeout"); } sendResult=this.mQClientFactory.getMQClientAPIImpl().sendMessage( brokerAddr, mq.getBrokerName(), msg, requestHeader, timeout-costTimeSync, communicationMode, context, this); break; default: assertfalse; break; } 官方文档中有这样一张图,十分清晰的描述了异步通信的详细过程: Ⅱ 阶段发送 源码清单-3 体现了本地事务的执行,localTransactionState 将本地事务执行结果与事务消息二阶段的发送关联起来。 值得注意的是,如果一阶段的发送结果是 SLAVENOTAVAILABLE,即便 broker 不可用时,也会将 localTransactionState 置为 Rollback,此时将不会执行本地事务。之后由 endTransaction 方法负责二阶段提交,见源码清单-4。具体到 endTransaction 的实现: 源码清单-14 publicvoidendTransaction( finalSendResultsendResult, finalLocalTransactionStatelocalTransactionState, finalThrowablelocalException)throwsRemotingException,MQBrokerException,InterruptedException,UnknownHostException{ finalMessageIdid; if(sendResult.getOffsetMsgId()!=null){ id=MessageDecoder.decodeMessageId(sendResult.getOffsetMsgId()); }else{ id=MessageDecoder.decodeMessageId(sendResult.getMsgId()); } StringtransactionId=sendResult.getTransactionId(); finalStringbrokerAddr=this.mQClientFactory.findBrokerAddressInPublish(sendResult.getMessageQueue().getBrokerName()); EndTransactionRequestHeaderrequestHeader=newEndTransactionRequestHeader(); requestHeader.setTransactionId(transactionId); requestHeader.setCommitLogOffset(id.getOffset()); switch(localTransactionState){ caseCOMMIT_MESSAGE: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_COMMIT_TYPE); break; caseROLLBACK_MESSAGE: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_ROLLBACK_TYPE); break; caseUNKNOW: requestHeader.setCommitOrRollback(MessageSysFlag.TRANSACTION_NOT_TYPE); break; default: break; } requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup()); requestHeader.setTranStateTableOffset(sendResult.getQueueOffset()); requestHeader.setMsgId(sendResult.getMsgId()); Stringremark=localException!=null?("executeLocalTransactionBranchexception:"+localException.toString()):null; //采用oneway的方式发送二阶段消息 this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr,requestHeader,remark, this.defaultMQProducer.getSendMsgTimeout()); } 在二阶段发送时,之所以用 oneway 的方式发送,个人理解这正是因为事务消息有一个特殊的可靠机制——回查。消息回查 当 Broker 经过了一个特定的时间,发现依然没有得到事务消息的二阶段是否要提交或者回滚的确切信息,Broker 不知道 Producer 发生了什么情况(可能 producer 挂了,也可能 producer 发了 commit 但网络抖动丢了,也可能……于是主动发起回查。 事务消息的回查机制,更多的是在 broker 端的体现。RocketMQ 的 broker 以 Half 消息、Op 消息、真实消息三个不同的 topic 来将不同发送阶段的事务消息进行了隔离,使得 Consumer 只能看到最终确认 commit 需要投递出去的消息。其中详细的实现逻辑在本文中暂不多赘述,后续可另开一篇专门来从 Broker 视角来解读。 回到 Producer 的视角,当收到了 Broker 的回查请求,Producer 将根据消息检查本地事务状态,根据结果决定提交或回滚,这就要求 Producer 必须指定回查实现,以备不时之需。当然,正常情况下,并不推荐主动发送 UNKNOW 状态,这个状态毫无疑问会给 broker 带来额外回查开销,只在出现不可预知的异常情况时才启动回查机制,是一种比较合理的选择。 另外,4.7.1 版本的事务回查并非无限回查,而是最多回查 15 次: 源码清单-15 /** *Themaximumnumberoftimesthemessagewaschecked,ifexceedthisvalue,thismessagewillbediscarded. */ @ImportantField privateinttransactionCheckMax=15;附录 官方给出 Producer 的默认参数如下(其中超时时长的参数,在前文中也已经提到,debug 的结果是默认 3000ms,并非 10000ms):

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册