Java 帝国之消息队列
张家村的历史
Java 帝国的张家村正在迎来一次重大的变革。
5年前网上购物兴起的时候, 帝国非常看好, 决定向这个领域进军, 于是兴建了张家村, 在这里安装了Java 虚拟机和数据库, 然后部署了一个基于Web的订单系统和一个库存系统, 由张家村的人负责操作。
张家村的老村长很清楚, 说是两个系统, 其实是逻辑上的一种划分方式, 在物理上两个系统还是部署在一个虚拟机中。 那个时候用户量少, 数据量也不大, 村民们只要使用这一个Java 虚拟机和数据库就足够了。
订单和库存系统运转一直很好, 用户的订单来了, 会在订单系统中存下来, 然后通知库存系统发货。
你要问怎么通知? 老村长会告诉你说: 其实简单的很, 就是普通Java 方法的直接调用, 调用库存系统某个类的某个方法而已。 由于在同一个虚拟机之内, 效率极高 。
转眼间5年过去了, 人类变得越来越懒, 越来越喜欢网上购物, 用户量和数据量暴增, 再加上他们时不时搞个什么秒杀活动, 更是让张家村变得不堪重负。 为了应付汹涌而来的订单, 张家村经常彻夜灯火通明, 全村人三班倒才能勉强应付。
老村长向镇上报告了很多次都没有回音, 要不是帝国的性能监控部门发现了这个异常,还不知道要被瞒到什么时候。
还好,钦差王大人来了, 变革要开始了。
拆分
王大人经验丰富, 目光如炬, 一眼就看出了问题, 立刻下达了第一道命令: 拆分 ! 把订单系统和库存系统分开!
老村长说: “那拆开了以后,我们村还是放不下啊”
王大人道: “订单系统还是留在张家村, 库存系统挪到李家庄去”
老村长暗自思忖,订单系统是直接面向用户的,很重要, 张家村一定要保住, 至于库存系统,主要是后台操作,挪走就挪走吧, 于是就答应了。
拆分不是那么容易的, 订单和库存耦合的比较紧密, 现在要把库存系统搬到李家庄的Java虚拟机和数据库去,免不了一番剧烈的折腾。
在王大人的指导下, 原先那些直接的Java 方法调用,也被改成了Web 服务 -- 张家村和李家庄虽然距离较远 , 还是有网络相通的。
现在用户下了订单以后, 先在张家村保存, 然后由张家村调用李家庄的Web服务来通知库存系统。
数据库自然也做了拆分, 老的库存数据被导出来, 再导入到李家庄的新数据库中 。
不管过程是多么艰难, 两个系统还是分了家,李家庄喜气洋洋、敲锣打鼓的迎接了库存系统的部署, 开始了试运营。
新问题
由于只需要处理订单, 张家村的负载一下子降了下来, 恢复了正常的作息。
可是好景不长,张家村很快发现李家庄对新系统根本不上心, 派了一个酒鬼小李去负责操作库存Web服务, 小李喝醉了啥都忘了, Web服务经常用不了。
这还不算,李家庄是严格的日出而作, 日落而息, 张家村正在繁忙的处理订单的时候, 李家庄已经把系统关了,睡觉去了。
这可苦了张家村的小张,用户提交了订单, 去调用Web服务通知库存发货, 可是Web服务经常不响应, 小张没有办法, 只好反复重试、等待一段时间后再试,导致一个订单很长时间才能完成, 用户体验极差, 大家怨声载道。
小张向李家庄投诉了很多次都不管用,没人搭理,跨村庄协作可真是难啊!
小张活干的不好,工分减少, 再这么下去,月底分粮的时候又要饿肚子了,小张想起来了自己经历过的《Java帝国之拨云见日识回调 》, 心里再次哀叹: 为什么受伤的总是我?
晚上到家,小张苦思悯想: 原来订单系统和库存系统都在一个虚拟机中, 处理起来很方便, 但是现在是个远程的Web服务, 酒鬼小李不给我返回结果, 我就没法结束, 这是典型的同步操作, 能不能改成异步的呢?
我把一个订单包裹发给小李, 他什么时候处理我就不用管了, 这样我这边的效率就会大大的提高! 可是现在的Web服务并不支持这种方式, 我怎么才能把包裹发过去呢?
消息队列
小张彻夜未眠, 第二天一大早就去请教老村长。
村长说: “你能想到这一层,非常不错! 近来我也一直在考虑这个问题啊, 在大型的分布式系统中,怎么做异步通信是个大问题, 我想到了一个叫消息队列的东西”
“消息队列? 没有听说过”
“就拿你遇到的情况来说吧, 我们开发一个消息队列,名称我都想好了,就叫ZhangMQ(Zhang Message Queue), 把它部署在我们张家村和李家庄之间, 我们村产生的订单, 你只要负责把订单消息写到这个队列里就完事大吉了。 ”
“奥,李家庄的酒鬼小李醒了,就让他从这个消息队列中读取消息,进行处理,对吧”
“没错,这不就变成异步的了? 酒鬼小李不处理, 那就是他们的责任了”
"那订单消息的格式需要和李家庄商量好, 另外次序也不能乱掉, 还有, 要是断电了或者重启了,消息队列中的订单消息也不能丢失 " 小张想的很深入
“没错,这些都是我们的ZhangMQ要考虑的,要实现持久化, 把订单消息存到硬盘上”
“这真是不错“ 小张跃跃欲试 ”村长,我想去开发这个ZhangMQ, 一定要让我参加啊“
”没问题, 不过我当前最重要的是说服李家庄, 让他们来采用我们的消息队列“
经过据理力争和艰难协商,李家庄终于同意了消息队列的方案,毕竟对他们也没啥损失, 也不用听张家村没完没了的投诉了,只需要改一点点代码,从消息队列中读取订单即可。
小张和其他人在家里埋头开发ZhangMQ, 半年后,ZhangMQ正式上线, 彻底解决了异步通信的问题。
前钦差王大人对张家村做了回访,发现了消息队列,赞不绝口,回去后就发来了褒奖令,还下令在帝国推广, ZhangMQ一下子出名了!
Java帝国之JMS的诞生
1
背景
本文续上篇《Java 帝国之消息队列》
自从张家村的ZhangMQ问世以来,大家都看到了消息队列在分布式系统中的巨大好处,纷纷另起炉灶搞一套自己的消息队列,各种MQ产品如雨后春笋班出现,各家都疯狂的宣传自己的宝贝。
为了吸引程序猿来使用, 各家八仙过海,各显神通,定义了各式各样的API, 由于是独立发展,这些API协议多样,互不兼容, 学习成本高,使用起来非常不方便。
这是帝国所不能容忍的 !
其实Java 帝国非常擅长搞出标准的协议和接口, 之前的JDBC就是一个典型的例子(参见文章《JDBC的诞生》), 制定了协议以后, 让各个产品厂商去实现, 实现了针对数据库编程的统一接口。
既然数据库可以这么干, 消息队列肯定也没问题!
由于张家村开发了第一个消息队列产品, 帝国把制定标准接口的光荣使命交给了张家村。
2
消息队列接口设计
张家村经验丰富的老村长又把任务分给了小张, 告诉他我们要做的是一个厂商独立的标准接口, 让他先去调研一下时下流行的MQ的现状。
小张先找到了某大厂著名MQ, 它占据了企业级市场不少份额, 但是直接使用它的 Java API 编程的话就不那么容易了, 大家可以快速浏览下:
小张能看的出这是在发送一个消息,但这MQEnvironment, openOptions,MQPutMessageOptions 看起来让小张心烦,特别是还得理解Queue Manager这样的概念,有点不容易。
小张又找了一个以开源吸引人的RabbitMQ , 这个看起来清爽多了:
但是这queueDeclare方法 和 basicPublish 方法小张总觉得的不爽。
只看了两个消息队列, 小张就不想再看了, 他去找村长说: 这差别也太大了,根本无法统一。
村长说:”不要被纷繁的现象迷住了双眼, 要看透背后的本质, 做出适当的抽象才可以。“
又是抽象! 小张暗自叹气, 这抽象实在是太难了。
(参见文章:《 抽象,程序员必备的能力 》 )
”你深入思考下“ 村长看出了小张的困难, 鼓励他说: ”其实也没那么难, 我们先搞出几个最基本的概念, 记不记得操作系统中学过的生产者-消费者模型? 我们完全可以应用到这里来啊, 消息生产者(Message Producer), 消息消费者 (Messge Consumer) , 生产者提供发送消息的方法, 消费者提供接收消息的方法, 如果加上消息队列 (Message Queue) 的话就是这样:“
小张说:”这也太抽象了吧, 我看人家还有什么Queue Manager, Connection ,Channel 之类的“
村长说: ”别急啊, 你看不管是生产者向队列发送消息,还是消费者去接收消息, 其实都是在和消息队列进行交互, 所以我们再引入一个会话(Session)的概念出来 。“
”奥, 我有点明白了 ,Session 可以创建消息, 还可以引入事务的支持呢“ 小张思维敏捷
“不错, 其实消息生产者/消费者也应该由Session来创建,因为他们要发送/接收消息肯定是在一个会话中, 另外你想想, Session对象由谁来创建?”
小张说: “应该是Connection ” 说着小张画了一张图:
“你看这概念不就出来了,是不是很简单? ” 村长笑着说。
小张挠挠头说: “会者不难,难者不会啊, 对了,我们还缺乏最关键的连接参数(ip地址,端口等)还有队列的名称之类的信息。 这些信息怎么办?”
“这确实有点复杂,各个厂商的具体情况差别太大。” 村长也表示犯难 ,“你让我想想, 下午再聊。”
3
配置和代码的分离
小张中午吃饭的时候也在想, 这些复杂的配置参数该怎么办, 要是都让程序员在代码里写,那就太丑陋了吧, 因为不同的MQ产品,配置都不一样啊。
下午的时候,看到村长一副喜气洋洋的表情, 小张知道问题解决了。
村长说: “我想到了一个办法, 一个很简单,但是有效的办法。”
小张说:“别卖关子了,快说吧”
”其实也是又老又俗的办法了, 这个办法就是把配置和代码分开, 你不是说这些连接参数很复杂,各个厂商不同吗? 那就作为配置信息把它放到Web容器里,对外只提供一个简单的ConnectionFactory的接口,由这个ConnectionFactory来创建Connection, 当然了各个厂商必须实现这个ConnectionFactory“
"那怎么才能得到这个ConnectionFactory ?"
"这就简单了, 对程序员来讲,通过JNDI 就可以轻松拿到了, 例如:"
”这办法不错,把细节都隐藏起来了, 既然ConnectionFactory可以这么搞, 队列(Queue)的配置信息也可以这么办啊。“
村长说:”所以ConnectionFactory, Queue 就是隔离细节的抽象层。”
4
再次抽象
标准接口初具模型,小张很高兴,晚上请喜欢的张二妮吃饭, 忍不住得瑟了一下。
张二妮说:“你们两个老土,定义的标准接口,都已经过时了!”
小张很生气: “怎么可能呢?”
二妮说:“告诉你们吧, 你们搞的这个叫Point to Point模型,就是一个发送方,对应一个接收方, 现在外边有很多人在用 发布/订阅 的模型,你们知道不? ”
“一个客户端(Client1)对一个Topic发布了消息, 很多订阅了这个Topic的客户端(Client2, Client3) 都可以接收到这个消息的副本。”
小张呆住了, 这和以前ZhangMQ的方式完全不同, 队列都不见了, 引入了一个新的主题(Topic)的概念。
第二天, 小张赶紧去找村长, 告诉他发生了新情况。
村长说: “你呀,还是太年轻, 慌什么,深入思考一下, 这个发布/订阅的本质和我们之前的生产者/消费者没什么不同。 ”
小张说: “那人家还有Topic的概念呢。”
“我们可以把Topic和Queue 变成一个更抽象的概念,他们都是消息的目的地, 嗯, 就叫做Destination吧,这个Destination的细节也是需要配置出来的, 通过JNDI来获取。”
“那订阅怎么处理?”
村长说: “原来我们定义的是MessageConsumer, 现在增加一个新概念叫做 TopicSubscriber , 可以从Destination获取消息,这不就行了, 其实从本质上来讲Subscriber也是消息消费者的一种而已。”
“那怎么才能实现订阅的功能呢?”
“别忘了, 我们只定义接口行为, 具体的实现需要由各个产品来负责!”
小张看着这幅图, 深感抽象的威力巨大, 这么多的细节最后变成了这几个简单的概念!
小张还特意写了一段代码,展示上面的概念:
张家村把这个设计交了上去, 帝国很满意,把它起名为Java Message Service (JMS), 随后强制各大产品实现JMS, 否则就不颁发进京证, 没这个证别想在帝国做生意!
JMS由于设计良好,概念清晰,其实不用怎么强制,很快就流行开了,成为了Java 帝国的事实标准。
后记: 其实JMS规范的制定是在Sun的领导下,各大厂商密切参与完成的:
There were a number of MOM vendors that participated in the creation of JMS. It was an industry effort rather than a Sun effort. Sun was the spec lead and did shepherd the work but it would not have been successful without the direct involvement of the messaging vendors. Although our original objective was to provide a Java API for connectivity to MOM systems, this changed over the course of the work to a broader objective of supporting messaging as a first class Java distributed computing paradigm on equal footing with RPC.
- Mark Hapner, JMS spec lead, Sun Microsystems
(完)
微信公众号【Java技术江湖】一位阿里 Java 工程师的技术小站。(关注公众号后回复”Java“即可领取 Java基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的Java学习指南、Java程序员面试指南等干货资源)