诸子百家

王选的博客


  • 首页

  • 标签

  • 分类

  • 归档

如何保证生产者投递到消息中间件的消息不丢失

发表于 2019-01-20 | 更新于 2019-08-28 | 分类于 消息中间件 , RabbitMQ

前情提示

上篇文章:面试大杀器:消息中间件如何实现消费吞吐量的百倍优化?,我们分析了RabbitMQ开启手动ack机制保证消费端数据不丢失的时候,prefetch机制对消费者的吞吐量以及内存消耗的影响。

通过分析,我们知道了prefetch过大容易导致内存溢出,prefetch过小又会导致消费吞吐量过低,所以在实际项目中需要慎重测试和设置。

这篇文章,我们转移到消息中间件的生产端,一起来看看如何保证投递到MQ的数据不丢失。

如果投递出去的消息在网络传输过程中丢失,或者在RabbitMQ的内存中还没写入磁盘的时候宕机,都会导致生产端投递到MQ的数据丢失。

而且丢失之后,生产端自己还感知不到,同时还没办法来补救。

下面的图就展示了这个问题。

保证投递消息不丢失的confirm机制

其实要解决这个问题,相信大家看过之前的消费端ack机制之后,也都猜到了。

很简单,就是生产端(比如上图的订单服务)首先需要开启一个confirm模式,接着投递到MQ的消息,如果MQ一旦将消息持久化到磁盘之后,必须也要回传一个confirm消息给生产端。

这样的话,如果生产端的服务接收到了这个confirm消息,就知道是已经持久化到磁盘了。

否则如果没有接收到confirm消息,那么就说明这条消息半路可能丢失了,此时你就可以重新投递消息到MQ去,确保消息不要丢失。

而且一旦你开启了confirm模式之后,每次消息投递也同样是有一个delivery tag的,也是起到唯一标识一次消息投递的作用。

这样,MQ回传ack给生产端的时候,会带上这个delivery tag。你就知道具体对应着哪一次消息投递了,可以删除这条消息。

此外,如果RabbitMQ接收到一条消息之后,结果内部出错发现无法处理这条消息,那么他会回传一个nack消息给生产端。此时你就会感知到这条消息可能处理有问题,你可以选择重新再次投递这条消息到MQ去。

或者另一种情况,如果某条消息很长时间都没给你回传ack/nack,那可能是极端意外情况发生了,数据也丢了,你也可以自己重新投递消息到MQ去。

通过这套confirm机制,就可以实现生产端投递消息不会丢失的效果。大家来看看下面的图,一起来感受一下。

confirm机制的代码实现

下面,我们再来看看confirm机制的代码实现:

confirm机制投递消息的高延迟性

这里有一个很关键的点,就是一旦启用了confirm机制投递消息到MQ之后,MQ是不保证什么时候会给你一个ack或者nack的。

因为RabbitMQ自己内部将消息持久化到磁盘,本身就是通过异步批量的方式来进行的。

正常情况下,你投递到RabbitMQ的消息都会先驻留在内存里,然后过了几百毫秒的延迟时间之后,再一次性批量把多条消息持久化到磁盘里去。

这样做,是为了兼顾高并发写入的吞吐量和性能的,因为要是你来一条消息就写一次磁盘,那么性能会很差,每次写磁盘都是一次fsync强制刷入磁盘的操作,是很耗时的。

所以正是因为这个原因,你打开了confirm模式之后,很可能你投递出去一条消息,要间隔几百毫秒之后,MQ才会把消息写入磁盘,接着你才会收到MQ回传过来的ack消息,这个就是所谓confirm机制投递消息的高延迟性。

大家看看下面的图,一起来感受一下。

高并发下如何投递消息才能不丢失

大家可以考虑一下,在生产端高并发写入MQ的场景下,你会面临两个问题:

1、你每次写一条消息到MQ,为了等待这条消息的ack,必须把消息保存到一个存储里。

并且这个存储不建议是内存,因为高并发下消息是很多的,每秒可能都几千甚至上万的消息投递出去,消息的ack要等几百毫秒的话,放内存可能有内存溢出的风险。

2、绝对不能以同步写消息 + 等待ack的方式来投递,那样会导致每次投递一个消息都同步阻塞等待几百毫秒,会导致投递性能和吞吐量大幅度下降。

针对这两个问题,相对应的方案其实也呼之欲出了。

首先,用来临时存放未ack消息的存储需要承载高并发写入,而且我们不需要什么复杂的运算操作,这种存储首选绝对不是MySQL之类的数据库,而建议采用kv存储。kv存储承载高并发能力极强,而且kv操作性能很高。

其次,投递消息之后等待ack的过程必须是异步的,也就是类似上面那样的代码,已经给出了一个初步的异步回调的方式。

消息投递出去之后,这个投递的线程其实就可以返回了,至于每个消息的异步回调,是通过在channel注册一个confirm监听器实现的。

收到一个消息ack之后,就从kv存储中删除这条临时消息;收到一个消息nack之后,就从kv存储提取这条消息然后重新投递一次即可;也可以自己对kv存储里的消息做监控,如果超过一定时长没收到ack,就主动重发消息。

大家看看下面的图,一起来体会一下:

消息中间件全链路100%数据不丢失能做到吗?

到此为止,我们已经把生产端和消费端如何保证消息不丢失的相关技术方案结合RabbitMQ这种中间件都给大家分析过了。

其实,架构思想是通用的, 无论你用的是哪一种MQ中间件,他们提供的功能是不太一样的,但是你都需要考虑如下几点:

生产端如何保证投递出去的消息不丢失:消息在半路丢失,或者在MQ内存中宕机导致丢失,此时你如何基于MQ的功能保证消息不要丢失?

MQ自身如何保证消息不丢失:起码需要让MQ对消息是有持久化到磁盘这个机制。

消费端如何保证消费到的消息不丢失:如果你处理到一半消费端宕机,导致消息丢失,此时怎么办?

目前来说,我们初步的借着RabbitMQ举例,已经把从前到后一整套技术方案的原理、设计和实现都给大家分析了一遍了。

但是此时真的能做到100%数据不丢失吗?恐怕未必,大家再考虑一下个特殊的场景。

生产端投递了消息到MQ,而且持久化到磁盘并且回传ack给生产端了。

但是此时MQ还没投递消息给消费端,结果MQ部署的机器突然宕机,而且因为未知的原因磁盘损坏了,直接在物理层面导致MQ持久化到磁盘的数据找不回来了。

这个大家千万别以为是开玩笑的,大家如果留意留意行业新闻,这种磁盘损坏导致数据丢失的是真的有的。

那么此时即使你把MQ重启了,磁盘上的数据也丢失了,数据是不是还是丢失了?

你说,我可以用MQ的集群机制啊,给一个数据做多个副本,比如后面我们就会给大家分析RabbitMQ的镜像集群机制,确实可以做到数据多副本。
但是即使数据多副本,一定可以做到100%数据不丢失?

比如说你的机房突然遇到地震,结果机房里的机器全部没了,数据是不是还是全丢了?

说这个,并不是说要抬杠。而是告诉大家,技术这个东西,100%都是理论上的期望。

应该说,我们凡事都朝着100%去做,但是理论上是不可能完全做到100%保证的,可能就是做到99.9999%的可能性数据不丢失,但是还是有千万分之一的概率会丢失。

当然,从实际的情况来说,能做到这种地步,其实基本上已经基本数据不会丢失了。

作者:石杉的架构笔记
链接:https://juejin.im/post/5c3c9fb3f265da61461e625b

消息中间件如何实现消费吞吐量的百倍优化

发表于 2019-01-20 | 更新于 2019-08-28 | 分类于 消息中间件 , RabbitMQ

前情提示

上一篇文章:互联网面试必杀:如何保证消息中间件全链路数据100%不丢失(2),我们分析了ack机制的底层实现原理(delivery tag机制),还有消除处理失败时的nack机制如何触发消息重发。

通过这个,已经让大家进一步对消费端保证数据不丢失的方案的理解更进一层了。

这篇文章,我们将会对ack底层的delivery tag机制进行更加深入的分析,让大家理解的更加透彻一些。

unack消息的积压问题

首先,我们要给大家介绍一下RabbitMQ的prefetch count这个概念。

大家看过上篇文章之后应该都知道了,对每个channel(其实对应了一个消费者服务实例,你大体可以这么来认为),RabbitMQ投递消息的时候,都是会带上本次消息投递的一个delivery tag的,唯一标识一次消息投递。

然后,我们进行ack时,也会带上这个delivery tag,基于同一个channel进行ack,ack消息里会带上delivery tag让RabbitMQ知道是对哪一次消息投递进行了ack,此时就可以对那条消息进行删除了。

大家先来看一张图,帮助大家回忆一下这个delivery tag的概念。

所以大家可以考虑一下,对于每个channel而言(你就认为是针对每个消费者服务实例吧,比如一个仓储服务实例),其实都有一些处于unack状态的消息。

比如RabbitMQ正在投递一条消息到channel,此时消息肯定是unack状态吧?

然后仓储服务接收到一条消息以后,要处理这条消息需要耗费时间,此时消息肯定是unack状态吧?

同时,即使你执行了ack之后,你要知道这个ack他默认是异步执行的,尤其如果你开启了批量ack的话,更是有一个延迟时间才会ack的,此时消息也是unack吧?

那么大家考虑一下,RabbitMQ他能够无限制的不停给你的消费者服务实例推送消息吗?

明显是不能的,如果RabbitMQ给你的消费者服务实例推送的消息过多过快,比如都有几千条消息积压在某个消费者服务实例的内存中。

那么此时这几千条消息都是unack的状态,一直积压着,是不是有可能会导致消费者服务实例的内存溢出?内存消耗过大?甚至内存泄露之类的问题产生?

所以说,RabbitMQ是必须要考虑一下消费者服务的处理能力的。

大家看看下面的图,感受一下如果消费者服务实例的内存中积压消息过多,都是unack的状态,此时会怎么样。

如何解决unack消息的积压问题

正是因为这个原因,RabbitMQ基于一个prefetch count来控制这个unack message的数量。

你可以通过”channel.basicQos(10)“这个方法来设置当前channel的prefetch count。

举个例子,比如你要是设置为10的话,那么意味着当前这个channel里,unack message的数量不能超过10个,以此来避免消费者服务实例积压unack message过多。

这样的话,就意味着RabbitMQ正在投递到channel过程中的unack message,以及消费者服务在处理中的unack message,以及异步ack之后还没完成ack的unack message,所有这些message加起来,一个channel也不能超过10个。

如果你要简单粗浅的理解的话,也大致可以理解为这个prefetch count就代表了一个消费者服务同时最多可以获取多少个message来处理。所以这里也点出了prefetch这个单词的意思。

prefetch就是预抓取的意思,就意味着你的消费者服务实例预抓取多少条message过来处理,但是最多只能同时处理这么多消息。

如果一个channel里的unack message超过了prefetch count指定的数量,此时RabbitMQ就会停止给这个channel投递消息了,必须要等待已经投递过去的消息被ack了,此时才能继续投递下一个消息。

老规矩,给大家上一张图,我们一起来看看这个东西是啥意思。

高并发场景下的内存溢出问题

好!现在大家对ack机制底层的另外一个核心机制:prefetch机制也有了一个深刻的理解了。

此时,咱们就应该来考虑一个问题了。就是如何来设置这个prefetch count呢?这个东西设置的过大或者过小有什么影响呢?

其实大家理解了上面的图就很好理解这个问题了。

假如说我们把prefetch count设置的很大,比如说3000,5000,甚至100000,就这样特别大的值,那么此时会如何呢?

这个时候,在高并发大流量的场景下,可能就会导致消费者服务的内存被快速的消耗掉。

因为假如说现在MQ接收到的流量特别的大,每秒都上千条消息,而且此时你的消费者服务的prefetch count还设置的特别大,就会导致可能一瞬间你的消费者服务接收到了达到prefetch count指定数量的消息。

打个比方,比如一下子你的消费者服务内存里积压了10万条消息,都是unack的状态,反正你的prefetch count设置的是10万。

那么对一个channel,RabbitMQ就会最多容忍10万个unack状态的消息,在高并发下也就最多可能积压10万条消息在消费者服务的内存里。

那么此时导致的结果,就是消费者服务直接被击垮了,内存溢出,OOM,服务宕机,然后大量unack的消息会被重新投递给其他的消费者服务,此时其他消费者服务一样的情况,直接宕机,最后造成雪崩效应。

所有的消费者服务因为扛不住这么大的数据量,全部宕机。

大家来看看下面的图,自己感受一下现场的氛围。

低吞吐量问题

那么如果反过来呢,我们要是把prefetch count设置的很小会如何呢?

比如说我们把prefetch count设置为1?此时就必然会导致消费者服务的吞吐量极低。因为你即使处理完一条消息,执行ack了也是异步的。

给你举个例子,假如说你的prefetch count = 1,RabbitMQ最多投递给你1条消息处于unack状态。

此时比如你刚处理完这条消息,然后执行了ack的那行代码,结果不幸的是,ack需要异步执行,也就是需要100ms之后才会让RabbitMQ感知到。

那么100ms之后RabbitMQ感知到消息被ack了,此时才会投递给你下一条消息!

这就尴尬了,在这100ms期间,你的消费者服务是不是啥都没干啊?

这不就直接导致了你的消费者服务处理消息的吞吐量可能下降10倍,甚至百倍,千倍,都有这种可能!

大家看看下面的图,感受一下低吞吐量的现场。

合理的设置prefetch count

所以鉴于上面两种极端情况,RabbitMQ官方给出的建议是prefetch count一般设置在100~300之间。

也就是一个消费者服务最多接收到100~300个message来处理,允许处于unack状态。

这个状态下可以兼顾吞吐量也很高,同时也不容易造成内存溢出的问题。

但是其实在我们的实践中,这个prefetch count大家完全是可以自己去压测一下的。

比如说慢慢调节这个值,不断加大,观察高并发大流量之下,吞吐量是否越来越大,而且观察消费者服务的内存消耗,会不会OOM、频繁FullGC等问题。

阶段性总结

其实通过最近几篇文章,基本上已经把消息中间件的消费端如何保证数据不丢失这个问题剖析的较为深入和透彻了。

如果你是基于RabbitMQ来做消息中间件的话,消费端的代码里,必须考虑三个问题:手动ack、处理失败的nack、prefetch count的合理设置

这三个问题背后涉及到了各种机制:

  • 自动ack机制
  • delivery tag机制
  • ack批量与异步提交机制
  • 消息重发机制
  • 手动nack触发消息重发机制
  • prefetch count过大导致内存溢出问题
  • prefetch count过小导致吞吐量过低

所以到现在,单论消费端这块的数据不丢失技术方案,相信大家可以有一整套自己的理解和方案。

作者:石杉的架构笔记
链接:https://juejin.im/post/5c3b392e518825255d296f78

如何保证消息中间件全链路数据不丢失2

发表于 2019-01-19 | 更新于 2019-08-28 | 分类于 消息中间件 , RabbitMQ

前情提示

上一篇文章:互联网面试必杀:如何保证消息中间件全链路数据100%不丢失(1),我们初步介绍了之前制定的那些消息中间件数据不丢失的技术方案遗留的问题。

一个最大的问题,就是生产者投递出去的消息,可能会丢失。

丢失的原因有很多,比如消息在网络传输到一半的时候因为网络故障就丢了,或者是消息投递到MQ的内存时,MQ突发故障宕机导致消息就丢失了。

针对这种生产者投递数据丢失的问题,RabbitMQ实际上是提供了一些机制的。

比如,有一种重量级的机制,就是事务消息机制。采用类事务的机制把消息投递到MQ,可以保证消息不丢失,但是性能极差,经过测试性能会呈现几百倍的下降。

所以说现在一般是不会用这种过于重量级的机制,而是会用轻量级的confirm机制。

但是我们这篇文章还不能直接讲解生产者保证消息不丢失的confirm机制,因为这种confirm机制实际上是采用了类似消费者的ack机制来实现的。

所以,要深入理解confirm机制,我们得先从这篇文章开始,深入的分析一下消费者手动ack机制保证消息不丢失的底层原理。

ack机制回顾

其实手动ack机制非常的简单,必须要消费者确保自己处理完毕了一个消息,才能手动发送ack给MQ,MQ收到ack之后才会删除这个消息。

如果消费者还没发送ack,自己就宕机了,此时MQ感知到他的宕机,就会重新投递这条消息给其他的消费者实例。

通过这种机制保证消费者实例宕机的时候,数据是不会丢失的。

再次提醒一下大家,如果还对手动ack机制不太熟悉的同学,可以回头看一下之前的一篇文章:扎心!线上服务宕机时,如何保证数据100%不丢失?。然后这篇文章,我们将继续深入探讨一下ack机制的实现原理。

ack机制实现原理:delivery tag

如果你写好了一个消费者服务的代码,让他开始从RabbitMQ消费数据,这时这个消费者服务实例就会自己注册到RabbitMQ。

所以,RabbitMQ其实是知道有哪些消费者服务实例存在的。

大家看看下面的图,直观的感受一下:

接着,RabbitMQ就会通过自己内部的一个“basic.delivery”方法来投递消息到仓储服务里去,让他消费消息。

投递的时候,会给这次消息的投递带上一个重要的东西,就是“delivery tag”,你可以认为是本次消息投递的一个唯一标识。

这个所谓的唯一标识,有点类似于一个ID,比如说消息本次投递到一个仓储服务实例的唯一ID。通过这个唯一ID,我们就可以定位一次消息投递。

所以这个delivery tag机制不要看很简单,实际上他是后面要说的很多机制的核心基础。

而且这里要给大家强调另外一个概念,就是每个消费者从RabbitMQ获取消息的时候,都是通过一个channel的概念来进行的。

大家回看一下下面的消费者代码片段,我们必须是先对指定机器上部署的RabbitMQ建立连接,然后通过这个连接获取一个channel。

而且如果大家还有点印象的话,我们在仓储服务里对消息的消费、ack等操作,全部都是基于这个channel来进行的,channel又有点类似于是我们跟RabbitMQ进行通信的这么一个句柄,比如看看下面的代码:

另外这里提一句:之前写那篇文章讲解手动ack保证数据不丢失的时候,有很多人提出疑问:为什么上面代码里直接是try finally,如果代码有异常,那还是会直接执行finally里的手动ack?其实很简单,自己加上catch就可以了。

好的,咱们继续。你大概可以认为这个channel就是进行数据传输的一个管道吧。对于每个channel而言,一个“delivery tag”就可以唯一的标识一次消息投递,这个delivery tag大致而言就是一个不断增长的数字。

大家来看看下面的图,相信会很好理解的:

如果采用手动ack机制,实际上仓储服务每次消费了一条消息,处理完毕完成调度发货之后,就会发送一个ack消息给RabbitMQ服务器,这个ack消息是会带上自己本次消息的delivery tag的。

咱们看看下面的ack代码,是不是带上了一个delivery tag?
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

然后,RabbitMQ根据哪个channel的哪个delivery tag,不就可以唯一定位一次消息投递了?

接下来就可以对那条消息删除,标识为已经处理完毕。

这里大家必须注意的一点,就是delivery tag仅仅在一个channel内部是唯一标识消息投递的。

所以说,你ack一条消息的时候,必须是通过接受这条消息的同一个channel来进行。

大家看看下面的图,直观的感受一下。

其实这里还有一个很重要的点,就是我们可以设置一个参数,然后就批量的发送ack消息给RabbitMQ,这样可以提升整体的性能和吞吐量。

比如下面那行代码,把第二个参数设置为true就可以了。
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), true);

看到这里,大家应该对这个ack机制的底层原理有了稍微进一步的认识了。起码是知道delivery tag是啥东西了,他是实现ack的一个底层机制。

然后,我们再来简单回顾一下自动ack、手动ack的区别。

实际上默认用自动ack,是非常简单的。RabbitMQ只要投递一个消息出去给仓储服务,那么他立马就把这个消息给标记为删除,因为他是不管仓储服务到底接收到没有,处理完没有的。

所以这种情况下,性能很好,但是数据容易丢失。

如果手动ack,那么就是必须等仓储服务完成商品调度发货以后,才会手动发送ack给RabbitMQ,此时RabbitMQ才会认为消息处理完毕,然后才会标记消息为删除。

这样在发送ack之前,仓储服务宕机,RabbitMQ会重发消息给另外一个仓储服务实例,保证数据不丢。

RabbitMQ如何感知到仓储服务实例宕机

之前就有同学提出过这个问题,但是其实要搞清楚这个问题,其实不需要深入的探索底层,只要自己大致的思考和推测一下就可以了。

如果你的仓储服务实例接收到了消息,但是没有来得及调度发货,没有发送ack,此时他宕机了。

我们想一想就知道,RabbitMQ之前既然收到了仓储服务实例的注册,因此他们之间必然是建立有某种联系的。

一旦某个仓储服务实例宕机,那么RabbitMQ就必然会感知到他的宕机,而且对发送给他的还没ack的消息,都发送给其他仓储服务实例。

所以这个问题以后有机会我们可以深入聊一聊,在这里,大家其实先建立起来这种认识即可。

我们再回头看看下面的架构图:

仓储服务处理失败时的消息重发

首先,我们来看看下面一段代码:

假如说某个仓储服务实例处理某个消息失败了,此时会进入catch代码块,那么此时我们怎么办呢?难道还是直接ack消息吗?

当然不是了,你要是还是ack,那会导致消息被删除,但是实际没有完成调度发货。

这样的话,数据不是还是丢失了吗?因此,合理的方式是使用nack操作。

就是通知RabbitMQ自己没处理成功消息,然后让RabbitMQ将这个消息再次投递给其他的仓储服务实例尝试去完成调度发货的任务。

我们只要在catch代码块里加入下面的代码即可:
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), true);

注意上面第二个参数是true,意思就是让RabbitMQ把这条消息重新投递给其他的仓储服务实例,因为自己没处理成功。

你要是设置为false的话,就会导致RabbitMQ知道你处理失败,但是还是删除这条消息,这是不对的。

同样,我们还是来一张图,大家一起来感受一下

阶段总结

这篇文章对之前的ack机制做了进一步的分析,包括底层的delivery tag机制,以及消息处理失败时的消息重发。

通过ack机制、消息重发等这套机制的落地实现,就可以保证一个消费者服务自身突然宕机、消息处理失败等场景下,都不会丢失数据。

作者:石杉的架构笔记
链接:https://juejin.im/post/5c3759fe51882525616dbaf6

Java反射探索-从类加载说起

发表于 2019-01-19 | 更新于 2019-08-28 | 分类于 Java基础 , JVM

类加载

JVM和类

当我们调用Java命令运行某个Java程序时,该命令将会启动一条Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里。正如前面介绍的,同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。当系统出现以下几种情况时,JVM进程将被终止:

  1. 程序运行到最后正常结束。
  2. 程序运行到使用System.exit()或Runtime.getRuntime().exit()代码结束程序。
  3. 程序执行过程中遇到未捕获的异常或错误而结束。
  4. 程序所在平台强制结束了JVM进程。

从上面的介绍可以看出,当Java程序运行结束时,JVM进程结束,该进程在内存中状态将会丢失。

类的生命周期

当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化。

加载:查找并加载类的二进制数据

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

注意:将编译后的java类文件(也就是.class文件)中的二进制数据读入内存,并将其放在运行时数据区的方法区内,然后再堆区创建一个Java.lang.Class对象,用来封装类在方法区的数据结构。即加载后最终得到的是Class对象,并且更加值得注意的是:该Java.lang.Class对象是单实例的,无论这个类创建了多少个对象,他的Class对象时唯一的!

连接:

  1. 验证:确保被加载的类的正确性
  2. 准备:为类的静态变量分配内存,并将其初始化为默认值
  3. 解析:把类中的符号引用转换为直接引用。

初始化:为类的静态变量赋予正确的初始值。

注意:连接和初始化阶段,其实静态变量经过了两次赋值:第一次是静态变量类型的默认值;第二次是我们真正赋给静态变量的值。

笔者简单画了个图,其过程如下:

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。事实上,每个类是一批具有相同特征的对象的抽象(或者说概念),而系统中所有的类,它们实际上也是对象,它们都是java.lang.Class的实例。

加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是我们前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源:

  1. 从本地文件系统来加载class文件,这是绝大部分示例程序的类加载方式。
  2. 从JAR包中加载class文件,这种方式也是很常见的,JDBC编程时用到的数据库驱动类就是放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
  3. 通过网络加载class文件。
  4. 把一个Java源文件动态编译、并执行加载。

类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

Java程序对类的使用方式

主动使用

  1. 创建类的实例
  2. 方法某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(如 Class.forName(“com.itzhai.Test”))
  5. 初始化一个类的子类
  6. Java虚拟机启动时被标明为启动类的类(Main Class)

被动使用

除了以上6中方式,其他对类的使用都是被动使用,都不会导致类的初始化。类的初始化时机正是java程序对类的首次主动使用,所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才初始化它们。

对象初始化

在类被装载、连接和初始化,这个类就随时都可能使用了。对象实例化和初始化是就是对象生命的起始阶段的活动,在这里我们主要讨论对象的初始化工作的相关特点。

Java编译器在编译每个类时都会为该类至少生成一个实例初始化方法–即”<init>()” 方法。此方法与源代码中的每个构造方法相对应,如果类没有明确地声明任何构造方法,编译器则为该类生成一个默认的无参构造方法,这个默认的构造器仅仅调用父类的无参构造器,与此同时也会生成一个与默认构造方法对应的 “<init>()” 方法.通常来说,<init>() 方法内包括的代码内容大概为:调用另一个 <init>() 方法;对实例变量初始化;与其对应的构造方法内的代码。

如果构造方法是明确地从调用同一个类中的另一个构造方法开始,那它对应的 <init>() 方法体内包括的内容为:一个对本类的 <init>() 方法的调用;对应用构造方法内的所有字节码。如果构造方法不是通过调用自身类的其它构造方法开始,并且该对象不是 Object 对象,那 <init>() 法内则包括的内容为:一个对父类 <init>() 方法的调用;对实例变量初始化方法的字节码;最后是对应构造子的方法体字节码。如果这个类是 Object,那么它的 <init>() 方法则不包括对父类 <init>() 方法的调用。

获取对象的异同

  1. 相同点:

    通过这几种方式,得到的都是Java.lang.Class对象(这个是上面讲到的类在加载时获得的最终产物),例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class people {

public static void main(String[] args) throws Exception {
System.out.println("..............使用不同的方式加载类...................");
System.out.println(people.class);//通过类.class获得Class对象
people a = new people();
System.out.println(a.getClass());//通过 实例名.getClass()获得Class对象
System.out.println(Class.forName("com.lin.people"));//通过Class.forName(全路径)获得Class对象
System.out.println("..............使用不同的方式创建对象...................");
System.out.println(a);//使用不同的方式创建对象
System.out.println(people.class.newInstance());
System.out.println(a.getClass().newInstance());
System.out.println(Class.forName("com.lin.people").newInstance());
}

}

结果:

从上面可以看到不同的方式加载类。其实这一过程只发生一次!

  1. 区别:

    下面用一个实例来说说它们的区别,如下新建一个类
    1
    2
    3
    4
    5
    public class Cat {
    static {
    System.out.println("生成了一只猫");
    }
    }

然后开始使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CatTest {

public static void main(String[] args) throws Exception{
System.out.println("---------------Cat.class开始------------------");
System.out.println(Cat.class);//通过类.class获得Class对象
System.out.println("---------------Cat.class结束------------------");

System.out.println("---------------Class.forName开始------------------");
System.out.println(Class.forName("com.lin.Cat"));//通过Class.forName(全路径)获得Class对象
System.out.println("---------------Class.forName结束------------------");

System.out.println("---------------cat.getClass()开始------------------");
Cat cat = new Cat();
System.out.println(cat.getClass());//通过Class.forName(全路径)获得Class对象
System.out.println("---------------cat.getClass()结束------------------");
}

}

输出结果:

如果,将Class.forName()去掉,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CatTest {

public static void main(String[] args) throws Exception{
System.out.println("---------------Cat.class开始------------------");
System.out.println(Cat.class);//通过类.class获得Class对象
System.out.println("---------------Cat.class结束------------------");


System.out.println("---------------cat.getClass()开始------------------");
Cat cat = new Cat();
System.out.println(cat.getClass());//通过Class.forName(全路径)获得Class对象
System.out.println("---------------cat.getClass()结束------------------");
}

}

结果又变成:

所以,可以得出以下结论:

  1. Class cl=Cat.class; JVM将使用类Cat的类装载器,将类A装入内存(前提是:类A还没有装入内存),不对类A做类的初始化工作.返回类A的Class的对象
  2. Class cl=对象引用o.getClass();返回引用o运行时真正所指的对象(因为:儿子对象的引用可能会赋给父对象的引用变量中)所属的类的Class的对象 ,如果还没装载过,会进行装载。
  3. Class.forName(“类名”); 装入类A,并做类的初始化(前提是:类A还没有装入内存)

new()和newInstance()

从JVM的角度看,我们使用关键字new创建一个类的时候,这个类可以没有被加载。但是使用Class对象的newInstance()方法的时候,就必须保证:

  • 这个类已经加载;
  • 这个类已经连接了。而完成上面两个步骤的正是Class的静态方法forName()所完成的,这个静态方法调用了启动类加载器,即加载 java API的那个加载器。

现在可以看出,Class对象的newInstance()(这种用法和Java中的工厂模式有着异曲同工之妙)实际上是把new这个方式分解为两步,即首先调用Class加载方法加载某个类,然后实例化。这样分步的好处是显而易见的。我们可以在调用class的静态加载方法forName时获得更好的灵活性,提供给了一种降耦的手段。 Class.forName().newInstance()和通过new得到对象的区别

  1. 使用newInstance可以解耦。使用newInstance的前提是,类已加载并且这个类已连接,这是正是class的静态方法forName()完成的工作。newInstance实际上是把new 这个方式分解为两步,即,首先调用class的加载方法加载某个类,然后实例化。
  2. newInstance: 弱类型。低效率。只能调用无参构造。 new: 强类型。相对高效。能调用任何public构造。
  3. newInstance()是实现IOC、反射、面对接口编程和依赖倒置等技术方法的必然选择,new只能实现具体类的实例化,不适合于接口编程。
  4. newInstance() 一般用于动态加载类。
  5. Class.forName(“”).newInstance()返回的是object 。
  6. newInstance( )是一个方法,而new是一个关键字;

注:一般在通用框架里面用的就是class.forName来加载类,然后再通过反射来调用其中的方法,譬如Tomcat源码里面,这样就避免了new关键字的耦合度,还有让不同的类加载器来加载不同的类,方便提高类之间的安全性和隔离性。

吃透JavaClassLoader

发表于 2019-01-19 | 更新于 2019-08-28 | 分类于 Java基础 , JVM

ClassLoader是Java最为神秘的技术之一,无数人被它伤透了脑筋,摸不清门道究竟在哪里。网上的文章也是一篇又一篇,绝大部分内容都是在误导别人。本文彻底吃透ClassLoader,以后其它的相关文章可以不必再细看了。

ClassLoader做什么的?

顾名思义,它是用来加载Class的。它负责将Class的字节码形式转换成内存形式的Class对象。字节码可以来自于磁盘文件*.class,也可以是jar包里的*.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

有很多字节码加密技术就是依靠定制ClassLoader来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader先解密文件内容再加载这些解密后的字节码。

每个Class对象的内部都有一个classLoader 字段来标识自己是由哪个ClassLoader加载的。ClassLoader就像一个容器,里面装了很多已经加载的Class对象。

1
2
3
4
5
class Class<T> {
...
private final ClassLoader classLoader;
...
}

延迟加载

JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。

比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。

各司其职

JVM运行实例中会存在多个ClassLoader,不同的ClassLoader会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的jar文件中加载,也可以从网络上不同的静态文件服务器来下载字节码再加载。

JVM中内置了三个重要的ClassLoader,分别是BootstrapClassLoader、ExtensionClassLoader和AppClassLoader。

BootstrapClassLoader负责加载JVM运行时核心类,这些类位于$JAVA_HOME/lib/rt.jar文件中,我们常用内置库 java.xxx.*都在里面,比如java.util.*、java.io.*、java.nio.*、java.lang.*等等。这个ClassLoader 比较特殊,它是由C代码实现的,我们将它称之为「根加载器」。

ExtensionClassLoader负责加载JVM扩展类,比如swing系列、内置的js引擎、xml解析器等等,这些库名通常以javax开头,它们的jar包位于$JAVA_HOME/lib/ext/*.jar中,有很多jar包。

AppClassLoader才是直接面向我们用户的加载器,它会加载Classpath环境变量里定义的路径中的jar包和目录。我们自己编写的代码以及使用的第三方jar包通常都是由它来加载的。

那些位于网络上静态文件服务器提供的jar包和class文件jdk内置了一个URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用URLClassLoader来加载远程类库了。URLClassLoader不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader和AppClassLoader都是URLClassLoader的子类,它们都是从本地文件系统里加载类库。

AppClassLoader可以由ClassLoader类提供的静态方法getSystemClassLoader()得到,它就是我们所说的「系统类加载器」,我们用户平时编写的类代码通常都是由它加载的。当我们的main方法执行的时候,这第一个用户类的加载器就是 AppClassLoader。

ClassLoader传递性

程序在运行过程中,遇到了一个未知的类,它会选择哪个ClassLoader来加载它呢?虚拟机的策略是使用调用者Class对象的ClassLoader来加载当前未知的类。何为调用者Class对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者Class对象。前面我们提到每个Class 对象里面都有一个classLoader属性记录了当前的类是由谁来加载的。

因为ClassLoader的传递性,所有延迟加载的类都会由初始调用main方法的这个ClassLoader全全负责,它就是 AppClassLoader。

双亲委派

前面我们提到AppClassLoader只负责加载Classpath下面的类库,如果遇到没有加载的系统类库怎么办,AppClassLoader必须将系统类库的加载工作交给BootstrapClassLoader和ExtensionClassLoader来做,这就是我们常说的「双亲委派」。

AppClassLoader在加载一个未知的类名时,它并不是立即去搜寻Classpath,它会首先将这个类名称交给 ExtensionClassLoader来加载,如果ExtensionClassLoader可以加载,那么AppClassLoader就不用麻烦了。否则它就会搜索Classpath 。

而ExtensionClassLoader在加载一个未知的类名时,它也并不是立即搜寻ext路径,它会首先将类名称交给BootstrapClassLoader来加载,如果BootstrapClassLoader可以加载,那么ExtensionClassLoader也就不用麻烦了。否则它就会搜索ext路径下的jar包。

这三个ClassLoader之间形成了级联的父子关系,每个ClassLoader都很懒,尽量把工作交给父亲做,父亲干不了了自己才会干。每个ClassLoader对象内部都会有一个parent属性指向它的父加载器。

1
2
3
4
5
class ClassLoader {
...
private final ClassLoader parent;
...
}

值得注意的是图中的ExtensionClassLoader的parent指针画了虚线,这是因为它的parent的值是 null,当parent字段是null时就表示它的父加载器是「根加载器」。如果某个Class对象的classLoader属性值是null,那么就表示这个类也是「根加载器」加载的。注意这里的parent不是super不是父类,只是ClassLoader内部的字段。

Class.forName

当我们在使用jdbc驱动时,经常会使用Class.forName方法来动态加载驱动类。
Class.forName("com.mysql.cj.jdbc.Driver");

其原理是mysql驱动的Driver类里有一个静态代码块,它会在Driver类被加载的时候执行。这个静态代码块会将mysql驱动实例注册到全局的jdbc驱动管理器里。

1
2
3
4
5
6
7
8
9
10
class Driver {
static {
  try {
      java.sql.DriverManager.registerDriver(new Driver());
  } catch (SQLException E) {
      throw new RuntimeException("Can't register driver!");
  }
}
...
}

forName方法同样也是使用调用者Class对象的ClassLoader来加载目标类。不过forName还提供了多参数版本,可以指定使用哪个ClassLoader来加载
Class<?> forName(String name, boolean initialize, ClassLoader cl)

通过这种形式的forName方法可以突破内置加载器的限制,通过使用自定类加载器允许我们自由加载其它任意来源的类库。根据ClassLoader的传递性,目标类库传递引用到的其它类库也将会使用自定义加载器加载。

自定义加载器

ClassLoader里面有三个重要的方法loadClass()、findClass()和defineClass()。

loadClass()方法是加载目标类的入口,它首先会查找当前ClassLoader以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用findClass()让自定义加载器自己来加载目标类。ClassLoader的findClass()方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用defineClass()方法将字节码转换成Class对象。下面我使用伪代码表示一下基本过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ClassLoader {
// 加载入口,定义了双亲委派规则
Class loadClass(String name) {
// 是否已经加载了
Class t = this.findFromLoaded(name);
if(t == null) {
// 交给双亲
t = this.parent.loadClass(name)
}
if(t == null) {
// 双亲都不行,只能靠自己了
t = this.findClass(name);
}
return t;
}

// 交给子类自己去实现
Class findClass(String name) {
throw ClassNotFoundException();
}

// 组装Class对象
Class defineClass(byte[] code, String name) {
return buildClassFromCode(code, name);
}
}

class CustomClassLoader extends ClassLoader {
Class findClass(String name) {
// 寻找字节码
byte[] code = findCodeFromSomewhere(name);
// 组装Class对象
return this.defineClass(code, name);
}
}

自定义类加载器不易破坏双亲委派规则,不要轻易覆盖loadClass方法。否则可能会导致自定义加载器无法加载内置的核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是null,那就表示父加载器是「根加载器」。

1
2
// ClassLoader 构造器
protected ClassLoader(String name, ClassLoader parent);

双亲委派规则可能会变成三亲委派,四亲委派,取决于你使用的父加载器是谁,它会一直递归委派到根加载器。

Class.forName vs ClassLoader.loadClass

这两个方法都可以用来加载目标类,它们之间有一个小小的区别,那就是Class.forName()方法可以获取原生类型的 Class,而ClassLoader.loadClass()则会报错。

1
2
3
4
5
6
7
8
9
10
11
Class<?> x = Class.forName("[I");
System.out.println(x);

x = ClassLoader.getSystemClassLoader().loadClass("[I");
System.out.println(x);

---------------------
class [I

Exception in thread "main" java.lang.ClassNotFoundException: [I
...

钻石依赖

项目管理上有一个著名的概念叫着「钻石依赖」,是指软件依赖导致同一个软件包的两个版本需要共存而不能冲突。

我们平时使用的maven是这样解决钻石依赖的,它会从多个冲突的版本中选择一个来使用,如果不同的版本之间兼容性很糟糕,那么程序将无法正常编译运行。Maven这种形式叫「扁平化」依赖管理。

使用ClassLoader可以解决钻石依赖问题。不同版本的软件包使用不同的ClassLoader来加载,位于不同ClassLoader 中名称一样的类实际上是不同的类。下面让我们使用URLClassLoader来尝试一个简单的例子,它默认的父加载器是 AppClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ cat ~/source/jcl/v1/Dep.java
public class Dep {
public void print() {
System.out.println("v1");
}
}

$ cat ~/source/jcl/v2/Dep.java
public class Dep {
public void print() {
System.out.println("v1");
}
}

$ cat ~/source/jcl/Test.java
public class Test {
public static void main(String[] args) throws Exception {
String v1dir = "file:///Users/qianwp/source/jcl/v1/";
String v2dir = "file:///Users/qianwp/source/jcl/v2/";
URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)});
URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)});

Class<?> depv1Class = v1.loadClass("Dep");
Object depv1 = depv1Class.getConstructor().newInstance();
depv1Class.getMethod("print").invoke(depv1);

Class<?> depv2Class = v2.loadClass("Dep");
Object depv2 = depv2Class.getConstructor().newInstance();
depv2Class.getMethod("print").invoke(depv2);

System.out.println(depv1Class.equals(depv2Class));
}
}

在运行之前,我们需要对依赖的类库进行编译

1
2
3
4
5
6
7
8
9
10
$ cd ~/source/jcl/v1
$ javac Dep.java
$ cd ~/source/jcl/v2
$ javac Dep.java
$ cd ~/source/jcl
$ javac Test.java
$ java Test
v1
v2
false

在这个例子中如果两个URLClassLoader指向的路径是一样的,下面这个表达式还是false,因为即使是同样的字节码用不同的ClassLoader加载出来的类都不能算同一个类
depv1Class.equals(depv2Class)

我们还可以让两个不同版本的Dep类实现同一个接口,这样可以避免使用反射的方式来调用Dep类里面的方法。

1
2
3
Class<?> depv1Class = v1.loadClass("Dep");
IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();
depv1.print()

ClassLoader固然可以解决依赖冲突问题,不过它也限制了不同软件包的操作界面必须使用反射或接口的方式进行动态调用。Maven没有这种限制,它依赖于虚拟机的默认懒惰加载策略,运行过程中如果没有显示使用定制的ClassLoader,那么从头到尾都是在使用AppClassLoader,而不同版本的同名类必须使用不同的ClassLoader加载,所以Maven不能完美解决钻石依赖。如果你想知道有没有开源的包管理工具可以解决钻石依赖的,我推荐你了解一下sofa-ark,它是蚂蚁金服开源的轻量级类隔离框架。

分工与合作

这里我们重新理解一下ClassLoader的意义,它相当于类的命名空间,起到了类隔离的作用。位于同一个ClassLoader 里面的类名是唯一的,不同的ClassLoader可以持有同名的类。ClassLoader是类名称的容器,是类的沙箱。

不同的ClassLoader之间也会有合作,它们之间的合作是通过parent属性和双亲委派机制来完成的。parent具有更高的加载优先级。除此之外,parent还表达了一种共享关系,当多个子ClassLoader共享同一个parent时,那么这个parent里面包含的类可以认为是所有子ClassLoader共享的。这也是为什么BootstrapClassLoader被所有的类加载器视为祖先加载器,JVM核心类库自然应该被共享。

Thread.contextClassLoader

如果你稍微阅读过Thread的源代码,你会在它的实例字段中发现有一个字段非常特别

1
2
3
4
5
6
7
8
9
10
11
12
13
class Thread {
...
private ClassLoader contextClassLoader;

public ClassLoader getContextClassLoader() {
return contextClassLoader;
}

public void setContextClassLoader(ClassLoader cl) {
this.contextClassLoader = cl;
}
...
}

contextClassLoader「线程上下文类加载器」,这究竟是什么东西?

首先contextClassLoader是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它。你可以使用下面这种方式来显示使用它
Thread.currentThread().getContextClassLoader().loadClass(name);

这意味着如果你使用forName(string name)方法加载目标类,它不会自动使用contextClassLoader。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用contextClassLoader来加载。

其次线程的contextClassLoader默认是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的main线程的contextClassLoader就是AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader都是AppClassLoader。

那这个contextClassLoader究竟是做什么用的?我们要使用前面提到了类加载器分工与合作的原理来解释它的用途。
它可以做到跨线程共享类,只要它们共享同一个contextClassLoader。父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。

如果不同的线程使用不同的contextClassLoader,那么不同的线程使用的类就可以隔离开来。

如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个contextClassLoader,线程池之间使用不同的contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。

如果我们不去定制contextClassLoader,那么所有的线程将会默认使用AppClassLoader,所有的类都将会是共享的。

线程的contextClassLoader使用场合比较罕见,如果上面的逻辑晦涩难懂也不必过于计较。

JDK9增加了模块功能之后对类加载器的结构设计做了一定程度的修改,不过类加载器的原理还是类似的,作为类的容器,它起到类隔离的作用,同时还需要依靠双亲委派机制来建立不同的类加载器之间的合作关系。

作者:老錢
链接:https://juejin.im/post/5c04892351882516e70dcc9b

Dubbo积累

发表于 2018-05-25 | 更新于 2019-08-28 | 分类于 Dubbo

zookeeper注册中心

zookeeper中的数据结构查看方式:
1.通过zkCli.sh查看,前提是能登录服务器,或者从任意一台具有zkCli.sh访问另一台。
2.通过Java客户端代码访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ZooKeeperClientTest {
public static String CONNSTR = "172.16.36.71:12233";
// public static String PATH = "/";
// public static String PATH = "/dubbo";
// public static String PATH = "/dubbo/com.suneee.scn.system.api.provider.UserProvider";
public static String PATH = "/dubbo/com.suneee.scn.system.api.provider.UserProvider/providers";

public static void main(String[] args) {
try {
ZooKeeper zooKeeper = new ZooKeeper(CONNSTR, 5000, event -> System.out.println("已经触发了" + event.getType() + "事件!"), true);
ZooKeeper.States state = zooKeeper.getState();
System.out.println("state=" + state + " isConnected=" + state.isConnected());
List<String> stringList = zooKeeper.getChildren(PATH, false);
System.out.println("stringList=" + stringList);
byte[] data = zooKeeper.getData(PATH, false, new Stat());
System.out.println(data);
if (data != null) {
System.out.println(new String(data));
}
} catch (IOException | InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}

dubbo在zookeeper注册中心的数据存储结构:
/dubbo/[接口地址1, 接口地址2]/[consumers, configurators, routers, providers]
例:

1
2
3
4
/dubbo
/dubbo/com.suneee.scn.system.api.provider.UserProvider
/dubbo/com.suneee.scn.system.api.provider.UserProvider/consumers
/dubbo/com.suneee.scn.system.api.provider.UserProvider/providers

其中/dubbo/com.suneee.scn.system.api.provider.UserProvider/providers下级即是提供者的注册信息,如下:

1
dubbo%3A%2F%2F172.16.36.67%3A21117%2Fcom.suneee.scn.system.api.provider.UserProvider%3Fanyhost%3Dtrue%26application%3Dsystem-provider%26dubbo%3D1.0.0-SNAPSHOT%26generic%3Dfalse%26interface%3Dcom.suneee.scn.system.api.provider.UserProvider%26methods%3DfindUserInfo%2CfindByRole%2CfindById%2CgetUserByParam%26owner%3DsunEeeSystem%26pid%3D44275%26revision%3D1.0.0-SNAPSHOT%26serialization%3Dkryo%26server%3Dnetty4%26side%3Dprovider%26timestamp%3D1524825191422%26version%3D1.0

将UTF-8转中文后:

1
dubbo://172.16.36.67:21117/com.suneee.scn.system.api.provider.UserProvider?anyhost=true&application=system-provider&dubbo=1.0.0-SNAPSHOT&generic=false&interface=com.suneee.scn.system.api.provider.UserProvider&methods=findUserInfo,findByRole,findById,getUserByParam&owner=sunEeeSystem&pid=44275&revision=1.0.0-SNAPSHOT&serialization=kryo&server=netty4&side=provider&timestamp=1524825191422&version=1.0

分析可知,这些信息包含提供者的IP地址、端口号、注册方法、系列化的实现等等

IDEA使用

发表于 2018-05-25 | 更新于 2019-08-28 | 分类于 工具

IDEA好用的插件

IDEA官方插件库网址
IDEA安装插件后,Settings->Plugins下选择某个插件,可查看功能、快捷键、使用方法介绍。
我常用的插件:

  • Alibaba Java Coding Guidelines
    阿里巴巴代码规约检测
  • Maven Helper
    依赖分析,冲突解决
  • MyBatisCodeHelperPro | MyBatis plugin | Free MyBatis plugin
    都是MyBatis插件,MyBatisCodeHelperPro大部分功能免费,MyBatis plugin收费,Free MyBatis plugin基于MyBatis plugin的免费版本
  • Rainbow Brackets
    彩虹括号
  • Material Theme UI
    Material主题
  • AceJump
    光标在代码中跳转到指定字符位置,按快捷键(Ctrl+;不同版本可能不同)进入AceJump模式后,再按任一个字符,插件就会在屏幕中这个字符的所有出现位置都打上标签,你只要再按一下标签的字符,就能把光标移到该位置上。
    缺点:只能跳转屏幕显示大小的位置。比如,一个文件有1000行,屏幕显示了1-100行,只能跳转到1-100行,无法跳转到100-1000的位置。
  • SequenceDiagram
    根据代码生成时序图
  • Key Promoter X | Key Promoter
    适合新手使用。当你点击鼠标一个功能的时候,可以提示你这个功能快捷键是什么。

PostgreSQL使用总结

发表于 2018-05-15 | 更新于 2019-08-28 | 分类于 数据库 , PostgreSQL

PostgreSQL大小写

  • PostgreSQL数据库对象名大小写敏感
    对象名:如表名、字段名
    PostgreSQL数据库内核是区分大小写的。
    只是为了方便使用,数据库在分析SQL脚本时,对不加双引号的所有对象名转化为小写字母。
    对象名加上双引号是区分大小写的,不加双引号转化为小写字母。
    例如1:
    SELECT * FROM "TUser" WHERE "Name" LIKE '%tony%';
    SELECT * FROM "tUser" WHERE "name" LIKE '%tony%';
    两者是不同的,两个的表名、字段名称区分大小写
    例如2:
    SELECT * FROM TUser WHERE Name LIKE '%tony%';
    SELECT * FROM tUser WHERE name LIKE '%tony%';
    两者是相同的,两个的表名、字段名称会被转换成小写
    例1和例2的区别在于是否加双引号
    注意:表名、字段名只允许双引号,不支持单引号

  • 数据区分大小写
    LIKE ‘%a%’,A是不会出来的
    注意:数据双引号、单引号都可以,建议使用单引号

清空数据库还原数据库为新建时的状态

在postgresql中,创建数据库时会自动创建public模式,一般我们把表都保存在该模式中,因此直接删除该模式再重新创建该模式。
若数据在其他模式中,则把public换为数据表所在模式即可。

1
2
3
4
-- 删除public模式以及模式里面所有的对象
DROP SCHEMA public CASCADE;
-- 创建public模式
CREATE SCHEMA public;

Dubbo异常集锦

发表于 2018-04-28 | 更新于 2019-08-28 | 分类于 Dubbo

消费者调用不到提供者

可能的情况:
1)消费者和提供者配置的zookeeper不是同一个。各自检查各自的zookeeper配置的ip。
2)消费者和提供者之间网络不通,通过dubbo-admin管理平台看提供者ip,然后从消费者机器,ping ip地址,看网络是否通。
3)提供者没有注册到zookeeper,可通过dubbo-admin管理平台看状态。

点对点直连服务提供者地址,将绕过注册中心

我们可以指定调用服务提供者。只需要做如下配置即可:

1
2
@Reference(check = false, url = "dubbo://172.19.24.134:20928/com.suneee.scn.system.api.provider.UserProvider")
private static UserProvider userProvider;

这种配置仅供开发测试使用,上测试环境或生产环境的时候一定要删除。
指定URL的具体IP,可以用作本机debug调试。

消费者调用不到已经注册了的提供者

提供者已经注册到zookeeper的注册中心,但是没有正确引用。
错误提示如下:

1
2
3
4
5
6
7
8
9
10
No provider available in [invoker :interface com.suneee.scn.system.api.provider.UserProvider ->
zookeeper://zookeeper.vr.weilian.cn:12233/com.alibaba.dubbo.registry.RegistryService?application=mcs-rest&check=false&default.check=fal
se&default.reference.filter=default&default.timeout=16000&dubbo=2.8.6&interface=com.suneee.scn.system.api.provider.UserProvider&methods
=findUserInfo,findByRole,getUserInfoAndEnterpriseInfo,findById,getUserListInEnterprise&owner=sunEeemcs&pid=8296&revision=1.0.0-20180424
.015002-6&side=consumer&timestamp=1524879034209,directory: com.alibaba.dubbo.registry.integration.RegistryDirectory@70211df5, invoker
:interface com.suneee.scn.system.api.provider.UserProvider ->
zookeeper://zookeeper.vr.weilian.cn:12233/com.alibaba.dubbo.registry.RegistryService?application=mcs-rest&check=false&default.check=fal
se&default.reference.filter=default&default.timeout=16000&dubbo=2.8.6&interface=com.suneee.scn.system.api.provider.UserProvider&methods
=findUserInfo,findByRole,getUserInfoAndEnterpriseInfo,findById,getUserListInEnterprise&owner=sunEeemcs&pid=8296&revision=1.0.0-20180424
.015002-6&side=consumer&timestamp=1524879034209,directory: com.alibaba.dubbo.registry.integration.RegistryDirectory@38e7ed69]

提供者在zookeeper的注册中心已经注册了,但是提供者加了版本号,提供者代码如下:

1
2
@Service(version="1.0")
public class UserProviderImpl implements UserProvider {

消费者引用代码如下:

1
2
@Reference(check = false)
private UserProvider userProvider;

错误原因:没有加版本号
修改代码如下:

1
2
@Reference(check = false, version = "1.0")
private UserProvider userProvider;

提供者的方法没有注册

消费者有的方法可以调用,有的不能调用,出现:com.alibaba.dubbo.rpc.RpcException: Failed to invoke the method异常,由于提供者新加的方法并没有注册到zookeeper的注册中心导致的。
错误提示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
com.alibaba.dubbo.rpc.RpcException: Failed to invoke the method getUserListInEnterprise in the service
com.suneee.scn.system.api.provider.UserProvider. Tried 3 times of the providers [172.16.36.67:21117] (1/1) from the registry
zookeeper.vr.weilian.cn:12233 on the consumer 172.19.24.134 using the dubbo version 2.8.6. Last error is: Failed to invoke remote
method: getUserListInEnterprise, provider:
dubbo://172.16.36.67:21117/com.suneee.scn.system.api.provider.UserProvider?anyhost=true&application=mcs-rest&check=false&default.check=
false&default.reference.filter=default&default.timeout=16000&dubbo=1.0.0-SNAPSHOT&generic=false&interface=com.suneee.scn.system.api.pro
vider.UserProvider&methods=findUserInfo,findByRole,findById,getUserByParam&owner=sunEeemcs&pid=10828&revision=1.0.0-20180424.015002-6&s
erialization=kryo&server=netty4&side=consumer&timestamp=1524825911114&version=1.0, cause: com.alibaba.dubbo.rpc.RpcException: Failed
to invoke remote proxy method getUserListInEnterprise to
registry://zookeeper.vr.weilian.cn:12233/com.alibaba.dubbo.registry.RegistryService?application=system-provider&dubbo=1.0.0-SNAPSHOT&ex
port=dubbo%3A%2F%2F172.16.36.67%3A21117%2Fcom.suneee.scn.system.api.provider.UserProvider%3Fanyhost%3Dtrue%26application%3Dsystem-provi
der%26dubbo%3D1.0.0-SNAPSHOT%26generic%3Dfalse%26interface%3Dcom.suneee.scn.system.api.provider.UserProvider%26methods%3DfindUserInfo%2
CfindByRole%2CfindById%2CgetUserByParam%26owner%3DsunEeeSystem%26pid%3D44275%26revision%3D1.0.0-SNAPSHOT%26serialization%3Dkryo%26serve
r%3Dnetty4%26side%3Dprovider%26timestamp%3D1524825191422%26version%3D1.0&owner=sunEeeSystem&pid=44275&registry=zookeeper&server=netty4&
timestamp=1524825181399, cause: Not found method "getUserListInEnterprise" in class com.suneee.scn.system.api.provider.UserProvider.
com.alibaba.dubbo.rpc.RpcException: Failed to invoke remote proxy method getUserListInEnterprise to
registry://zookeeper.vr.weilian.cn:12233/com.alibaba.dubbo.registry.RegistryService?application=system-provider&dubbo=1.0.0-SNAPSHOT&ex
port=dubb

如果有提供者的代码,包含了新加方法的代码,本机启动,向zookeeper的注册中心注册新加的方法,消费者就可以调用到新注册的方法,这样只能作为本机调试!最终还是得让提供者重启服务向注册中心注册新加的方法!

Dubbox的kyro系列化不支持JDK8新功能LocalDateTime、LocalDate、LocalTime

Dubbox不同于Dubbo,Dubbox是当当网在Dubbo的基础上进行的扩展,其中一项扩展是支持kyro系列化。
当Consumer调用Provider的时候,参数Bean的属性类型如果包含LocalDateTime、LocalDate、LocalTime将产生超时,需要将这些类型转换成Date。其中Dubbox的版本是2.8.6,希望Dubbox的以后版本能解决这个问题!

Atom使用

发表于 2018-04-27 | 更新于 2019-08-28 | 分类于 工具

Atom常用快捷键

Ctrl-\ | Cmd-\ 显示或隐藏目录树
Ctrl-J | Cmd-J 将下一行与当前行合并
Ctrl-Shift-P | Cmd-Shift-P 打开命令菜单
Ctrl-Up/Down | Ctrl-Cmd-Up/Down 选中行上移、下移
Ctrl-Shift-D | Cmd-Shift-D 复制当前行到下一行
Ctrl-Shift-K | Ctrl-Shift-K 删除当前行
Ctrl-Shift-L | 选择文本类型
Ctrl-Alt-B 插件atom-beautify的快捷键,需要安装,格式化代码
Ctrl-Shift-M 自带插件markdown-preview或插件Markdown Preview Plus的快捷键,Markdown预览

Atom好用的主题(Themes)

  • atom-material-ui
  • atom-material-syntax

Atom好用的插件(Packages)

插件相关使用方法可以查看插件官方介绍,通过File->Setting->Packages,查找相关插件可进入插件官方网址。

  • sync-settings
    Atom的备份神器,可将配置信息自动备份到gist。
    打开命令菜单,sync-settings:check-backup同步gist上配置到本地,sync-settings:backup同步配置到gist。
  • markdown-preview
    自带插件,Markdown预览
  • Markdown Preview Plus
    Markdown预览插件
  • Language Markdown
    Markdown语法支持插件
  • atom-beautify
    格式化代码,快捷键Ctrl-Alt-B
  • linter
    linter是一语法检查插件,它可以识别大部分语法,并对你的语法错误进行纠正。linter只是一个框架,针对不同语言的有不同具体插件。
    linter针对不同语言的包:
    • linter-htmlhint html
    • linter-markdown markdown
    • linter-jsonlint json
    • linter-jshint js
  • platformio-ide-terminal
    控制台,底部状态栏或者快捷键ctrl-`打开
  • git-plus
    管理git项目,集成各种常用git功能,Ctrl-Shift-H | Cmd-Shift-H打开命令面板操作
  • minimap
    代码导航,方便美观的缩略滚动图
  • autocomplete-paths
    自动补全文件路径
12
wangxuan

wangxuan

诗意生活,如诗如画!

19 日志
14 分类
13 标签
GitHub E-Mail
© 2019 wangxuan
由 Hexo 强力驱动 v3.9.0
|
主题 – NexT.Gemini v6.7.0