开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

一个开源的 Java 我的世界 (Minecraft) 服务

发表于 2017-11-24

Built with Love Join the Discord chat
Build Status

Glowstone logo

A fast, customizable and compatible open source server for Minecraft: Java Edition.

Introduction

Glowstone is a lightweight, from scratch, open source Minecraft server written in Java that supports plugins written for the Bukkit API and its major forks, Spigot and Paper.

The main goals of the project are to provide a lightweight implementation of the Bukkit API and Minecraft server where exact vanilla functionality is not needed or higher performance is desired than the official software can deliver. Glowstone
makes use of a thread-per-world model and performs synchronization only when necessitated by the Bukkit API.

Still have questions? Check out our FAQ.

Features

Glowstone has a few key advantages over CraftBukkit:

  • It is 100% open source. While CraftBukkit and most other mods are open source, they rely on decompiled Minecraft source code. Glowstone’s code is completely original.
  • Because of this, it is easy to contribute to Glowstone’s development. The barrier of entry to contributions is lower because there is no need to work around decompiled source or maintain a minimal diff.
  • Glowstone supports all plugins written for the Bukkit, Spigot and Paper APIs natively. In practice, some plugins may try to make use of parts of the API which are not yet implemented, but in a completed state Glowstone would support all Bukkit
    plugins.
  • Glowstone’s simplicity affords it a performance improvement over CraftBukkit and other servers, making it especially suited for situations where a large amount of players must be supported but vanilla game features are not needed.

However, there are several drawbacks:

  • Glowstone is not finished. Nothing is guaranteed to work, though many things are likely to. If in doubt, file an issue.
  • Bukkit plugins which expect the presence of CraftBukkit-specific code (that are in the org.bukkit.craftbukkit or net.minecraft.server packages) will not work on Glowstone unless they are designed to fail gracefully.
  • Glowstone is not produced by the Bukkit team, and while we do make an effort to produce quality work, Glowstone does not undergo the same rigorious testing as the Bukkit project.

For a current list of features, check the wiki.

Downloads

Build Status

If you don’t want to build from source, pre-built jar files are available to download from CircleCI - click the latest build and then open the “Artifacts”
tab (you must be logged in for this to show). The glowstone.jar artifact will be under tmp/circle-artifacts.#######/.

Building

1. Setup

After installing Oracle JDK (recommended) or OpenJDK, and Maven, checkout the source:

1
2
复制代码git clone https://github.com/GlowstoneMC/Glowstone
cd Glowstone

2. Build

1
复制代码./setup.sh

The final jar will be placed in target/ named glowstone.jar.

Running

Running Glowstone is simple because its dependencies are shaded into the output jar at compile time. Simply execute java -jar glowstone.jar along with any extra JVM options desired (we recommend using java -Xms1G -Xmx1G -XX:+UseG1GC -jar glowstone.jar).
A variety of command-line options are also available - run java -jar glowstone.jar --help for more information.

By default, configuration is stored in the config/ subdirectory and logs are stored in the logs/ subdirectory. The main configuration file is config/glowstone.yml, which replaces CraftBukkit’s server.properties and bukkit.yml. Settings from these two files will be copied over to Glowstone’s configuration during the default configuration generation process.

Glowstone uses JLine for console input and colored console output. The JLine console can be disabled in the configuration if a flat console is desired.

Need more help? Check out our wiki for some guides that will help you with running, maintaining and configuring your Glowstone server.

Playing

For those of you who just want to play on a Glowstone server, we have one available for testing at mc.glowstone.net. Have fun!

Docs and Support

The best place to receive support is on GitHub issues. When reporting bugs, please retest and include whether the problem reproduces on:

  • Earlier builds of Glowstone
  • Glowstone Legacy, if applicable

Javadocs for Glowstone can be found here.

For documentation on the Glowkit API (an updated Bukkit for Glowstone, based on the Paper API, compatible with Spigot’s update to Bukkit), see the Glowkit Javadocs.

Contributing

First of all, thank you for your interest in advancing Glowstone! We always love to see new developers work on the project! You can find all of our resources on how to get started on our wiki.

Credits

  • The Minecraft Coalition and #mcdevs - protocol and file formats research.
  • The Bukkit team for their outstandingly well-designed plugin API.
  • The SpigotMC team for updating and enhancing the Bukkit plugin API.
  • AquiferMC for further enhancing the Bukkit API.
  • The SpongePowered Team for creating the Sponge API.
  • Trustin Lee - author of the Netty library.
  • Graham Edgecombe - author of the original Lightstone.
  • Tad Hardesty and all the contributors to Glowstone Legacy.
  • All the people behind Maven and Java.
  • Notch and Mojang - for making such an awesome game in the first place!

Copyright

Glowstone is open-source software released under the MIT license. Please see the LICENSE file for details.

Glowkit is open-source software released under the GPL license. Please see the LICENSE.txt file in the Glowkit repository for details.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

分析型数据仓库中读写分离的实现

发表于 2017-11-24

本文作者为神策数据资深研发工程师张广强,版权归神策数据所有。

和以 MySQL
为代表的传统事务型数据库相比,数据仓库有一个很大的特点,就是主要面向批量写和查询进行优化,可以不支持更新、事务这些高级特性。一些商用的数据仓库分析系统,例如 Vertica
,已经可以做到千亿级数据的秒级导入和秒级查询。

神策数据一直致力于帮助企业搭建数据仓库,实现数据的秒级响应,积累数据资产。本文主要通过神策数据在技术上的探索与实践,探讨如何利用现有的开源组件实现分析型数据仓库当中的读写分离。

为什么要进行读写分离

分析性数据仓库一般有如下几个特点:

  • (1
    )面临着复杂的多维分析需求,能够进行任意维度的上卷下钻。
  • (2
    )存储的数据维度一般较多,所以是宽表,而且一般比较稀疏。
  • (3
    )数据量比较大,一次写入,多次查询。

针对这样特点,分析性数据库一般选择列存储数据格式,例如 Parquet
等。优点是对于统计分析效率很高,而且对于稀疏的宽表具有很高的存储压缩比。所以我们可以认为列存储格式是一种面向读进行优化的存储格式,我们称为 ReadOptimized Store
(ROS
)。

但是列存储格式也有一个缺点:这种格式的数据一旦生成,就很难进行修改,也很难往已有的数据文件当中插入新数据,只能增加新的数据文件。像 MySQL
这种传统的数据库,使用的行存储文件格式是一种适合修改和插入的存储格式,我们可以认为这种行存储格式是面向写进行优化的存储格式,称为 WriteOptimized Store
(WOS
)。

综上所诉,要实现一个可以秒级导入、秒级查询的分析型数据库,如果只选用 ROS
,则很难支持大数据量的秒级导入。如果只选用 WOS
,则很难实现任意维度的秒级查询,所以我们需要进行读写分离。

读写分离的实现原理

数据仓库当中需要同时存在 WOS
和 ROS
,这样对于所有的写操作我们都生成 WOS
型文件;同时所有的读操作,则主要依赖于 ROS
文件,但也要查询少量的 WOS
文件。整体示意图如下:

图1
读写分离原理图

如图所示,WOS
文件需要定期转换为 ROS
文件,同时因为 ROS
在数据仓库当中一般是分为多个 Partition
存在,所以一个 WOS
可能转化为多个 ROS
。转化的过程需要是原子操作,因为对上层查询引擎来说,同一时刻,同样的数据只能有一份。

开源方案的操作

前面简单介绍了读写分离方案的原理,具体的工程实践过程中,神策数据的工程师还面临着很多方案的选择和实践难点。下面简单介绍一下神策数据在搭建数据仓库的实践中啃过的“硬骨头”。

ROS
的选择比较简单,我们的工程师选择了 Parquet+ Impala
的查询方案,同时结合我们的业务特点做了很多代码级别的优化。(相关链接:
付力力:
基于
Impala
构建实时用户行为分析引擎

)WOS
的选择可能会比较多,我们可以选择常用的 HDFS
行存储文件格式,例如 TextFile
、SequenceFile
、Avro
等。

以 SequenceFile
为例,我们在定义自己的 Impala
表的时候,可以指定一个特殊的 Partition
文件的存储格式为 SequenceFile
,同时其它的 Partition
作为正常的按照日期 Partition
的数据,指定格式为 Parquet
,这种方式的优势体现在始终只有一个表。

后来基于查询效率和未来架构升级方面的考虑,我们最终选择了 Kudu
作为 WOS
,架构实现示意图如下:

图2
读写分离的实现图

如图所示,我们会建立三张物理表,其中两张 Kudu
表作为 WOS
,一张 Parquet
表作为 ROS
。所有的写操作都会写入到 Ingesting
状态的 Kudu
表中,当 Ingesting
表写到一定大小之后,会自动转换为 Staging
状态。

这时我们一方面生成一张新的 Kudu
表作为 Ingesting
表,另一方面开始 WOS
到 ROS
的转换,通过一个叫做 Mover
的任务执行这个操作。将 Staging
状态的 Kudu
表中的数据全部转换到对应 Partition
的 Parquet
表当中。

Staging
状态的表转换完成且 Ingesting
状态的表写满时,会触发一个切表操作,需要更新元数据,告诉 Impala
使用新的数据进行查询,整个切表的操作是原子的。而且已经转化的 Staging
表还需要保留一段时间,避免切表之前发起的查询操作没有及时执行完成。

对于查询请求来说我们会建立一个包含 Staging
表、Ingesting
表和 ROS
表的虚拟表,即一个 View
。用户的查询始终指向一个 View
,但是下面的物理表会经常发生变化。这样就兼顾查询数据的不断更新及查询性能的优化两方面了。

在实现的过程中还有很多具体的工作,例如如何对表进行加列操作,保证各个表的结构一致;Parquet
表中碎文件较多影响查询效率,如何定期合并等。限于篇幅,这里不再具体介绍。神策数据最终的技术架构如下图:

图3
神策数据技术架构图

综上所述,神策数据为了实现数据驱动,在数据仓库的读写效率方面做了比较深入的探索,也参考了众多优秀的开源项目,做了适配产品的优化,累计十万行代码以上,大数据行业技术才是企业的核心竞争力,也希望大家在技术和业务层面进行开放性的探讨。

如果您对更多大数据干货有兴趣,欢迎关注神策数据(ID:SensorsDataCrop)。

神策.jpg

原 文:神策数据

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】Java中9个处理Exception的最佳实践

发表于 2017-11-24

在Java中处理异常并不是一个简单的事情。不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范对异常的处理的原因。而团队之间的这些规范往往是截然不同的。

本文给出几个被很多团队使用的异常处理最佳实践。

  1. 在Finally块中清理资源或者使用try-with-resource语句

当使用类似InputStream这种需要使用后关闭的资源时,一个常见的错误就是在try块的最后关闭资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
// do NOT do this
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}

上述代码在没有任何exception的时候运行是没有问题的。但是当try块中的语句抛出异常或者自己实现的代码抛出异常,那么就不会执行最后的关闭语句,从而资源也无法释放。

合理的做法则是将所有清理的代码都放到finally块中或者使用try-with-resource语句。

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
复制代码public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}

public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
  1. 指定具体的异常

尽可能的使用最具体的异常来声明方法,这样才能使得代码更容易理解。

1
2
3
4
5
6
复制代码public void doNotDoThis() throws Exception {
...
}
public void doThis() throws NumberFormatException {
...
}

如上,NumberFormatException字面上即可以看出是数字格式化错误。

  1. 对异常进行文档说明

当在方法上声明抛出异常时,也需要进行文档说明。和前面的一点一样,都是为了给调用者提供尽可能多的信息,从而可以更好地避免/处理异常。

在Javadoc中加入throws声明,并且描述抛出异常的场景。

1
2
3
4
5
6
7
8
9
复制代码/**
* This method does something extremely useful ...
*
* @param input
* @throws MyBusinessException if ... happens
*/
public void doSomething(String input) throws MyBusinessException {
...
}
  1. 抛出异常的时候包含描述信息

在抛出异常时,需要尽可能精确地描述问题和相关信息,这样无论是打印到日志中还是监控工具中,都能够更容易被人阅读,从而可以更好地定位具体错误信息、错误的严重程度等。

但这里并不是说要对错误信息长篇大论,因为本来Exception的类名就能够反映错误的原因,因此只需要用一到两句话描述即可。

1
2
3
4
5
复制代码try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
}

NumberFormatException即告诉了这个异常是格式化错误,异常的额外信息只需要提供这个错误字符串即可。当异常的名称不够明显的时候,则需要提供尽可能具体的错误信息。

  1. 首先捕获最具体的异常

现在很多IDE都能智能提示这个最佳实践,当你试图首先捕获最笼统的异常时,会提示不能达到的代码。

当有多个catch块中,按照捕获顺序只有第一个匹配到的catch块才能执行。因此,如果先捕获IllegalArgumentException,那么则无法运行到对NumberFormatException的捕获。

1
2
3
4
5
6
7
8
9
复制代码public void catchMostSpecificExceptionFirst() {
try {
doSomething("A message");
} catch (NumberFormatException e) {
log.error(e);
} catch (IllegalArgumentException e) {
log.error(e)
}
}
  1. 不要捕获Throwable

Throwable是所有异常和错误的父类。你可以在catch语句中捕获,但是永远不要这么做。

如果catch了throwable,那么不仅仅会捕获所有exception,还会捕获error。而error是表明无法恢复的jvm错误。因此除非绝对肯定能够处理或者被要求处理error,不要捕获throwable。

1
2
3
4
5
6
7
复制代码public void doNotCatchThrowable() {
try {
// do something
} catch (Throwable t) {
// don't do this!
}
}
  1. 不要忽略异常

很多时候,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或者记录日志。

1
2
3
4
5
6
7
8
复制代码
public void doNotIgnoreExceptions() {
try {
// do something
} catch (NumberFormatException e) {
// this will never happen
}
}

但现实是经常会出现无法预料的异常或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码),而此时由于异常被捕获,使得无法拿到足够的错误信息来定位问题。

合理的做法是至少要记录异常的信息。

1
2
3
4
5
6
7
复制代码public void logAnException() {
try {
// do something
} catch (NumberFormatException e) {
log.error("This should never happen: " + e);
}
}
  1. 不要记录并抛出异常

可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下:

1
2
3
4
5
6
复制代码try {
new Long("xyz");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}

这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下:

1
2
3
4
5
6
7
复制代码17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"
Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)

如上所示,后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息,那么可以将异常包装为自定义异常。

1
2
3
4
5
6
7
8
复制代码
public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}

因此,仅仅当想要处理异常时才去捕获,否则只需要在方法签名中声明让调用者去处理。

  1. 包装异常时不要抛弃原始的异常

捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。

需要注意的是,包装异常时,一定要把原始的异常设置为cause(Exception有构造方法可以传入cause)。否则,丢失了原始的异常信息会让错误的分析变得困难。

1
2
3
4
5
6
7
复制代码public void wrapException(String input) throws MyBusinessException {
try {
// do something
} catch (NumberFormatException e) {
throw new MyBusinessException("A message that describes the error.", e);
}
}

总结

综上可知,当抛出或者捕获异常时,有很多不一样的东西需要考虑。其中的许多点都是为了提升代码的可阅读性或者api的可用性。

异常不仅仅是一个错误控制机制,也是一个沟通媒介,因此与你的协作者讨论这些最佳实践并制定一些规范能够让每个人都理解相关的通用概念并且能够按照同样的方式使用它们。

原文链接: dzone.com/articles/9-…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot与RabbitMQ结合实现延迟队列

发表于 2017-11-24

背景

何为延迟队列?

顾名思义,延迟队列就是进入该队列的消息会被延迟消费的队列。而一般的队列,消息一旦入队了之后就会被消费者马上消费。

延迟队列能做什么?

延迟队列多用于需要延迟工作的场景。最常见的是以下两种场景:

  1. 延迟消费。比如:
    • 用户生成订单之后,需要过一段时间校验订单的支付状态,如果订单仍未支付则需要及时地关闭订单。
    • 用户注册成功之后,需要过一段时间比如一周后校验用户的使用情况,如果发现用户活跃度较低,则发送邮件或者短信来提醒用户使用。
  2. 延迟重试。比如消费者从队列里消费消息时失败了,但是想要延迟一段时间后自动重试。

如果不使用延迟队列,那么我们只能通过一个轮询扫描程序去完成。这种方案既不优雅,也不方便做成统一的服务便于开发人员使用。但是使用延迟队列的话,我们就可以轻而易举地完成。

如何实现?

别急,在下文中,我们将详细介绍如何利用Spring Boot加RabbitMQ来实现延迟队列。

本文出现的示例代码都已push到Github仓库中:github.com/Lovelcp/blo…

实现思路

在介绍具体的实现思路之前,我们先来介绍一下RabbitMQ的两个特性,一个是Time-To-Live Extensions,另一个是Dead Letter Exchanges。

Time-To-Live Extensions

RabbitMQ允许我们为消息或者队列设置TTL(time to live),也就是过期时间。TTL表明了一条消息可在队列中存活的最大时间,单位为毫秒。也就是说,当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在经过TTL秒后“死亡”,成为Dead Letter。如果既配置了消息的TTL,又配置了队列的TTL,那么较小的那个值会被取用。更多资料请查阅官方文档。

Dead Letter Exchange

刚才提到了,被设置了TTL的消息在过期后会成为Dead Letter。其实在RabbitMQ中,一共有三种消息的“死亡”形式:

  1. 消息被拒绝。通过调用basic.reject或者basic.nack并且设置的requeue参数为false。
  2. 消息因为设置了TTL而过期。
  3. 消息进入了一条已经达到最大长度的队列。

如果队列设置了Dead Letter Exchange(DLX),那么这些Dead Letter就会被重新publish到Dead Letter Exchange,通过Dead Letter Exchange路由到其他队列。更多资料请查阅官方文档。

流程图

聪明的你肯定已经想到了,如何将RabbitMQ的TTL和DLX特性结合在一起,实现一个延迟队列。

针对于上述的延迟队列的两个场景,我们分别有以下两种流程图:

延迟消费

延迟消费是延迟队列最为常用的使用模式。如下图所示,生产者产生的消息首先会进入缓冲队列(图中红色队列)。通过RabbitMQ提供的TTL扩展,这些消息会被设置过期时间,也就是延迟消费的时间。等消息过期之后,这些消息会通过配置好的DLX转发到实际消费队列(图中蓝色队列),以此达到延迟消费的效果。

延迟重试

延迟重试本质上也是延迟消费的一种,但是这种模式的结构与普通的延迟消费的流程图较为不同,所以单独拎出来介绍。

如下图所示,消费者发现该消息处理出现了异常,比如是因为网络波动引起的异常。那么如果不等待一段时间,直接就重试的话,很可能会导致在这期间内一直无法成功,造成一定的资源浪费。那么我们可以将其先放在缓冲队列中(图中红色队列),等消息经过一段的延迟时间后再次进入实际消费队列中(图中蓝色队列),此时由于已经过了“较长”的时间了,异常的一些波动通常已经恢复,这些消息可以被正常地消费。

代码实现

接下来我们将介绍如何在Spring Boot中实现基于RabbitMQ的延迟队列。我们假设读者已经拥有了Spring Boot与RabbitMQ的基本知识。如果想快速了解Spring Boot的相关基础知识,可以参考我之前写的一篇文章。

初始化工程

首先我们在Intellij中创建一个Spring Boot工程,并且添加spring-boot-starter-amqp扩展。

配置队列

从上述的流程图中我们可以看到,一个延迟队列的实现,需要一个缓冲队列以及一个实际的消费队列。又由于在RabbitMQ中,我们拥有两种消息过期的配置方式,所以在代码中,我们一共配置了三条队列:

  • delay_queue_per_message_ttl:TTL配置在消息上的缓冲队列。
  • delay_queue_per_queue_ttl:TTL配置在队列上的缓冲队列。
  • delay_process_queue:实际消费队列。

我们通过Java Config的方式将上述的队列配置为Bean。由于我们添加了spring-boot-starter-amqp扩展,Spring Boot在启动时会根据我们的配置自动创建这些队列。为了方便接下来的测试,我们将delay_queue_per_message_ttl以及delay_queue_per_queue_ttl的DLX配置为同一个,且过期的消息都会通过DLX转发到delay_process_queue。

delay_queue_per_message_ttl

首先介绍delay_queue_per_message_ttl的配置代码:

1
2
3
4
5
6
7
复制代码@Bean
Queue delayQueuePerMessageTTL() {
return QueueBuilder.durable(DELAY_QUEUE_PER_MESSAGE_TTL_NAME)
.withArgument("x-dead-letter-exchange", DELAY_EXCHANGE_NAME) // DLX,dead letter发送到的exchange
.withArgument("x-dead-letter-routing-key", DELAY_PROCESS_QUEUE_NAME) // dead letter携带的routing key
.build();
}

其中,x-dead-letter-exchange声明了队列里的死信转发到的DLX名称,x-dead-letter-routing-key声明了这些死信在转发时携带的routing-key名称。

delay_queue_per_queue_ttl

类似地,delay_queue_per_queue_ttl的配置代码:

1
2
3
4
5
6
7
8
复制代码@Bean
Queue delayQueuePerQueueTTL() {
return QueueBuilder.durable(DELAY_QUEUE_PER_QUEUE_TTL_NAME)
.withArgument("x-dead-letter-exchange", DELAY_EXCHANGE_NAME) // DLX
.withArgument("x-dead-letter-routing-key", DELAY_PROCESS_QUEUE_NAME) // dead letter携带的routing key
.withArgument("x-message-ttl", QUEUE_EXPIRATION) // 设置队列的过期时间
.build();
}

delay_queue_per_queue_ttl队列的配置比delay_queue_per_message_ttl队列的配置多了一个x-message-ttl,该配置用来设置队列的过期时间。

delay_process_queue

delay_process_queue的配置最为简单:

1
2
3
4
5
复制代码@Bean
Queue delayProcessQueue() {
return QueueBuilder.durable(DELAY_PROCESS_QUEUE_NAME)
.build();
}

配置Exchange

配置DLX

首先,我们需要配置DLX,代码如下:

1
2
3
4
复制代码@Bean
DirectExchange delayExchange() {
return new DirectExchange(DELAY_EXCHANGE_NAME);
}

然后再将该DLX绑定到实际消费队列即delay_process_queue上。这样所有的死信都会通过DLX被转发到delay_process_queue:

1
2
3
4
5
6
复制代码@Bean
Binding dlxBinding(Queue delayProcessQueue, DirectExchange delayExchange) {
return BindingBuilder.bind(delayProcessQueue)
.to(delayExchange)
.with(DELAY_PROCESS_QUEUE_NAME);
}

配置延迟重试所需的Exchange

从延迟重试的流程图中我们可以看到,消息处理失败之后,我们需要将消息转发到缓冲队列,所以缓冲队列也需要绑定一个Exchange。在本例中,我们将delay_process_per_queue_ttl作为延迟重试里的缓冲队列。具体代码是如何配置的,这里就不赘述了,大家可以查阅我Github中的代码。

定义消费者

我们创建一个最简单的消费者ProcessReceiver,这个消费者监听delay_process_queue队列,对于接受到的消息,他会:

  • 如果消息里的消息体不等于FAIL_MESSAGE,那么他会输出消息体。
  • 如果消息里的消息体恰好是FAIL_MESSAGE,那么他会模拟抛出异常,然后将该消息重定向到缓冲队列(对应延迟重试场景)。

另外,我们还需要新建一个监听容器用于存放消费者,代码如下:

1
2
3
4
5
6
7
8
复制代码@Bean
SimpleMessageListenerContainer processContainer(ConnectionFactory connectionFactory, ProcessReceiver processReceiver) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(DELAY_PROCESS_QUEUE_NAME); // 监听delay_process_queue
container.setMessageListener(new MessageListenerAdapter(processReceiver));
return container;
}

至此,我们前置的配置代码已经全部编写完成,接下来我们需要编写测试用例来测试我们的延迟队列。

编写测试用例

延迟消费场景

首先我们编写用于测试TTL设置在消息上的测试代码。

我们借助spring-rabbit包下提供的RabbitTemplate类来发送消息。由于我们添加了spring-boot-starter-amqp扩展,Spring Boot会在初始化时自动地将RabbitTemplate当成bean加载到容器中。

解决了消息的发送问题,那么又该如何为每个消息设置TTL呢?这里我们需要借助MessagePostProcessor。MessagePostProcessor通常用来设置消息的Header以及消息的属性。我们新建一个ExpirationMessagePostProcessor类来负责设置消息的TTL属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码/**
* 设置消息的失效时间
*/
public class ExpirationMessagePostProcessor implements MessagePostProcessor {
private final Long ttl; // 毫秒
public ExpirationMessagePostProcessor(Long ttl) {
this.ttl = ttl;
}
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties()
.setExpiration(ttl.toString()); // 设置per-message的失效时间
return message;
}
}

然后在调用RabbitTemplate的convertAndSend方法时,传入ExpirationMessagePostPorcessor即可。我们向缓冲队列中发送3条消息,过期时间依次为1秒,2秒和3秒。具体的代码如下所示:

1
2
3
4
5
6
7
8
9
10
复制代码@Test
public void testDelayQueuePerMessageTTL() throws InterruptedException {
ProcessReceiver.latch = new CountDownLatch(3);
for (int i = 1; i <= 3; i++) {
long expiration = i * 1000;
rabbitTemplate.convertAndSend(QueueConfig.DELAY_QUEUE_PER_MESSAGE_TTL_NAME,
(Object) ("Message From delay_queue_per_message_ttl with expiration " + expiration), new ExpirationMessagePostProcessor(expiration));
}
ProcessReceiver.latch.await();
}

细心的朋友一定会问,为什么要在代码中加一个CountDownLatch呢?这是因为如果没有latch阻塞住测试方法的话,测试用例会直接结束,程序退出,我们就看不到消息被延迟消费的表现了。

那么类似地,测试TTL设置在队列上的代码如下:

1
2
3
4
5
6
7
8
9
复制代码@Test
public void testDelayQueuePerQueueTTL() throws InterruptedException {
ProcessReceiver.latch = new CountDownLatch(3);
for (int i = 1; i <= 3; i++) {
rabbitTemplate.convertAndSend(QueueConfig.DELAY_QUEUE_PER_QUEUE_TTL_NAME,
"Message From delay_queue_per_queue_ttl with expiration " + QueueConfig.QUEUE_EXPIRATION);
}
ProcessReceiver.latch.await();
}

我们向缓冲队列中发送3条消息。理论上这3条消息会在4秒后同时过期。

延迟重试场景

我们同样还需测试延迟重试场景。

1
2
3
4
5
6
7
8
复制代码@Test
public void testFailMessage() throws InterruptedException {
ProcessReceiver.latch = new CountDownLatch(6);
for (int i = 1; i <= 3; i++) {
rabbitTemplate.convertAndSend(QueueConfig.DELAY_PROCESS_QUEUE_NAME, ProcessReceiver.FAIL_MESSAGE);
}
ProcessReceiver.latch.await();
}

我们向delay_process_queue发送3条会触发FAIL的消息,理论上这3条消息会在4秒后自动重试。

查看测试结果

延迟消费场景

延迟消费的场景测试我们分为了TTL设置在消息上和TTL设置在队列上两种。首先,我们先看一下TTL设置在消息上的测试结果:

从上图中我们可以看到,ProcessReceiver分别经过1秒、2秒、3秒收到消息。测试结果表明消息不仅被延迟消费了,而且每条消息的延迟时间是可以被个性化设置的。TTL设置在消息上的延迟消费场景测试成功。
然后,TTL设置在队列上的测试结果如下图:

从上图中我们可以看到,ProcessReceiver经过了4秒的延迟之后,同时收到了3条消息。测试结果表明消息不仅被延迟消费了,同时也证明了当TTL设置在队列上的时候,消息的过期时间是固定的。TTL设置在队列上的延迟消费场景测试成功。

延迟重试场景

接下来,我们再来看一下延迟重试的测试结果:

ProcessReceiver首先收到了3条会触发FAIL的消息,然后将其移动到缓冲队列之后,过了4秒,又收到了刚才的那3条消息。延迟重试场景测试成功。

总结

本文首先介绍了延迟队列的概念以及用途,并且通过代码详细讲解了如何通过Spring Boot和RabbitMQ实现一个延迟队列。希望本文能够对大家平时的学习和工作能有所启发和帮助。有什么意见或者问题欢迎在评论下方留言,谢谢!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

架构师之路-在Dubbo中开发REST风格的远程调用

发表于 2017-11-24

概述

dubbo支持多种远程调用方式,例如dubbo RPC(二进制序列化 + tcp协议)、http invoker(二进制序列化 + http协议,至少在开源版本没发现对文本序列化的支持)、hessian(二进制序列化 + http协议)、WebServices (文本序列化 + http协议)等等,但缺乏对当今特别流行的REST风格远程调用(文本序列化

  • http协议)的支持。

有鉴于此,我们基于标准的Java REST API——JAX-RS 2.0(Java API for RESTful Web Services的简写),为dubbo提供了接近透明的REST调用支持。由于完全兼容Java标准API,所以为dubbo开发的所有REST服务,未来脱离dubbo或者任何特定的REST底层实现一般也可以正常运行。

特别值得指出的是,我们并不需要完全严格遵守REST的原始定义和架构风格。即使著名的Twitter REST API也会根据情况做适度调整,而不是机械的遵守原始的REST风格。

附注:我们将这个功能称之为REST风格的远程调用,即RESTful Remoting(抽象的远程处理或者调用),而不是叫RESTful RPC(具体的远程“过程”调用),是因为REST和RPC本身可以被认为是两种不同的风格。在dubbo的REST实现中,可以说有两个面向,其一是提供或消费正常的REST服务,其二是将REST作为dubbo RPC体系中一种协议实现,而RESTful Remoting同时涵盖了这个面向。

REST的优点

以下摘自维基百科:

可更高效利用缓存来提高响应速度
通讯本身的无状态性可以让不同的服务器的处理一系列请求中的不同请求,提高服务器的扩展性
浏览器即可作为客户端,简化软件需求
相对于其他叠加在HTTP协议之上的机制,REST的软件依赖性更小
不需要额外的资源发现机制
在软件技术演进中的长期的兼容性更好
这里我还想特别补充REST的显著优点:基于简单的文本格式消息和通用的HTTP协议,使它具备极广的适用性,几乎所有语言和平台都对它提供支持,同时其学习和使用的门槛也较低。

应用场景

正是由于REST在适用性方面的优点,所以在dubbo中支持REST,可以为当今多数主流的远程调用场景都带来(显著)好处:

显著简化企业内部的异构系统之间的(跨语言)调用。此处主要针对这种场景:dubbo的系统做服务提供端,其他语言的系统(也包括某些不基于dubbo的java系统)做服务消费端,两者通过HTTP和文本消息进行通信。即使相比Thrift、ProtoBuf等二进制跨语言调用方案,REST也有自己独特的优势(详见后面讨论)

显著简化对外Open API(开放平台)的开发。既可以用dubbo来开发专门的Open API应用,也可以将原内部使用的dubbo service直接“透明”发布为对外的Open REST API(当然dubbo本身未来最好可以较透明的提供诸如权限控制、频次控制、计费等诸多功能)

显著简化手机(平板)APP或者PC桌面客户端开发。类似于2,既可以用dubbo来开发专门针对无线或者桌面的服务器端,也可以将原内部使用的dubbo service直接”透明“的暴露给手机APP或桌面程序。当然在有些项目中,手机或桌面程序也可以直接访问以上场景2中所述的Open API。

显著简化浏览器AJAX应用的开发。类似于2,既可以用dubbo来开发专门的AJAX服务器端,也可以将原内部使用的dubbo service直接”透明“的暴露给浏览器中JavaScript。当然,很多AJAX应用更适合与web框架协同工作,所以直接访问dubbo service在很多web项目中未必是一种非常优雅的架构。

为企业内部的dubbo系统之间(即服务提供端和消费端都是基于dubbo的系统)提供一种基于文本的、易读的远程调用方式。

一定程度简化dubbo系统对其它异构系统的调用。可以用类似dubbo的简便方式“透明”的调用非dubbo系统提供的REST服务(不管服务提供端是在企业内部还是外部)

需要指出的是,我认为1~3是dubbo的REST调用最有价值的三种应用场景,并且我们为dubbo添加REST调用,其最主要到目的也是面向服务的提供端,即开发REST服务来提供给非dubbo的(异构)消费端。

归纳起来,所有应用场景如下图所示:

借用Java过去最流行的宣传语,为dubbo添加REST调用后,可以实现服务的”一次编写,到处访问“,理论上可以面向全世界开放,从而真正实现比较理想化的面向服务架构(SOA)。

当然,传统的WebServices(WSDL/SOAP)也基本同样能满足以上场景(除了场景4)的要求(甚至还能满足那些需要企业级特性的场景),但由于其复杂性等问题,现在已经越来越少被实际采用了。

快速入门

在dubbo中开发一个REST风格的服务会比较简单,下面以一个注册用户的简单服务为例说明。

这个服务要实现的功能是提供如下URL(注:这个URL不是完全符合REST的风格,但是更简单实用):

1
复制代码http://localhost:8080/users/register

而任何客户端都可以将包含用户信息的JSON字符串POST到以上URL来完成用户注册。

首先,开发服务的接口:

1
2
3
复制代码public class UserService {    
void registerUser(User user);
}

然后,开发服务的实现:

1
2
3
4
复制代码@Path("users")public class UserServiceImpl implements UserService {    @POST
@Path("register") @Consumes({MediaType.APPLICATION_JSON}) public void registerUser(User user) { // save the user...
}
}

上面的服务实现代码非常简单,但是由于REST服务是要被发布到特定HTTP URL,供任意语言客户端甚至浏览器来访问,所以这里要额外添加了几个JAX-RS的标准annotation来做相关的配置:

@Path(“users”):指定访问UserService的URL相对路径是/users,即http://localhost:8080/users

@Path(“register”):指定访问registerUser()方法的URL相对路径是/register,再结合上一个@Path为UserService指定的路径,则调用UserService.register()的完整路径为http://localhost:8080/users/register

@POST:指定访问registerUser()用HTTP POST方法

@Consumes({MediaType.APPLICATION_JSON}):指定registerUser()接收JSON格式的数据。REST框架会自动将JSON数据反序列化为User对象

最后,在spring配置文件中添加此服务,即完成所有服务开发工作:

1
复制代码<!-- 用rest协议在8080端口暴露服务 --><dubbo:protocol name="rest" port="8080"/><!-- 声明需要暴露的服务接口 --><dubbo:service interface="xxx.UserService" ref="userService"/><!-- 和本地bean一样实现服务 --><bean id="userService" class="xxx.UserServiceImpl" />

标准Java REST API:JAX-RS简介

JAX-RS是标准的Java REST API,得到了业界的广泛支持和应用,其著名的开源实现就有很多,包括Oracle的Jersey,RedHat的RestEasy,Apache的CXF和Wink,以及restlet等等。另外,所有支持JavaEE 6.0以上规范的商用JavaEE应用服务器都对JAX-RS提供了支持。因此,JAX-RS是一种已经非常成熟的解决方案,并且采用它没有任何所谓vendor lock-in的问题。

JAX-RS在网上的资料非常丰富,例如下面的入门教程:

Oracle官方的tutorial:docs.oracle.com/javaee…
IBM developerWorks中国站文章:www.ibm.com/developerw.…
更多的资料请自行google或者百度一下。就学习JAX-RS来说,一般主要掌握其各种annotation的用法即可。

注意:dubbo是基于JAX-RS 2.0版本的,有时候需要注意一下资料或REST实现所涉及的版本。

REST服务提供端详解

下面我们扩充“快速入门”中的UserService,进一步展示在dubbo中REST服务提供端的开发要点。

HTTP POST/GET的实现

REST服务中虽然建议使用HTTP协议中四种标准方法POST、DELETE、PUT、GET来分别实现常见的“增删改查”,但实际中,我们一般情况直接用POST来实现“增改”,GET来实现“删查”即可(DELETE和PUT甚至会被一些防火墙阻挡)。

前面已经简单演示了POST的实现,在此,我们为UserService添加一个获取注册用户资料的功能,来演示GET的实现。

这个功能就是要实现客户端通过访问如下不同URL来获取不同ID的用户资料:

1
2
3
复制代码http://localhost:8080/users/1001
http://localhost:8080/users/1002
http://localhost:8080/users/1003

当然,也可以通过其他形式的URL来访问不同ID的用户资料,例如:

1
复制代码http://localhost:8080/users/load?id=1001

JAX-RS本身可以支持所有这些形式。但是上面那种在URL路径中包含查询参数的形式(http://localhost:8080/users/1001) 更符合REST的一般习惯,所以更推荐大家来使用。下面我们就为UserService添加一个getUser()方法来实现这种形式的URL访问:

@GET@Path(“{id : \d+}”)
@Produces({MediaType.APPLICATION_JSON})public User getUser(@PathParam(“id”) Long id) { // …}
@GET:指定用HTTP GET方法访问

@Path(“{id : d+}”):根据上面的功能需求,访问getUser()的URL应当是“http://localhost:8080/users/ + 任意数字”,并且这个数字要被做为参数传入getUser()方法。 这里的annotation配置中,@Path中间的{id: xxx}指定URL相对路径中包含了名为id参数,而它的值也将被自动传递给下面用@PathParam(“id”)修饰的方法参数id。{id:后面紧跟的d+是一个正则表达式,指定了id参数必须是数字。

@Produces({MediaType.APPLICATION_JSON}):指定getUser()输出JSON格式的数据。框架会自动将User对象序列化为JSON数据。

Annotation放在接口类还是实现类

在Dubbo中开发REST服务主要都是通过JAX-RS的annotation来完成配置的,在上面的示例中,我们都是将annotation放在服务的实现类中。但其实,我们完全也可以将annotation放到服务的接口上,这两种方式是完全等价的,例如:

1
2
3
复制代码@Path("users")public interface UserService {    @GET
@Path("{id : \\d+}") @Produces({MediaType.APPLICATION_JSON}) User getUser(@PathParam("id") Long id);
}

在一般应用中,我们建议将annotation放到服务实现类,这样annotation和java实现代码位置更接近,更便于开发和维护。另外更重要的是,我们一般倾向于避免对接口的污染,保持接口的纯净性和广泛适用性。

但是,如后文所述,如果我们要用dubbo直接开发的消费端来访问此服务,则annotation必须放到接口上。

如果接口和实现类都同时添加了annotation,则实现类的annotation配置会生效,接口上的annotation被直接忽略。

JSON、XML等多数据格式的支持

在dubbo中开发的REST服务可以同时支持传输多种格式的数据,以给客户端提供最大的灵活性。其中我们目前对最常用的JSON和XML格式特别添加了额外的功能。

比如,我们要让上例中的getUser()方法支持分别返回JSON和XML格式的数据,只需要在annotation中同时包含两种格式即可:

1
复制代码@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})User getUser(@PathParam("id") Long id);

或者也可以直接用字符串(还支持通配符)表示MediaType:

1
复制代码@Produces({"application/json", "text/xml"})User getUser(@PathParam("id") Long id);

如果所有方法都支持同样类型的输入输出数据格式,则我们无需在每个方法上做配置,只需要在服务类上添加annotation即可:

1
2
3
复制代码@Path("users")
@Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_XML})public class UserServiceImpl implements UserService { // ...}

在一个REST服务同时对多种数据格式支持的情况下,根据JAX-RS标准,一般是通过HTTP中的MIME header(content-type和accept)来指定当前想用的是哪种格式的数据。

但是在dubbo中,我们还自动支持目前业界普遍使用的方式,即用一个URL后缀(.json和.xml)来指定想用的数据格式。例如,在添加上述annotation后,直接访问http://localhost:8888/users/1001.json则表示用json格式,直接访问http://localhost:8888/users/1002.xml则表示用xml格式,比用HTTP
Header更简单直观。Twitter、微博等的REST API都是采用这种方式。

如果你既不加HTTP header,也不加后缀,则dubbo的REST会优先启用在以上annotation定义中排位最靠前的那种数据格式。

注意:这里要支持XML格式数据,在annotation中既可以用MediaType.TEXT_XML,也可以用MediaType.APPLICATION_XML,但是TEXT_XML是更常用的,并且如果要利用上述的URL后缀方式来指定数据格式,只能配置为TEXT_XML才能生效。

中文字符支持

为了在dubbo REST中正常输出中文字符,和通常的Java web应用一样,我们需要将HTTP响应的contentType设置为UTF-8编码。

基于JAX-RS的标准用法,我们只需要做如下annotation配置即可:

1
复制代码@Produces({"application/json; charset=UTF-8", "text/xml; charset=UTF-8"})User getUser(@PathParam("id") Long id);

为了方便用户,我们在dubbo REST中直接添加了一个支持类,来定义以上的常量,可以直接使用,减少出错的可能性。

1
复制代码@Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8})User getUser(@PathParam("id") Long id);

XML数据格式的额外要求

由于JAX-RS的实现一般都用标准的JAXB(Java API for XML Binding)来序列化和反序列化XML格式数据,所以我们需要为每一个要用XML传输的对象添加一个类级别的JAXB annotation,否则序列化将报错。例如为getUser()中返回的User添加如下:

1
复制代码@XmlRootElementpublic class User implements Serializable {    // ...}

此外,如果service方法中的返回值是Java的 primitive类型(如int,long,float,double等),最好为它们添加一层wrapper对象,因为JAXB不能直接序列化primitive类型。

例如,我们想让前述的registerUser()方法返回服务器端为用户生成的ID号:

1
复制代码long registerUser(User user);

由于primitive类型不被JAXB序列化支持,所以添加一个wrapper对象:

1
2
3
4
5
6
复制代码@XmlRootElementpublic class RegistrationResult implements Serializable {    private Long id;    public RegistrationResult() {
} public RegistrationResult(Long id) { this.id = id;
} public Long getId() { return id;
} public void setId(Long id) { this.id = id;
}
}

并修改service方法:

1
复制代码RegistrationResult registerUser(User user);

这样不但能够解决XML序列化的问题,而且使得返回的数据都符合XML和JSON的规范。例如,在JSON中,返回的将是如下形式:

1
复制代码{"id": 1001}

如果不加wrapper,JSON返回值将直接是

1
复制代码1001

而在XML中,加wrapper后返回值将是:

1
2
3
复制代码<registrationResult>
<id>1002</id>
</registrationResult>

这种wrapper对象其实利用所谓Data Transfer Object(DTO)模式,采用DTO还能对传输数据做更多有用的定制。

定制序列化

如上所述,REST的底层实现会在service的对象和JSON/XML数据格式之间自动做序列化/反序列化。但有些场景下,如果觉得这种自动转换不满足要求,可以对其做定制。

Dubbo中的REST实现是用JAXB做XML序列化,用Jackson做JSON序列化,所以在对象上添加JAXB或Jackson的annotation即可以定制映射。

例如,定制对象属性映射到XML元素的名字:

1
2
3
复制代码@XmlRootElement@XmlAccessorType(XmlAccessType.FIELD)public class User implements Serializable {    @XmlElement(name="username") 
private String name;
}

定制对象属性映射到JSON字段的名字:

1
2
复制代码public class User implements Serializable {    @JsonProperty("username")    private String name;
}

更多资料请参考JAXB和Jackson的官方文档,或自行google。

配置REST Server的实现

目前在dubbo中,我们支持5种嵌入式rest server的实现,并同时支持采用外部应用服务器来做rest server的实现。rest server的实现是通过如下server这个XML属性来选择的:

1
复制代码<dubbo:protocol name="rest" server="jetty"/>

以上配置选用了嵌入式的jetty来做rest server,同时,如果不配置server属性,rest协议默认也是选用jetty。jetty是非常成熟的java servlet容器,并和dubbo已经有较好的集成(目前5种嵌入式server中只有jetty和后面所述的tomcat、tjws,与dubbo监控系统等完成了无缝的集成),所以,如果你的dubbo系统是单独启动的进程,你可以直接默认采用jetty即可。

1
复制代码<dubbo:protocol name="rest" server="tomcat"/>

以上配置选用了嵌入式的tomcat来做rest server。在嵌入式tomcat上,REST的性能比jetty上要好得多(参见后面的基准测试),建议在需要高性能的场景下采用tomcat。

1
复制代码<dubbo:protocol name="rest" server="netty"/>

以上配置选用嵌入式的netty来做rest server。(TODO more contents to add)

1
2
复制代码<dubbo:protocol name="rest" server="tjws"/> (tjws is now deprecated)
<dubbo:protocol name="rest" server="sunhttp"/>

以上配置选用嵌入式的tjws或Sun HTTP server来做rest server。这两个server实现非常轻量级,非常方便在集成测试中快速启动使用,当然也可以在负荷不高的生产环境中使用。 注:tjws目前已经被deprecated掉了,因为它不能很好的和servlet 3.1 API工作。

如果你的dubbo系统不是单独启动的进程,而是部署到了Java应用服务器中,则建议你采用以下配置:

1
复制代码<dubbo:protocol name="rest" server="servlet"/>

通过将server设置为servlet,dubbo将采用外部应用服务器的servlet容器来做rest server。同时,还要在dubbo系统的web.xml中添加如下配置:

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
复制代码<web-app>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/classes/META-INF/spring/dubbo-demo-provider.xml</param-value>
</context-param>

<listener>
<listener-class>com.alibaba.dubbo.remoting.http.servlet.BootstrapListener</listener-class>
</listener>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>com.alibaba.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>

即必须将dubbo的BootstrapListener和DispatherServlet添加到web.xml,以完成dubbo的REST功能与外部servlet容器的集成。

注意:如果你是用spring的ContextLoaderListener来加载spring,则必须保证BootstrapListener配置在ContextLoaderListener之前,否则dubbo初始化会出错。

其实,这种场景下你依然可以坚持用嵌入式server,但外部应用服务器的servlet容器往往比嵌入式server更加强大(特别是如果你是部署到更健壮更可伸缩的WebLogic,WebSphere等),另外有时也便于在应用服务器做统一管理、监控等等。

获取上下文(Context)信息

在远程调用中,值得获取的上下文信息可能有很多种,这里特别以获取客户端IP为例。

在dubbo的REST中,我们有两种方式获取客户端IP。

第一种方式,用JAX-RS标准的@Context annotation:

1
2
复制代码public User getUser(@PathParam("id") Long id, @Context HttpServletRequest request) {    System.out.println("Client address is " + request.getRemoteAddr());
}

用Context修饰getUser()的一个方法参数后,就可以将当前的HttpServletRequest注入进来,然后直接调用servlet api获取IP。

注意:这种方式只能在设置server=”tjws”或者server=”tomcat”或者server=”jetty”或者server=”servlet”的时候才能工作,因为只有这几种REST server的实现才提供了servlet容器。另外,标准的JAX-RS还支持用@Context修饰service类的一个实例字段来获取HttpServletRequest,但在dubbo中我们没有对此作出支持。

第二种方式,用dubbo中常用的RpcContext:

1
2
复制代码public User getUser(@PathParam("id") Long id) {    System.out.println("Client address is " + RpcContext.getContext().getRemoteAddressString());
}

注意:这种方式只能在设置server=”jetty”或者server=”tomcat”或者server=”servlet”或者server=”tjws”的时候才能工作。另外,目前dubbo的RpcContext是一种比较有侵入性的用法,未来我们很可能会做出重构。

如果你想保持你的项目对JAX-RS的兼容性,未来脱离dubbo也可以运行,请选择第一种方式。如果你想要更优雅的服务接口定义,请选用第二种方式。

此外,在最新的dubbo rest中,还支持通过RpcContext来获取HttpServletRequest和HttpServletResponse,以提供更大的灵活性来方便用户实现某些复杂功能,比如在dubbo标准的filter中访问HTTP Header。用法示例如下:

1
2
3
复制代码if (RpcContext.getContext().getRequest() != null && RpcContext.getContext().getRequest() instanceof HttpServletRequest) {    System.out.println("Client address is " + ((HttpServletRequest) RpcContext.getContext().getRequest()).getRemoteAddr());
}if (RpcContext.getContext().getResponse() != null && RpcContext.getContext().getResponse() instanceof HttpServletResponse) { System.out.println("Response object from RpcContext: " + RpcContext.getContext().getResponse());
}

注意:为了保持协议的中立性,RpcContext.getRequest()和RpcContext.getResponse()返回的仅仅是一个Object类,而且可能为null。所以,你必须自己做null和类型的检查。

注意:只有在设置server=”jetty”或者server=”tomcat”或者server=”servlet”的时候,你才能通过以上方法正确的得到HttpServletRequest和HttpServletResponse,因为只有这几种server实现了servlet容器。

为了简化编程,在此你也可以用泛型的方式来直接获取特定类型的request/response:

1
2
3
复制代码if (RpcContext.getContext().getRequest(HttpServletRequest.class) != null) {    System.out.println("Client address is " + RpcContext.getContext().getRequest(HttpServletRequest.class).getRemoteAddr());
}if (RpcContext.getContext().getResponse(HttpServletResponse.class) != null) { System.out.println("Response object from RpcContext: " + RpcContext.getContext().getResponse(HttpServletResponse.class));
}

如果request/response不符合指定的类型,这里也会返回null。

配置端口号和Context Path

dubbo中的rest协议默认将采用80端口,如果想修改端口,直接配置:

1
复制代码<dubbo:protocol name="rest" port="8888"/>

另外,如前所述,我们可以用@Path来配置单个rest服务的URL相对路径。但其实,我们还可以设置一个所有rest服务都适用的基础相对路径,即java web应用中常说的context path。

只需要添加如下contextpath属性即可:

1
复制代码<dubbo:protocol name="rest" port="8888" contextpath="services"/>

以前面代码为例:

1
2
3
4
复制代码@Path("users")public class UserServiceImpl implements UserService {    @POST
@Path("register") @Consumes({MediaType.APPLICATION_JSON}) public void registerUser(User user) { // save the user...
}
}

现在registerUser()的完整访问路径为:

1
复制代码http://localhost:8888/services/users/register

注意:如果你是选用外部应用服务器做rest server,即配置:

1
复制代码<dubbo:protocol name="rest" port="8888" contextpath="services" server="servlet"/>

则必须保证这里设置的port、contextpath,与外部应用服务器的端口、DispatcherServlet的上下文路径(即webapp path加上servlet url pattern)保持一致。例如,对于部署为tomcat ROOT路径的应用,这里的contextpath必须与web.xml中DispacherServlet的完全一致:

1
2
3
4
复制代码<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/services/*</url-pattern>
</servlet-mapping>

配置线程数和IO线程数

可以为rest服务配置线程池大小:

1
复制代码<dubbo:protocol name="rest" threads="500"/>

注意:目前线程池的设置只有当server=”netty”或者server=”jetty”或者server=”tomcat”的时候才能生效。另外,如果server=”servlet”,由于这时候启用的是外部应用服务器做rest server,不受dubbo控制,所以这里的线程池设置也无效。如果是选用netty server,还可以配置Netty的IO worker线程数:

1
复制代码<dubbo:protocol name="rest" iothreads="5" threads="100"/>

配置长连接

Dubbo中的rest服务默认都是采用http长连接来访问,如果想切换为短连接,直接配置:

1
复制代码<dubbo:protocol name="rest" keepalive="false"/>

注意:这个配置目前只对server=”netty”和server=”tomcat”才能生效。

配置最大的HTTP连接数

可以配置服务器提供端所能同时接收的最大HTTP连接数,防止REST server被过多连接撑爆,以作为一种最基本的自我保护机制:

1
复制代码<dubbo:protocol name="rest" accepts="500" server="tomcat/>

注意:这个配置目前只对server=”tomcat”才能生效。

配置每个消费端的超时时间和HTTP连接数

如果rest服务的消费端也是dubbo系统,可以像其他dubbo RPC机制一样,配置消费端调用此rest服务的最大超时时间以及每个消费端所能启动的最大HTTP连接数。

1
复制代码<dubbo:service interface="xxx" ref="xxx" protocol="rest" timeout="2000" connections="10"/>

当然,由于这个配置针对消费端生效的,所以也可以在消费端配置:

1
复制代码<dubbo:reference id="xxx" interface="xxx" timeout="2000" connections="10"/>

但是,通常我们建议配置在服务提供端提供此类配置。按照dubbo官方文档的说法:“Provider上尽量多配置Consumer端的属性,让Provider实现者一开始就思考Provider服务特点、服务质量的问题。”

注意:如果dubbo的REST服务是发布给非dubbo的客户端使用,则这里dubbo:service/上的配置完全无效,因为这种客户端不受dubbo控制。

GZIP数据压缩

Dubbo的REST支持用GZIP压缩请求和响应的数据,以减少网络传输时间和带宽占用,但这种方式会也增加CPU开销。

TODO more contents to add

用Annotation取代部分Spring XML配置

以上所有的讨论都是基于dubbo在spring中的xml配置。但是,dubbo/spring本身也支持用annotation来作配置,所以我们也可以按dubbo官方文档中的步骤,把相关annotation加到REST服务的实现中,取代一些xml配置,例如:

1
2
3
4
5
6
7
复制代码@Service(protocol = "rest")
@Path("users")public class UserServiceImpl implements UserService { @Autowired
private UserRepository userRepository; @POST
@Path("register") @Consumes({MediaType.APPLICATION_JSON}) public void registerUser(User user) { // save the user
userRepository.save(user);
}
}

annotation的配置更简单更精确,经常也更便于维护(当然现代IDE都可以在xml中支持比如类名重构,所以就这里的特定用例而言,xml的维护性也很好)。而xml对代码对侵入性更小一些,尤其有利于动态修改配置,特别是比如你要针对单个服务配置连接超时时间、每客户端最大连接数、集群策略、权重等等。另外,特别对复杂应用或者模块来说,xml提供了一个中心点来涵盖的所有组件和配置,更一目了然,一般更便于项目长时期的维护。

当然,选择哪种配置方式没有绝对的优劣,和个人的偏好也不无关系。

添加自定义的Filter、Interceptor等

Dubbo的REST也支持JAX-RS标准的Filter和Interceptor,以方便对REST的请求与响应过程做定制化的拦截处理。

其中,Filter主要用于访问和设置HTTP请求和响应的参数、URI等等。例如,设置HTTP响应的cache header:

1
2
3
4
5
复制代码public class CacheControlFilter implements ContainerResponseFilter {    public void filter(ContainerRequestContext req, ContainerResponseContext res) {        if (req.getMethod().equals("GET")) {
res.getHeaders().add("Cache-Control", "someValue");
}
}
}

Interceptor主要用于访问和修改输入与输出字节流,例如,手动添加GZIP压缩:

1
2
3
4
5
6
复制代码public class GZIPWriterInterceptor implements WriterInterceptor {    @Override
public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { OutputStream outputStream = context.getOutputStream();
context.setOutputStream(new GZIPOutputStream(outputStream));
context.proceed();
}
}

在标准JAX-RS应用中,我们一般是为Filter和Interceptor添加@Provider annotation,然后JAX-RS runtime会自动发现并启用它们。而在dubbo中,我们是通过添加XML配置的方式来注册Filter和Interceptor:

1
复制代码<dubbo:protocol name="rest" port="8888" extension="xxx.TraceInterceptor, xxx.TraceFilter"/>

在此,我们可以将Filter、Interceptor和DynamicFuture这三种类型的对象都添加到extension属性上,多个之间用逗号分隔。(DynamicFuture是另一个接口,可以方便我们更动态的启用Filter和Interceptor,感兴趣请自行google。)

当然,dubbo自身也支持Filter的概念,但我们这里讨论的Filter和Interceptor更加接近协议实现的底层,相比dubbo的filter,可以做更底层的定制化。

注:这里的XML属性叫extension,而不是叫interceptor或者filter,是因为除了Interceptor和Filter,未来我们还会添加更多的扩展类型。

如果REST的消费端也是dubbo系统(参见下文的讨论),则也可以用类似方式为消费端配置Interceptor和Filter。但注意,JAX-RS中消费端的Filter和提供端的Filter是两种不同的接口。例如前面例子中服务端是ContainerResponseFilter接口,而消费端对应的是ClientResponseFilter:

1
2
3
4
5
复制代码public class LoggingFilter implements ClientResponseFilter {    public void filter(ClientRequestContext reqCtx, ClientResponseContext resCtx) throws IOException {        System.out.println("status: " + resCtx.getStatus());        System.out.println("date: " + resCtx.getDate());        System.out.println("last-modified: " + resCtx.getLastModified());        System.out.println("location: " + resCtx.getLocation());        System.out.println("headers:");        for (Entry<String, List<String>> header : resCtx.getHeaders().entrySet()) {            System.out.print("\t" + header.getKey() + " :");            for (String value : header.getValue()) {                System.out.print(value + ", ");
} System.out.print("\n");
} System.out.println("media-type: " + resCtx.getMediaType().getType());
}
}

添加自定义的Exception处理

Dubbo的REST也支持JAX-RS标准的ExceptionMapper,可以用来定制特定exception发生后应该返回的HTTP响应。

1
2
3
4
复制代码public class CustomExceptionMapper implements ExceptionMapper<NotFoundException> {    public Response toResponse(NotFoundException e) {     
return Response.status(Response.Status.NOT_FOUND).entity("Oops! the requested resource is not found!").type("text/plain").build();
}
}

和Interceptor、Filter类似,将其添加到XML配置文件中即可启用:

1
复制代码<dubbo:protocol name="rest" port="8888" extension="xxx.CustomExceptionMapper"/>

配置HTTP日志输出

Dubbo rest支持输出所有HTTP请求/响应中的header字段和body消息体。

在XML配置中添加如下自带的REST filter:

1
复制代码<dubbo:protocol name="rest" port="8888" extension="com.alibaba.dubbo.rpc.protocol.rest.support.LoggingFilter"/>

然后配置在logging配置中至少为com.alibaba.dubbo.rpc.protocol.rest.support打开INFO级别日志输出,例如,在log4j.xml中配置:

1
2
3
4
复制代码<logger name="com.alibaba.dubbo.rpc.protocol.rest.support">
<level value="INFO"/>
<appender-ref ref="CONSOLE"/>
</logger>

当然,你也可以直接在ROOT logger打开INFO级别日志输出:

1
2
3
4
复制代码<root>
<level value="INFO" />
<appender-ref ref="CONSOLE"/>
</root>

然后在日志中会有类似如下的内容输出:

1
2
3
4
5
6
7
8
9
10
复制代码The HTTP headers are: 
accept: application/json;charset=UTF-8
accept-encoding: gzip, deflate
connection: Keep-Alive
content-length: 22
content-type: application/json
host: 192.168.1.100:8888
user-agent: Apache-HttpClient/4.2.1 (java 1.5)
The contents of request body is:
{"id":1,"name":"dang"}

打开HTTP日志输出后,除了正常日志输出的性能开销外,也会在比如HTTP请求解析时产生额外的开销,因为需要建立额外的内存缓冲区来为日志的输出做数据准备。

输入参数的校验

dubbo的rest支持采用Java标准的bean validation annotation(JSR 303)来做输入校验beanvalidation.org/

为了和其他dubbo远程调用协议保持一致,在rest中作校验的annotation必须放在服务的接口上,例如:

1
2
复制代码public interface UserService {    User getUser(@Min(value=1L, message="User ID must be greater than 1") Long id);
}

当然,在很多其他的bean validation的应用场景都是将annotation放到实现类而不是接口上。把annotation放在接口上至少有一个好处是,dubbo的客户端可以共享这个接口的信息,dubbo甚至不需要做远程调用,在本地就可以完成输入校验。

然后按照dubbo的标准方式在XML配置中打开验证:

1
复制代码<dubbo:service interface=xxx.UserService" ref="userService" protocol="rest" validation="true"/>

在dubbo的其他很多远程调用协议中,如果输入验证出错,是直接将RpcException抛向客户端,而在rest中由于客户端经常是非dubbo,甚至非java的系统,所以不便直接抛出Java异常。因此,目前我们将校验错误以XML的格式返回:

1
2
3
4
5
6
7
复制代码<violationReport>
<constraintViolations>
<path>getUserArgument0</path>
<message>User ID must be greater than 1</message>
<value>0</value>
</constraintViolations>
</violationReport>

稍后也会支持其他数据格式的返回值。至于如何对验证错误消息作国际化处理,直接参考bean validation的相关文档即可。

如果你认为默认的校验错误返回格式不符合你的要求,可以如上面章节所述,添加自定义的ExceptionMapper来自由的定制错误返回格式。需要注意的是,这个ExceptionMapper必须用泛型声明来捕获dubbo的RpcException,才能成功覆盖dubbo rest默认的异常处理策略。为了简化操作,其实这里最简单的方式是直接继承dubbo rest的RpcExceptionMapper,并覆盖其中处理校验异常的方法即可:

1
2
3
4
5
6
7
8
9
复制代码public class MyValidationExceptionMapper extends RpcExceptionMapper {    protected Response handleConstraintViolationException(ConstraintViolationException cve) {        ViolationReport report = new ViolationReport();        for (ConstraintViolation cv : cve.getConstraintViolations()) {
report.addConstraintViolation(new RestConstraintViolation(
cv.getPropertyPath().toString(),
cv.getMessage(),
cv.getInvalidValue() == null ? "null" : cv.getInvalidValue().toString()));
} // 采用json输出代替xml输出
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(report).type(ContentType.APPLICATION_JSON_UTF_8).build();
}
}

然后将这个ExceptionMapper添加到XML配置中即可:

1
复制代码<dubbo:protocol name="rest" port="8888" extension="xxx.MyValidationExceptionMapper"/>

是否应该透明发布REST服务

Dubbo的REST调用和dubbo中其它某些RPC不同的是,需要在服务代码中添加JAX-RS的annotation(以及JAXB、Jackson的annotation),如果你觉得这些annotation一定程度“污染”了你的服务代码,你可以考虑编写额外的Facade和DTO类,在Facade和DTO上添加annotation,而Facade将调用转发给真正的服务实现类。当然事实上,直接在服务代码中添加annotation基本没有任何负面作用,而且这本身是Java EE的标准用法,另外JAX-RS和JAXB的annotation是属于java标准,比我们经常使用的spring、dubbo等等annotation更没有vendor
lock-in的问题,所以一般没有必要因此而引入额外对象。

另外,如果你想用前述的@Context annotation,通过方法参数注入HttpServletRequest(如public User getUser(@PathParam(“id”) Long id, @Context HttpServletRequest request)),这时候由于改变了服务的方法签名,并且HttpServletRequest是REST特有的参数,如果你的服务要支持多种RPC机制的话,则引入额外的Facade类是比较适当的。

当然,在没有添加REST调用之前,你的服务代码可能本身已经就充当了Facade和DTO的角色(至于为什么有些场景需要这些角色,有兴趣可参考微观SOA:服务设计原则及其实践方式)。这种情况下,在添加REST之后,如果你再额外添加与REST相关的Facade和DTO,就相当于对原有代码对再一次包装,即形成如下调用链:

1
复制代码RestFacade/RestDTO -> Facade/DTO -> Service

这种体系比较繁琐,数据转换之类的工作量也不小,所以一般应尽量避免如此。

REST服务消费端详解

这里我们用三种场景来分别讨论:

非dubbo的消费端调用dubbo的REST服务(non-dubbo –> dubbo)
dubbo消费端调用dubbo的REST服务 (dubbo –> dubbo)
dubbo的消费端调用非dubbo的REST服务 (dubbo –> non-dubbo)

场景1:非dubbo的消费端调用dubbo的REST服务

这种场景的客户端与dubbo本身无关,直接选用相应语言和框架中合适的方式即可。

如果是还是java的客户端(但没用dubbo),可以考虑直接使用标准的JAX-RS Client API或者特定REST实现的Client API来调用REST服务。下面是用JAX-RS Client API来访问上述的UserService的registerUser():

1
2
3
4
5
6
复制代码User user = new User();
user.setName("Larry");Client client = ClientBuilder.newClient();WebTarget target = client.target("http://localhost:8080/services/users/register.json");Response response = target.request().post(Entity.entity(user, MediaType.APPLICATION_JSON_TYPE));try { if (response.getStatus() != 200) { throw new RuntimeException("Failed with HTTP error code : " + response.getStatus());
} System.out.println("The generated id is " + response.readEntity(RegistrationResult.class).getId());
} finally {
response.close();
client.close(); // 在真正开发中不要每次关闭client,比如HTTP长连接是由client持有的}

上面代码片段中的User和RegistrationResult类都是消费端自己编写的,JAX-RS Client API会自动对它们做序列化/反序列化。

当然,在java中也可以直接用自己熟悉的比如HttpClient,FastJson,XStream等等各种不同技术来实现REST客户端,在此不再详述。

场景2:dubbo消费端调用dubbo的REST服务

这种场景下,和使用其他dubbo的远程调用方式一样,直接在服务提供端和服务消费端共享Java服务接口,并添加spring xml配置(当然也可以用spring/dubbo的annotation配置),即可透明的调用远程REST服务:

1
复制代码<dubbo:reference id="userService" interface="xxx.UserService"/>

如前所述,这种场景下必须把JAX-RS的annotation添加到服务接口上,这样在dubbo在消费端才能共享相应的REST配置信息,并据之做远程调用:

1
2
3
复制代码@Path("users")public interface UserService {    @GET
@Path("{id : \\d+}") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) User getUser(@PathParam("id") Long id);
}

如果服务接口的annotation中配置了多种数据格式,这里由于两端都是dubbo系统,REST的大量细节被屏蔽了,所以不存在用前述URL后缀之类选择数据格式的可能。目前在这种情况下,排名最靠前的数据格式将直接被使用。

因此,我们建议你在定义annotation的时候最好把最合适的数据格式放到前面,比如以上我们是把json放在xml前面,因为json的传输性能优于xml。

场景3:dubbo的消费端调用非dubbo的REST服务

这种场景下,可以直接用场景1中描述的Java的方式来调用REST服务。但其实也可以采用场景2中描述的方式,即更透明的调用REST服务,即使这个服务并不是dubbo提供的。

如果用场景2的方式,由于这里REST服务并非dubbo提供,一般也就没有前述的共享的Java服务接口,所以在此我们需要根据外部REST服务的情况,自己来编写Java接口以及相应参数类,并添加JAX-RS、JAXB、Jackson等的annotation,dubbo的REST底层实现会据此去自动生成请求消息,自动解析响应消息等等,从而透明的做远程调用。或者这种方式也可以理解为,我们尝试用JAX-RS的方式去仿造实现一遍外部的REST服务提供端,然后把写成服务接口放到客户端来直接使用,dubbo的REST底层实现就能像调用dubbo的REST服务一样调用其他REST服务。

例如,我们要调用如下的外部服务

1
2
复制代码http://api.foo.com/services/users/1001
http://api.foo.com/services/users/1002

获取不同ID的用户资料,返回格式是JSON

1
复制代码{    "id": 1001,    "name": "Larry"}

我们可根据这些信息,编写服务接口和参数类即可:

1
2
3
4
复制代码@Path("users")public interface UserService {    @GET
@Path("{id : \\d+}") @Produces({MediaType.APPLICATION_JSON}) User getUser(@PathParam("id") Long id);
}
public class User implements Serializable { private Long id; private String name; // …}

对于spring中的配置,因为这里的REST服务不是dubbo提供的,所以无法使用dubbo的注册中心,直接配置外部REST服务的url地址即可(如多个地址用逗号分隔):

1
复制代码<dubbo:reference id="userService" interface="xxx.UserService" url="rest://api.foo.com/services/"/>

注意:这里协议必须用rest://而不是http://之类。如果外部的REST服务有context path,则在url中也必须添加上(除非你在每个服务接口的@Pathannotation中都带上context path),例如上面的/services/。同时这里的services后面必须带上/,这样才能使dubbo正常工作。

另外,这里依然可以配置客户端可启动的最大连接数和超时时间:

1
复制代码<dubbo:reference id="userService" interface="xxx.UserService" url="rest://api.foo.com/services/" timeout="2000" connections="10"/>

Dubbo中JAX-RS的限制

Dubbo中的REST开发是完全兼容标准JAX-RS的,但其支持的功能目前是完整JAX-RS的一个子集,部分因为它要受限于dubbo和spring的特定体系。

在dubbo中使用的JAX-RS的局限包括但不限于:

服务实现只能是singleton的,不能支持per-request scope和per-lookup scope不支持用@Context annotation对服务的实例字段注入 ServletConfig、ServletContext、HttpServletRequest、HttpServletResponse等等,但可以支持对服务方法参数的注入。但对某些特定REST server实现,(祥见前面的叙述),也不支持对服务方法参数的注入。

REST常见问题解答(REST FAQ)

Dubbo REST的服务能和Dubbo注册中心、监控中心集成吗?

可以的,而且是自动集成的,也就是你在dubbo中开发的所有REST服务都会自动注册到服务册中心和监控中心,可以通过它们做管理。

但是,只有当REST的消费端也是基于dubbo的时候,注册中心中的许多服务治理操作才能完全起作用。而如果消费端是非dubbo的,自然不受注册中心管理,所以其中很多操作是不会对消费端起作用的。

Dubbo REST中如何实现负载均衡和容错(failover)?

如果dubbo REST的消费端也是dubbo的,则Dubbo REST和其他dubbo远程调用协议基本完全一样,由dubbo框架透明的在消费端做load balance、failover等等。

如果dubbo REST的消费端是非dubbo的,甚至是非java的,则最好配置服务提供端的软负载均衡机制,目前可考虑用LVS、HAProxy、 Nginx等等对HTTP请求做负载均衡。

JAX-RS中重载的方法能够映射到同一URL地址吗?

stackoverflow.com/ques…

JAX-RS中作POST的方法能够接收多个参数吗?

stackoverflow.com/ques…

Dubbo当前体系的不足之处(与REST相关的)

我认为dubbo当前体系中显然也有不少不足之处,这里列出几个与REST有关的、并影响用户使用的问题(不包括内部实现的问题),供参考评论,为下一步重构作准备。

RpcContext的侵入性在前文,前面我们已经提到过RpcContext用法的侵入性,由于它是用单例的方式来访问上下文信息,这完全不符合spring应用的一般风格,不利于应用扩展和单元测试。未来我们可能用依赖注入方式注入一个接口,再用它去访问ThreadLocal中的上下文信息。

Protocol配置的局限性

dubbo支持多种远程调用方式,但所有调用方式都是用dubbo:protocol/来配置的,例如:

1
2
复制代码<dubbo:protocol name="dubbo" port="9090" server="netty" client="netty" codec="dubbo" serialization="hessian2" 
charset="UTF-8" threadpool="fixed" threads="100" queues="0" iothreads="9" buffer="8192" accepts="1000" payload="8388608"/>

其实,上面很多属性实际上dubbo RPC远程调用方式特有的,很多dubbo中的其它远程调用方式根本就不支持例如server, client, codec, iothreads, accepts, payload等等(当然,有的是条件所限不支持,有的是根本没有必要支持)。这给用户的使用徒增很多困惑,用户也并不知道有些属性(比如做性能调优)添加了实际上是不起作用的。

另一方面,各种远程调用方式往往有大量自己独特的配置需要,特别是我们逐步为每种远程调用方式都添加更丰富、更高级的功能,这就不可避免的扩展中的属性(例如目前我们在REST中已经添加了keepalive和extension两个属性),到最后会导致臃肿不堪,用户的使用也更加困惑。

当然,dubbo中有一种扩展的方式是用dubbo:parameter/,但这种方式显然很有局限性,而且用法复杂,缺乏schema校验。

所以,最好的方式是为每种远程调用方式设置自己的protocol元素,比如,等等,每种元素用XML schema规定自己的属性(当然属性在各种远程调用方式之间能通用是最好的)。

如此一来,例如前面提到过的extension配置也可以用更自由的方式,从而更清楚更可扩展(以下只是举例,当然也许有更好的方式):

1
2
3
4
5
6
复制代码<dubbo:protocol-rest port="8080">
<dubbo:extension>someInterceptor</dubbo:extension>
<dubbo:extension>someFilter</dubbo:extension>
<dubbo:extension>someDynamicFeature</dubbo:extension>
<dubbo:extension>someEntityProvider</dubbo:extension>
</dubbo:protocol-rest>

XML命名不符合spring规范

dubbo的XML配置中大量命名都不符合spring规范,比如:

1
2
复制代码<dubbo:protocol name="dubbo" port="9090" server="netty" client="netty" codec="dubbo" serialization="hessian2" 
charset="UTF-8" threadpool="fixed" threads="100" queues="0" iothreads="9" buffer="8192" accepts="1000" payload="8388608"/>

上面threadpool应该改为thread-pool,iothreads应该改为io-threads,单词之间应该用”-“分隔。这虽然看起来是个小问题,但也涉及到了可读性,特别是可扩展性,因为有时候我们不可避免要用更多单词来描述XML元素和属性。

其实dubbo本身也是建议遵守spring到XML的命名规范。

REST最佳实践

TODO

性能基准测试

测试环境

粗略如下:

两台独立服务器
4核Intel(R) Xeon(R) CPU E5-2603 0 @ 1.80GHz
8G内存
服务器之间网络通过百兆交换机
CentOS 5
JDK 7
Tomcat 7
JVM参数-server -Xms1g -Xmx1g -XX:PermSize=64M -XX:+UseConcMarkSweepGC

测试脚本

和dubbo自身的基准测试保持接近:

10个并发客户端持续不断发出请求:

传入嵌套复杂对象(但单个数据量很小),不做任何处理,原样返回
传入50K字符串,不做任何处理,原样返回(TODO:结果尚未列出)
进行5分钟性能测试。(引用dubbo自身测试的考虑:“主要考察序列化和网络IO的性能,因此服务端无任何业务逻辑。取10并发是考虑到http协议在高并发下对CPU的使用率较高可能会先打到瓶颈。”)

测试结果

下面的结果主要对比的是REST和dubbo RPC两种远程调用方式,并对它们作不同的配置,例如:

“REST: Jetty + XML + GZIP”的意思是:测试REST,并采用jetty server,XML数据格式,启用GZIP压缩。
“Dubbo: hessian2”的意思是:测试dubbo RPC,并采用hessian2序列化方式。
针对复杂对象的结果如下(响应时间越小越好,TPS越大越好):

远程调用方式 平均响应时间 平均TPS(每秒事务数)

REST: Jetty + JSON 7.806 1280
REST: Jetty + JSON + GZIP TODO TODO
REST: Jetty + XML TODO TODO
REST: Jetty + XML + GZIP TODO TODO
REST: Tomcat + JSON 2.082 4796
REST: Netty + JSON 2.182 4576
Dubbo: FST 1.211 8244
Dubbo: kyro 1.182
8444
Dubbo: dubbo serialization 1.43 6982
Dubbo: hessian2 1.49 6701
Dubbo: fastjson 1.572 6352

仅就目前的结果,一点简单总结:

dubbo RPC(特别是基于高效java序列化方式如kryo,fst)比REST的响应时间和吞吐量都有较显著优势,内网的dubbo系统之间优先选择dubbo RPC。
在REST的实现选择上,仅就性能而言,目前tomcat7和netty最优(当然目前使用的jetty和netty版本都较低)。tjws和sun http server在性能测试中表现极差,平均响应时间超过200ms,平均tps只有50左右(为了避免影响图片效果,没在上面列出)。
在REST中JSON数据格式性能优于XML(数据暂未在以上列出)。
在REST中启用GZIP对企业内网中的小数据量复杂对象帮助不大,性能反而有下降(数据暂未在以上列出)。

性能优化建议

如果将dubbo REST部署到外部Tomcat上,并配置server=”servlet”,即启用外部的tomcat来做为rest server的底层实现,则最好在tomcat上添加如下配置:

1
2
3
4
5
6
7
8
复制代码<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443"
minSpareThreads="20"
enableLookups="false"
maxThreads="100"
maxKeepAliveRequests="-1"
keepAliveTimeout="60000"/>

特别是maxKeepAliveRequests=”-1”,这个配置主要是保证tomcat一直启用http长连接,以提高REST调用性能。但是请注意,如果REST消费端不是持续的调用REST服务,则一直启用长连接未必是最好的做法。另外,一直启用长连接的方式一般不适合针对普通webapp,更适合这种类似rpc的场景。所以为了高性能,在tomcat中,dubbo REST应用和普通web应用最好不要混合部署,而应该用单独的实例。

TODO more contents to add

扩展讨论
REST与Thrift、Protobuf等的对比
TODO

REST与传统WebServices的对比TODO

JAX-RS与Spring MVC的对比
初步看法,摘自www.infoq.com/cn/news/…

谢谢,对于jax-rs和spring mvc,其实我对spring mvc的rest支持还没有太深入的看过,说点初步想法,请大家指正:

spring mvc也支持annotation的配置,其实和jax-rs看起来是非常非常类似的。

我个人认为spring mvc相对更适合于面向web应用的restful服务,比如被AJAX调用,也可能输出HTML之类的,应用中还有页面跳转流程之类,spring mvc既可以做好正常的web页面请求也可以同时处理rest请求。但总的来说这个restful服务是在展现层或者叫web层之类实现的

而jax-rs相对更适合纯粹的服务化应用,也就是传统Java EE中所说的中间层服务,比如它可以把传统的EJB发布成restful服务。在spring应用中,也就把spring中充当service之类的bean直接发布成restful服务。总的来说这个restful服务是在业务、应用层或者facade层。而MVC层次和概念在这种做比如(后台)服务化的应用中通常是没有多大价值的。

当然jax-rs的有些实现比如jersey,也试图提供mvc支持,以更好的适应上面所说的web应用,但应该是不如spring mvc。

在dubbo应用中,我想很多人都比较喜欢直接将一个本地的spring service bean(或者叫manager之类的)完全透明的发布成远程服务,则这里用JAX-RS是更自然更直接的,不必额外的引入MVC概念。当然,先不讨论透明发布远程服务是不是最佳实践,要不要添加facade之类。

当然,我知道在dubbo不支持rest的情况下,很多朋友采用的架构是spring mvc restful调用dubbo (spring) service来发布restful服务的。这种方式我觉得也非常好,只是如果不修改spring mvc并将其与dubbo深度集成,restful服务不能像dubbo中的其他远程调用协议比如webservices、dubbo rpc、hessian等等那样,享受诸多高级的服务治理的功能,比如:注册到dubbo的服务注册中心,通过dubbo监控中心监控其调用次数、TPS、响应时间之类,通过dubbo的统一的配置方式控制其比如线程池大小、最大连接数等等,通过dubbo统一方式做服务流量控制、权限控制、频次控制。另外spring
mvc仅仅负责服务端,而在消费端,通常是用spring restTemplate,如果restTemplate不和dubbo集成,有可能像dubbo服务客户端那样自动或者人工干预做服务降级。如果服务端消费端都是dubbo系统,通过spring的rest交互,如果spring rest不深度整合dubbo,则不能用dubbo统一的路由分流等功能。

当然,其实我个人认为这些东西不必要非此即彼的。我听说spring创始人rod johnson总是爱说一句话,the customer is always right,其实与其非要探讨哪种方式更好,不如同时支持两种方式就是了,所以原来在文档中也写过计划支持spring rest annoation,只是不知道具体可行性有多高。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

spring security ajax登录及返回

发表于 2017-11-24

序

本文讲述一下如何自定义spring security的登录页,网上给的资料大多过时,而且是基于后端模板技术的,讲的不是太清晰,本文给出一个采用ajax的登录及返回的前后端分离方式。

ajax返回

总共需要处理3个地方,一个是异常的处理,需要兼容ajax请求,一个是成功返回的处理,一个是失败返回的处理。

ajax的异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
if(isAjaxRequest(request)){
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,authException.getMessage());
}else{
response.sendRedirect("/login.html");
}

}

public static boolean isAjaxRequest(HttpServletRequest request) {
String ajaxFlag = request.getHeader("X-Requested-With");
return ajaxFlag != null && "XMLHttpRequest".equals(ajaxFlag);
}
}

这里我们自定义成功及失败的ajax返回,当然这里我们简单处理,只返回statusCode

AjaxAuthSuccessHandler

1
2
3
4
5
6
复制代码public class AjaxAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
}
}

AjaxAuthFailHandler

1
2
3
4
5
6
复制代码public class AjaxAuthFailHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed");
}
}

security配置

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
复制代码@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling().authenticationEntryPoint(new UnauthorizedEntryPoint())
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/login","/css/**", "/js/**","/fonts/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login")
.usernameParameter("name")
.passwordParameter("password")
.successHandler(new AjaxAuthSuccessHandler())
.failureHandler(new AjaxAuthFailHandler())
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.permitAll();
}

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("admin").roles("USER");
}
}

这里有几个要注意的点:

  • permitAll

这里要添加前端资源路径,以及登陆表单请求的接口地址/login

  • loginPage

这里设置登录页面的地址,这里我们用静态页面,即static目录下的login.html

  • ajax配置

将authenticationEntryPoint,successHandler,failureHandler设置为上面自定义的ajax处理类

登录页面

就是一个纯粹的html页面,其中登录按钮的ajax请求如下:

1
2
3
4
5
6
7
8
9
10
11
复制代码$.ajax({
url: '/login',
type: 'POST',
data: "name="+name+"&password="+password,
success: function (res, status) {
window.location.href='/ok.html'
},
error: function (res, status) {
dangerDialog(res.statusText);
}
});

这里是请求/login,也就是spring security会默认拦截的路径,不了解spring security的人可能会纳闷,我请求这个路径,但是工程里头没有定义/login的request mapping,不要紧么。下面来剖析一下。

spring security内置的各种filter:

Alias Filter Class Namespace Element or Attribute
CHANNEL_FILTER ChannelProcessingFilter http/intercept-url@requires-channel
SECURITY_CONTEXT_FILTER SecurityContextPersistenceFilter http
CONCURRENT_SESSION_FILTER ConcurrentSessionFilter session-management/concurrency-control
HEADERS_FILTER HeaderWriterFilter http/headers
CSRF_FILTER CsrfFilter http/csrf
LOGOUT_FILTER LogoutFilter http/logout
X509_FILTER X509AuthenticationFilter http/x509
PRE_AUTH_FILTER AbstractPreAuthenticatedProcessingFilter Subclasses N/A
CAS_FILTER CasAuthenticationFilter N/A
FORM_LOGIN_FILTER UsernamePasswordAuthenticationFilter http/form-login
BASIC_AUTH_FILTER BasicAuthenticationFilter http/http-basic
SERVLET_API_SUPPORT_FILTER SecurityContextHolderAwareRequestFilter http/@servlet-api-provision
JAAS_API_SUPPORT_FILTER JaasApiIntegrationFilter http/@jaas-api-provision
REMEMBER_ME_FILTER RememberMeAuthenticationFilter http/remember-me
ANONYMOUS_FILTER AnonymousAuthenticationFilter http/anonymous
SESSION_MANAGEMENT_FILTER SessionManagementFilter session-management
EXCEPTION_TRANSLATION_FILTER ExceptionTranslationFilter http
FILTER_SECURITY_INTERCEPTOR FilterSecurityInterceptor http
SWITCH_USER_FILTER SwitchUserFilter N/A

这里我们要关注的就是这个UsernamePasswordAuthenticationFilter,顾名思义,它是filter,在执行/login请求的时候拦截,因而是不需要工程里头去定义login的request mapping的。

UsernamePasswordAuthenticationFilter

spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/authentication/UsernamePasswordAuthenticationFilter.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
25
26
27
28
29
30
31
32
33
复制代码public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {

public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
}
//......
}

这里就是拦截,获取login.html提交的参数,然后交给authenticationManager去认证。之后就是走后续的filter,如果成功,则会进行相应的session配置。

doc

  • spring security动态配置url权限
  • Spring Security笔记:自定义Login/Logout Filter、AuthenticationProvider、AuthenticationToken

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Apache Pulsar中的地域复制,第2篇:模式和实践

发表于 2017-11-24

在上一篇文章中,曾经概括介绍了Apache Pulsar的地域复制功能。Apache Pulsar可以使用Apache BookKeeper提供的可伸缩流存储,这是一种可跨越多个数据中心,同时支持同步地域复制(借助Apache
BookKeeper)和异步地域复制(在Pulsar Broker的层面上配置)的消息系统。本文将介绍跨越多个数据中心配置地域复制的几种常见模式。

异步地域复制简介

首先想简要介绍一下Apache Pulsar中异步地域复制的工作原理。图1展示了在3个数据中心之间为Apache Pulsar配置的全连接(Full-mesh)地域复制。


图1:三个数据中心之间为Apache
Pulsar配置的全连接地域复制

在上图中,无论生产方(Producer)P1、P2和P3在什么时候分别将消息发布给Cluster A、Cluster B和Cluster C中的主题T1,这些消息均会立刻复制到整个集群。一旦完成复制,消费者(Consumer)C1和C2即可从自己所在的集群消耗这些消息。

Pulsar中的地域复制可针对每个租户启用,但只有在租户能访问所有集群的情况下,才能在这些集群之间启用地域复制。复制是在名称空间的层面上管理的,借此即可在租户能够访问的两个或更多已配置的集群中创建并配置要复制的名称空间。随后,发布到这种名称空间内任何主题中的消息都会复制到所配置的所有集群中。

3个命令创建全连接地域复制

只需要几条命令即可轻松地在Apache Pulsar中启用地域复制(与现有消息系统截然不同,不需要任何额外的复制程序或镜像程序即可实现地域复制)。下文将尝试在Pulsar集群之间配置全连接地域复制。

假设有三个数据中心:us-west、eu-central和apac-australia。开始操作前,需要在Pulsar中创建一个资产(租户),并为该租户提供访问所有这些数据中心的权限。

1
2
3
复制代码__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__$ bin/pulsar-admin properties create my-property \
--admin-roles my-admin-role \
--allowed-clusters us-west,eu-central,apac-australia__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__

随后即可针对名称空间配置复制。先创建一个名为full-mesh的名称空间:

1
复制代码__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__$ bin/pulsar-admin namespaces create my-property/global/full-mesh__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__

创建后的名称空间并未分配给任何集群,我们需要将其分配给一个或多个集群,为此可使用set-clusters命令。下列命令可将该名称空间分配给全部的三个可用集群。

1
2
复制代码__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__$ bin/pulsar-admin namespaces set-clusters my-property/global/full-mesh \
--clusters us-west,eu-central,apac-australia__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__

运行完这三条命令后,三个数据中心之间就建立了全连接的地域复制。随后,任何数据中心内产生的消息都可自动复制到另外两个数据中心以供使用。

如果随后需要更改复制设置,例如公司建成了第四个数据中心,或关闭了某个现有数据中心,我们可以随时更改复制设置,同时不会对流量产生任何影响。只要配置改动应用到数据中心,所有集群的复制渠道会立刻做出调整或停用。下列例子为full-mesh名称空间添加了第四个数据中心:apac-china。

1
2
复制代码__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__$ bin/pulsar-admin namespaces set-clusters my-property/global/full-mesh \
--clusters us-west,eu-central,apac-australia,apac-china__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__

选择性复制每条消息

默认情况下,全连接名称空间会在集群之间创建全连接复制,消息会复制到该名称空间配置的所有集群。但也可以对复制进行选择性地限制,在应用程序层面上直接为消息指定要使用的复制列表。随后这些消息将只复制到复制列表中定义的子集范围内。

下列代码可以使用Pulsar Java客户端生成一条消息,并只复制到apac-australia数据中心。

1
2
3
4
5
6
7
8
复制代码__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__List<String> restrictDatacenters = Lists.newArrayList("apac-australia");

Message message = MessageBuilder.create()
…
.setReplicationClusters(restrictDatacenters)
.build();

producer.send(message);__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__

地域复制的不同模式

通过使用全连接复制,并在发送消息时应用选择性复制信息,即可获得极大的灵活性,借此在任何数量的数据中心之间运行几条命令,即可打造任何类型的复制拓扑。


图2:跨越三个数据中心的全连接地域复制

除了全连接地域复制,还可以使用其他几种复制模式。

双活复制

这是一种特殊形态的全连接地域复制,只使用两个数据中心,生产者可在任一数据中心中运行并生成消息,消费者也可以在任何数据中心内消耗这些消息。

主备复制

这是一种特殊类型的双活复制。生产者在主数据中心内生成消息,消息复制到备用数据中心但仅仅是为了备份。当主数据中心故障后,备用数据中心将接手并成为主数据中心。随后即可让生产者在备用数据中心(但现在已成为主数据中心)生成消息。


图3:主备地域复制的配置

汇聚复制

有时候我们可能需要从多个前端数据中心复制消息到一个中央数据中心以实现汇聚。例如,假设有三个前端数据中心:front1、front2和front3,以及一个汇聚数据中心(假设叫aggregated)。随后为front1数据中心使用的主题创建名称空间front1-aggregation,为front2数据中心使用的主题创建名称空间front2-aggregation,并为front3数据中心使用的主题创建名称空间front3-aggregation。最后可将aggregated数据中心分配到这些名称空间。


图4:跨越四个数据中心的汇聚地域复制

汇聚和边缘计算

汇聚模式通常会被用于将物联网消息从边缘复制到云端,Pulsar可以非常轻松地支持这种做法。该技术的灵活性以及高效实用的复制管理工具使得该技术成为边缘计算的关键。借此,用户完全不再需要设置非常繁琐的复制程序或镜像程序。

地域复制的最佳实践

除了自行设置地域复制架构过程中的常规指导,建议大家能考虑测下列这些最佳实践。

密切监视复制积压

Pulsar集群的监视对业务很重要。最重要的是,在使用地域复制的情况下,产生网络分区,或多个数据中心之间网络性能的退化的可能性会远远高过只使用一个数据中心的时候。因此一定要密切监视复制状态,尤其要注意是否存在复制积压。

复制积压是指源集群已经生成了一系列消息,但尚未复制到其他远程集群。我们必须监视复制积压的主要原因有两个:

  1. 如果有必要从源数据中心故障转移至目标数据中心,那么所有在源位置生成但尚未复制到目标位置的消息将暂时不可用,直到源数据中心重新上线才能恢复。
  2. 目标位置进行的任何消息处理工作都会因为积压而产生延迟。

通常只会积压几条消息(取决于数据中心之间的网络延迟),但如果存在网络分区,数量可能会增多。如果积压数量持续增长,这代表复制吞吐率低于源集群产生新消息的速率(例如源集群中的生产者正在以100 MB/s的速度产生消息,而Pulsar复制消息的速度只能达到50 MB/s),此时有必要在远程数据中心增加更多Broker,或增加数据中心之间的带宽。

有关复制状态如何监视,具体监视社么的详细信息,请阅读有关Pulsar状态的文档。

数据中心故障后的容量规划

在为多个数据中心设计地域复制规划时,另一个重要问题是必须确保,如果一个或多个数据中心故障,消息(Apache Pulsar中的Broker)以及存储组件(Apache BookKeeper中的Bookie)能够承受足够长时间的构建积压,时间可能从数小时到数天不等。至少要保证故障转移集群具备足够的容量以存储故障转移过来的所有流量。

另外需要注意,当跨数据中心网络的故障成功解决后,必须要能提供足够的网络或I/O带宽,以便能用比新消息生成更快的速度尽快排空积压的消息,同时不能对现有流量产生影响。

复制的限流调节

当一个数据中心出现故障后,目标集群会积累大量消息。当故障的数据中心恢复后,Pulsar需要将目标集群积累的消息重新复制回源集群。由于积压的大量内容需要通过复制排空,因此需要密切注意积压内容的排空速率,并为重要主题的I/O配置高优先级,对不那么重要的主题进行限流调节。这样即可确保业务可以尽快恢复至常态。

例如,可以这样在名称空间中针对每个主题限制排空速率:

1
2
复制代码__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__$ bin/pulsar-admin namespaces set-dispatch-rate my-property/global/full-mesh \
--msg-dispatch-rate 20000__Fri Nov 24 2017 10:19:52 GMT+0800 (CST)____Fri Nov 24 2017 10:19:52 GMT+0800 (CST)__

结论

本文介绍了Apache Pulsar中异步地域复制的用法,并提供了相关的最佳实践。希望本文可以帮助大家更好地理解Apache Pulsar及其地域复制功能。下篇文章将会介绍Apache Pulsar中的持久消息功能和Effectively-once语义。

如果对Pulsar感兴趣,大家可以通过下列方式加入Pulsar社区:

  • Pulsar Slack频道,可在这里自行注册:apache-pulsar.herokuapp.com。
  • Pulsar邮件列表。

有关Apache Pulsar项目的常规信息,请访问官网:pulsar.incubator.apache.org/,并可关注该项目的Twitter帐号:@apache_pulsar。

作者:Sijie Guo,阅读英文原文:Geo-replication in Apache Pulsar, part 2: patterns and practices

感谢杜小芳对本文的审校。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

还不明白 Docker 的镜像跟容器?

发表于 2017-11-23

一、docker 是什么

Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的、可移植的、自给自足的容器。开发者在自己机器上编译测试通过的容器可以批量地在生产环境中部署,包括VMs(虚拟机)、bare metal、OpenStack 集群和其他的基础应用平台。这是docker的百度百科定义,看完这个好像并不能说明docker有什么用,没关系,对比一下就知道了,首先说明,这篇文章不会涉及很深奥的内容,旨在用通俗易懂的话语来阐述docke的优点及其基础用法。我们都用过虚拟机吧,第一映像肯定是笨重,启动慢,当然功能上是完全无可挑剔的,但这还不够,于是docker横空出世,还有什么语言能比图片更能说明一切呢?

docker

docker

上面这张图就很好的说明一切了,传统的虚拟机是通过软件把计算机的一个个硬件给模拟出来,之后再在模拟出的计算机中安装系统,之后再在系统内部跑应用,就比如需要两个应用,一个跑Nginx一个跑MySQL,那么用虚拟机的话就要安装两次系统,宿主机也就是真实的非虚拟出来的计算机要保存这两个虚拟的系统,占用空间不说最主要的是给宿主机额外的开销,这两个虚拟机的系统都是需要cpu和内存资源的,再来看看docke,可看到的是它只在宿主机内安装个Docker Engine就支持隔离多个应用,注意还是环境隔离的哦,少了两个OS无疑是节约了巨大的资源,这两个应用给我们的感觉就是跑在两个不同的系统中一样,两者通信还要借助特定网络通信,但他们其实是共用的宿主机的系统,至于他们是怎么做到隔离的这又是一个高深的话题了,看到docker有这么多的优点是不是迫不及待的想要试一试了?

二、docker安装及基本配置

在win7中安装是要通过虚拟机来安装的,在win10中就方便多了,有Docker for Windows安装包,linux下下载对应源码编译安装即可,因为平时开发都是在win10上开发,故在这就简单说一下win10安装Docker for Windows后的配置吧,注意在win10中安装docker for window需要有 Hyper-V 支持,不支持这个的也只能是安装虚拟机来体验docker了,安装完后进入设置界面:

docker_settings

docker_settings

给需要共享的磁盘勾选,只有勾选了这个,容器才能访问宿主机的文件,设置这个是需要管理员密码的,没设密码的需要去设置管理员密码,不然不能配置成功,接着就是配置镜像加速了,docker容器是依赖镜像(在docker里面是image)创建的,镜像是从远程拉取的,就跟我们从github上拉取代码一样的,众所周知国内的网路想要拉取一个镜像是多么的不容易,所以就需要配置镜像加速,在Daemon那一栏的Registry mirrors 中填上加速地址就行了,我习惯是使用阿里云的 ‘ otsyn80i.mirror.aliyuncs.com ‘ 加速地址,一切都ok了之后就可以开始了我们喜欢的命令行敲命令了!

三、docker基本操作

1
2
复制代码 ->docker --version  #查看版本
Docker version 17.09.0-ce, build afdb6d4

搜索一个image并且拉取下来
下载镜像的命令非常简单,使用docker pull命令即可。在docker的镜像网站上面,镜像都是按照”用户名/镜像名”的方式来存储的。
可以使用docker images查看拉取下来的镜像,其中IMAGE ID就是镜像id,删除镜像时可以使用这个IMAGE ID删除 docker rmi [IMAGE ID,REPOSITORY]

1
2
3
4
5
6
7
8
9
复制代码 ->docker search centos
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
centos The official build of CentOS. 3824 [OK]
...
->docker pull centos #拉取镜像
->docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest a7876479f1aa 4 years ago 128MB
->docker rmi a7876479f1aa #删除image

在docker容器中运行hello world,使用docker run创建一个容器,docker run之后就会产生一个容器并且保存起来,之后要运行就可以做直接运行容器而不用从新创建容器,这里的容器就是 container,docker的容器可以理解为在沙盒中运行的进程。这个沙盒包含了该进程运行所必须的资源,包括文件系统、系统类库、shell 环境等等。但这个沙盒默认是不会运行任何程序的。需要在沙盒中运行一个进程来启动某一个容器。这个进程是该容器的唯一进程,所以当该进程结束的时候,容器也会完全的停止。
可以通过 docker ps 查看正在运行的容器,当我们运行docker run centos echo “hello word”后看到输出 “hello word” 后再运行 docker ps 时没有任何容器输出出来,这是因为容器在运行完 echo “hello word” 后没有运行任何进程,所以容器就退出了。通过 docker ps -a 可以查看到所有的 container

1
2
3
4
5
复制代码 ->docker run centos echo "hello word" #通过 centos 镜像创建一个容器来输出 "hello word"
hello word
->docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e46ec634bbe1 centos "echo 'hello word'" 12 minutes ago Exited (0) 12 minutes ago

接下来就是给容器安装一个简单的程序。之前下载的是centos镜像,所以可以使用 yum 命令来安装程序。
备注:yum 命令执行完毕之后,容器就会停止,但对容器的改动不会丢失。但是从 image 重新 run 一个容器出来,之前安装的程序并不存在,因为所有的修改都是针对容器 (container) 的,并不针对 image,所以从 image run 出来的容器都是一个全新的

1
2
3
4
5
复制代码->docker run centos yum -y install net-tools
->docker ps -a #可以看到已经存在两个 container
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2f1bbd6c30e3 centos "yum install -y ne..." 1 minutes ago Exited (0) 1 minute ago competent
35129633c933 centos "echo hello word" 1 minutes ago Exited (0) 1 minute ago dazzling

现在容器是有了,需要的程序也安装好了,但就是只有一个容器,不能运行又有什么用,别急下面就来看看运行容器,但运行容器也有几种方法,一种就是在创建容器的时候并运行,之前说过,容器要保持运行就需要一个活动的进程,当容器内所有的进程都退出了,那么容器也就相应的停止了,所以要创建容器并保持运行容器的话很简单,用ping命令就可以了。

1
2
3
4
5
6
7
8
复制代码->docker run centos ping lozz.cc
PING lorencoll.coding.me (103.218.240.147) 56(84) bytes of data.
64 bytes from 103.218.240.147 (103.218.240.147): icmp_seq=1 ttl=37 time=16.0 ms
64 bytes from 103.218.240.147 (103.218.240.147): icmp_seq=2 ttl=37 time=16.4 ms
Ctrl+C
->docker ps #查看活动的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10c5c2754279 centos "ping lozz.cc" About a minute ago Up About a minute jolly

有启动就会有停止,停止可以用 docker stop container_id 命令来停止某个容器,好了现在开始讲第二种启动容器的方法,这种方法是在容器已经被创建的情况下使用的,我们知道每次 run 都是创建出一个新的容器,有的时候不需要创建,可以使用 docker start container_id 来启动一个容器,这时就需要注意了,启动后的容器没活动的进程容器依然是会退出的,因为容器是从镜像创建而来的,所以容器也是包含了创建这个容器的附加命令的,在 docker start 的时候又会从新执行一遍创建这个容器的附件命令,有点绕口是不是,敲一遍就知道了。

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
复制代码->docker ps    #查看活动的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10c5c2754279 centos "ping lozz.cc" 23 minutes ago Up 23 minutes jolly

->docker ps -a #查看所有存在的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10c5c2754279 centos "ping lozz.cc" 21 minutes ago Up 21 minutes jolly
2f1bbd6c30e3 centos "yum install -y ne..." 36 minutes ago Exited (0) 3 minute ago competent
35129633c933 centos "echo hello word" About hour ago Exited (0) hour ago dazzling

# 尝试启动安装了 net-tools 的容器,
# -i 的意思是把信息输出到控制台中,没有这个参数执行后就直接返回一个CONTAINER ID
->docker start -i 2f1bbd6c30e3
Loaded plugins: fastestmirror, ovl
Loading mirror speeds from cached hostfile
* base: mirrors.aliyun.com
* extras: mirrors.aliyun.com
* updates: mirrors.cn99.com
Package net-tools-2.0-0.22.20131004git.el7.x86_64 already installed and latest version
Nothing to do
->docker ps #查看活动的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10c5c2754279 centos "ping lozz.cc" 30 minutes ago Up 30 minutes jolly
->docker stop 10c5c2754279 #停止执行ping命令的容器
10c5c2754279
->docker start -i 10c5c2754279 #启动执行ping命令的容器
PING lorencoll.coding.me (103.72.144.62) 56(84) bytes of data.
64 bytes from 103.72.144.62 (103.72.144.62): icmp_seq=1 ttl=37 time=15.1 ms
64 bytes from 103.72.144.62 (103.72.144.62): icmp_seq=2 ttl=37 time=16.3 ms
64 bytes from 103.72.144.62 (103.72.144.62): icmp_seq=3 ttl=37 time=16.8 ms
Ctrl+C

从上面的提示就可以知道,这个 start 命令把之前创建命令 docker run centos yum -y install net-tools 中的 yum -y install net-tools 也执行了一遍,因为这个容器已经安装过了 net-tools ,所以 yum 才提示不需要安装,查看活动的容器中还是只有之前的容器在运行。那么现在有一个问题,我安装 net-tools 的那个容器不能用了吗,其实不然,这个容器之所以“不能用”只是因为创建的时候没有指定合适的命令,要用它也是可以的,我们把他从新打包成一个 image 也就是镜像,打包好了后再通过它生成容器就可以了,实际上这也符合 Docker 的应用隔离思想,把容器打包成镜像的命令很简单 docker commit container_id image_name,我的打包命令是 docker commit 2f1bbd6c30e3 lorenwe/centos_net_tools,这个命名是有规矩的,前面是 docker 的用户id,如果想要把这个镜像推送到 docker hub 这个名字就不能随便命名了,如果不推送到 docker hub 就无所谓了,但也不能太随便对不对。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码->docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10c5c2754279 centos "ping lozz.cc" About an hour ago jolly
2f1bbd6c30e3 centos "yum install -y ne..." About an hour ago competent
35129633c933 centos "echo hello word" About an hour ago dazzling
->docker commit 2f1bbd6c30e3 lorenwe/centos_net_tools
sha256:35f8073cede14473601d9f138a9815bc9ab5c7d97f914ca2f5ce910bd78b5750
->docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
lorenwe/centos_net_tools latest 35f8073cede1 23 seconds ago 277MB
centos latest d123f4e55e12 2 weeks ago 197MB
d4w/nsenter latest 9e4f13a0901e 14 months ago 83.8kB

就这么简单,一个新的镜像(image)就创建好了,现在可以通过这个镜像来做些有趣的事情了,依然是 docker run 命令,只不过这次多增加一些参数,如 docker run -itd –name my_net_tools lorenwe/centos_net_tools /bin/bash
其中参数 itd 分别是表示 ‘标准输入给容器’,‘分配一个虚拟终端’,‘以守护进程方式运行(后台)’, –name 自然是指定创建后容器的名称了,/bin/bash 执行bash脚本,执行以上命令,就能创建一个后台运行的容器了。

1
2
3
4
5
6
复制代码->docker run -itd --name my_net_tools lorenwe/centos_net_tools /bin/bash
e1d843f7726f67d2635042695e2065b383736a341edd2e83753be9fabec03de0
->docker ps #查看活动的容器
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e1d843f7726f lorenwe/centos_net_tools "/bin/bash" 7 seconds ago my_net_tools
10c5c2754279 centos "ping lozz.cc" Up 25 minutes jolly

嗯,运行了,然后呢,当然是可以进入容器中玩玩呀,使用 docker attach container_id 进入Docker容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码->docker attach e1d843f7726f
[root@e1d843f7726f /]# ip addr
bash: ip: command not found
[root@e1d843f7726f /]# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.3 netmask 255.255.0.0 broadcast 0.0.0.0
ether 02:42:ac:11:00:03 txqueuelen 0 (Ethernet)
RX packets 40 bytes 1900 (1.8 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
loop txqueuelen 1 (Local Loopback)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

[root@e1d843f7726f /]#

这个因为这个容器是基于 centos 来的,而 docker 的基础 centos 镜像都是精简版的,故很多命令没有,比如这个 ip addr ,这也是之前选择安装 net-tools 的原因,因为安装了这个就可以使用 ifconfig 来查看网卡配置。至此,是不是对 docker 有了一个基本的了解了呢,是不是突然来了一个灵感,比如想要搭建一个 Nginx 静态服务器,步骤就是 pull 一个基础镜像下来,通过这个基础镜像 run 出一个容器后再容器内安装上 Nginx 之后再从新打包成一个新的镜像就可以了。实际上使用 docke 构建镜像不会那样去做,步骤繁琐不说,还不是很灵活,毕竟之前构建的 lorenwe/centos_net_tools 镜像也有277MB呢!分发起来不太方便,于是 Dockerfile 出现了,其作用就是通过特定的格式把一个镜像描述出来,通过 docker 来构建出这个镜像,描述的流程其实和之前的手动构建的流程差不多,通过同一个 Dockerfile 可以构建出一模一样的环境,这也是 docke 常用于工作中统一系统运行环境的原因。

四、把 docker 推送到 docker hub

推送镜像的命令很简单,只需要 docker push image_name 就可以了,在把镜像推送到 docker hub 之前还是有一些准备工作要做的,需要先去 docker hub 注册一个账号,之后再在 docker 中登入这个账号就可以推送镜像了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码->docker login  #登入到 docker hub
Login with your Docker ID to push and pull images from Docker Hub.
If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username (lorenwe): lorenwe
Password:
Login Succeeded
# 现在就可推送镜像到 docker hub
->docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
lorenwe/centos_net_tools latest 35f8073cede1 About an hour ago 277MB
centos latest d123f4e55e12 2 weeks ago 197MB
d4w/nsenter latest 9e4f13a0901e 14 months ago 83.8kB
->docker push lorenwe/centos_net_tools
The push refers to a repository [docker.io/lorenwe/centos_net_tools]
d0ba94ecc37e: Pushed
cf516324493c: Mounted from library/centos
latest: digest: sha256:276814315437cf5d416ed4b5713fe10c914beaea96bcf583b786a6778c80830f size: 741

由于墙和 docker hub 的服务器离天朝远的原因推送会很慢,成功后就能在 docker hub 的个人中心看到自己推送的镜像了,以后想要在使用这个镜像就可以直接 docker pull 就行了,也可以把镜像推送到国内的容器镜像服务平台,比如我用的是阿里云的容器Hub,可能是因为速度快吧所以用着舒服了,唯一不好的就是那又臭又长的镜像名称有点难受。

五、结语

docker 基础到这一步已经是介绍的差不多了,写这篇文章的目的就是用通俗易懂的语言及示例来讲明 docker 镜像与容器之间的关系,限于篇幅 docker 还有很多功能没有介绍,日后有时间再来做后续的文章,例如 Dockerfile,docker 卷的共享和网络通信等等都是一些特别有意思的功能,模拟构建一个可移植的分布式的开发平台已经不是梦了。
处女作,还望轻喷!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

日志 分布式系统的核心 什么是日志? 日志的应用 结语

发表于 2017-11-23

最近这段时间一直在研究消息队列、文件系统、数据库等,慢慢的发现他们都有一个核心组件:日志.有时也叫write-ahead logs 、commit logs 或者事物 logs, 通常指在应用所有的修改之前先写入日志,一般会将重放日志、撤销日志都写进去。 我们经常听到很多名词,NoSQL数据库、KV存储、Hadoop、raft、paxos 以及版本控制等等,这些中间件或者协议本质上都或多或少依赖于日志,可以发现日志一直都在分布式系统中扮演者非常重要的角色。

什么是日志?

日志就是按照时间顺序追加的、完全有序的记录序列,其实就是一种特殊的文件格式,文件是一个字节数组,而这里日志是一个记录数据,只是相对于文件来说,这里每条记录都是按照时间的相对顺序排列的,可以说日志是最简单的一种存储模型,读取一般都是从左到右,例如消息队列,一般是线性写入log文件,消费者顺序从offset开始读取。
由于日志本身固有的特性,记录从左向右开始顺序插入,也就意味着左边的记录相较于右边的记录“更老”, 也就是说我们可以不用依赖于系统时钟,这个特性对于分布式系统来说相当重要。
1511274263284


日志的应用

日志在数据库中的应用

日志是什么时候出现已经无从得知,可能是概念上来讲太简单。在数据库领域中日志更多的是用于在系统crash的时候同步数据以及索引等,例如MySQL中的redo log,redo log是一种基于磁盘的数据结构,用于在系统挂掉的时候保证数据的正确性、完整性,也叫预写日志,例如在一个事物的执行过程中,首先会写redo log,然后才会应用实际的更改,这样当系统crash后恢复时就能够根据redo log进行重放从而恢复数据(在初始化的过程中,这个时候不会还没有客户端的连接)。日志也可以用于数据库主从之间的同步,因为本质上,数据库所有的操作记录都已经写入到了日志中,我们只要将日志同步到slave,并在slave重放就能够实现主从同步,这里也可以实现很多其他需要的组件,我们可以通过订阅redo
log 从而拿到数据库所有的变更,从而实现个性化的业务逻辑,例如审计、缓存同步等等。

日志在分布式系统中的应用

image
分布式系统服务本质上就是关于状态的变更,这里可以理解为状态机,两个独立的进程(不依赖于外部环境,例如系统时钟、外部接口等)给定一致的输入将会产生一致的输出并最终保持一致的状态,而日志由于其固有的顺序性并不依赖系统时钟,正好可以用来解决变更有序性的问题。
我们利用这个特性实现解决分布式系统中遇到的很多问题。例如RocketMQ中的备节点,主broker接收客户端的请求,并记录日志,然后实时同步到salve中,slave在本地重放,当master挂掉的时候,slave可以继续处理请求,例如拒绝写请求并继续处理读请求。日志中不仅仅可以记录数据,也可以直接记录操作,例如SQL语句。
image
日志是解决一致性问题的关键数据结构,日志就像是操作序列,每一条记录代表一条指令,例如应用广泛的Paxos、Raft协议,都是基于日志构建起来的一致性协议。
image

日志在Message Queue中的应用

日志可以很方便的用于处理数据之间的流入流出,每一个数据源都可以产生自己的日志,这里数据源可以来自各个方面,例如某个事件流(页面点击、缓存刷新提醒、数据库binlog变更),我们可以将日志集中存储到一个集群中,订阅者可以根据offset来读取日志的每条记录,根据每条记录中的数据、操作应用自己的变更。
这里的日志可以理解为消息队列,消息队列可以起到异步解耦、限流的作用。为什么说解耦呢?因为对于消费者、生产者来说,两个角色的职责都很清晰,就负责生产消息、消费消息,而不用关心下游、上游是谁,不管是来数据库的变更日志、某个事件也好,对于某一方来说我根本不需要关心,我只需要关注自己感兴趣的日志以及日志中的每条记录。
image

我们知道数据库的QPS是一定的,而上层应用一般可以横向扩容,这个时候如果到了双11这种请求突然的场景,数据库会吃不消,那么我们就可以引入消息队列,将每个队数据库的操作写到日志中,由另外一个应用专门负责消费这些日志记录并应用到数据库中,而且就算数据库挂了,当恢复的时候也可以从上次消息的位置继续处理(RocketMQ和Kafka都支持Exactly Once语义),这里即使生产者的速度异于消费者的速度也不会有影响,日志在这里起到了缓冲的作用,它可以将所有的记录存储到日志中,并定时同步到slave节点,这样消息的积压能力能够得到很好的提升,因为写日志都是有master节点处理,读请求这里分为两种,一种是tail-read,就是说消费速度能够跟得上写入速度的,这种读可以直接走缓存,而另一种也就是落后于写入请求的消费者,这种可以从slave节点读取,这样通过IO隔离以及操作系统自带的一些文件策略,例如pagecache、缓存预读等,性能可以得到很大的提升。
image

分布式系统中可横向扩展是一个相当重要的特性,加机器能解决的问题都不是问题。那么如何实现一个能够实现横向扩展的消息队列呢? 加入我们有一个单机的消息队列,随着topic数目的上升,IO、CPU、带宽等都会逐渐成为瓶颈,性能会慢慢下降,那么这里如何进行性能优化呢?

  1. topic/日志分片,本质上topic写入的消息就是日志的记录,那么随着写入的数量越多,单机会慢慢的成为瓶颈,这个时候我们可以将单个topic分为多个子topic,并将每个topic分配到不同的机器上,通过这种方式,对于那些消息量极大的topic就可以通过加机器解决,而对于一些消息量较少的可以分到到同一台机器或不进行分区
  2. group commit,例如Kafka的producer客户端,写入消息的时候,是先写入一个本地内存队列,然后将消息按照每个分区、节点汇总,进行批量提交,对于服务器端或者broker端,也可以利用这种方式,先写入pagecache,再定时刷盘,刷盘的方式可以根据业务决定,例如金融业务可能会采取同步刷盘的方式。
  3. 规避无用的数据拷贝
  4. IO隔离
    image

结语

日志在分布式系统中扮演了很重要的角色,是理解分布式系统各个组件的关键,随着理解的深入,我们发现很多分布式中间件都是基于日志进行构建的,例如Zookeeper、HDFS、Kafka、RocketMQ、Google Spanner等等,甚至于数据库,例如Redis、MySQL等等,其master-slave都是基于日志同步的方式,依赖共享的日志系统,我们可以实现很多系统: 节点间数据同步、并发更新数据顺序问题(一致性问题)、持久性(系统crash时能够通过其他节点继续提供服务)、分布式锁服务等等,相信慢慢的通过实践、以及大量的论文阅读之后,一定会有更深层次的理解。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring框架整理

发表于 2017-11-23

Spring系列文章

Spring框架-1(基础)
Spring框架-2(IOC上)
Spring框架-3(IOC下)
Spring框架-4(AOP)
Spring框架-5(JDBC模板&Spring事务管理)
Spring框架-6(SpringMvc)
Spring框架-7(搭建SSM)

Spring框架-8(SpringMVC2)

介绍

Spring是一个分层的(一站式) 轻量级开源框架 Spring的核心是控制反转(IoC)和面向切面(AOP)

为什么说分层一站式呢?

javaEE分三层开发 WEB层,业务层,持久层。在ssh整合框架中s == Struts2, s == spring,h == Hibernate ,spring 的一站式开发就是不用struts2 和hibernate,在spring中有SpringMvc可以替代Struts2,springJDBC可以替代Hibernate。等于一个spring框架可以快速开发JavaEE应用。关于轻量级就不太多说了,spring整个框架打包出来也才1M多的内存大小。spring运行中的消耗也不大。那肯定是轻量级的框架。

Spring的模块

组成 Spring 框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现。每个模块的功能如下:

image
image

  1. 核心容器:核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转 (IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。
  2. Spring 上下文:Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能。
  3. Spring AOP:通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中。
  4. Spring DAO:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。
  5. Spring ORM:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。
  6. Spring Web 模块:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。
  7. Spring MVC 框架:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。

什么是IOC的功能?

概念

IoC – Inverse of Control,控制反转,将对象的创建权反转给Spring!! 使用IOC可以解决的程序耦合性高的问题!!

控制反转

假设我需要做一个功能,在这个功能当中我需要调用servic层,然后再调用dao层,去取数据。在传统的javaEE开发中我就直接去new一个service 然后再new一个dao。但是在spring框架中,我们吧new service和new dao的权利交个spring框架,假设我需要使用我就直接去spring框架中寻找。等于说我的资源创建的权利交给了spring框架,这就叫做控制反转。

解耦

刚刚我们说资源创建交给了sring,我们需要什么就找spring。这过程就像是工厂模式。但是在spring框架中它需要创建哪些对象,它需要一个配置文件。这个配置文件告诉spring,需要创建哪些资源。

例如:假设我需要去数据库查询数据显示页面

程序启动,spring框架去找配置文件创建资源,把资源放置再一个容器中,开始运行,前端请求数据,在spring中找controller层,再找service层,再找dao层要数据,最后数据原路返回controller,再显示到页面上。其中service被spring注入到controlller层,dao层被spring注入到service层。这个过程分工明确。每一层各司其职。传统的一个开发,在servlet中直接new然后去查数据,然后数据返回到界面上。万一操作一多所有的判断,查询不同的表,这个servlet的代码变得十分的臃肿。不说开发慢,你开发完了看代码也费劲。
所以说控制反转可以用来解耦


什么是面向切面(AOP)?

概念

  • 在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程
  • AOP是一种编程范式,隶属于软工范畴,指导开发者如何组织程序结构
  • AOP最早由AOP联盟的组织提出的,制定了一套规范.Spring将AOP思想引入到框架中,必须遵守AOP联盟的规范
  • 通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术
  • AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型
  • 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率

其实AOP可以在不修改源代码的前提下,对程序进行增强!!

Spring框架的AOP的底层实现

  1. Spring框架的AOP技术底层也是采用的代理技术,代理的方式提供了两种
  1. 基于JDK的动态代理
1
复制代码 必须是面向接口的,只有实现了具体接口的类才能生成代理对象
  1. 基于CGLIB动态代理
1
复制代码对于没有实现了接口的类,也可以产生代理,产生这个类的子类的方式
  1. Spring的传统AOP中根据类是否实现接口,来采用不同的代理方式
  1. 如果实现类接口,使用JDK动态代理完成AOP
  2. 如果没有实现接口,采用CGLIB动态代理完成AOP

JDK的动态代理

注意:需要实现类接口

例子:假设我有两个工作,工作1,工作2.

1
2
3
4
5
6
复制代码//写一个接口
public interface Working {
void wokingOne();

void WorkingTwo();
}
1
2
3
4
5
6
7
8
9
10
11
12
复制代码//接口实现类
public class WorkingImpl implements Working {
@Override
public void wokingOne() {
System.out.println("做任务1");
}

@Override
public void WorkingTwo() {
System.out.println("做任务2");
}
}

好的,现在我要先做任务1,然后再做任务2我们的写法为:

1
2
3
4
5
复制代码public static void main(String[] args) {
Working working = new WorkingImpl();
working.wokingOne();//做任务1
working.WorkingTwo();//做任务2
}

好的精彩的地方来了,我再做任务2之前我要先休息10分钟,但是不能修改源代码。怎么办呢?这时候就用到我们的JDK动态代理了。代码如下:

先写一个代理的工具类。再做任务2前我们休息十分钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public class MyProxyUtils {
public static Working getProxy(final Working working) {
// 使用Proxy类生成代理对象
Working proxy = (Working) Proxy.newProxyInstance(working.getClass().getClassLoader(),
working.getClass().getInterfaces(), new InvocationHandler() {

// 代理对象方法一直线,invoke方法就会执行一次
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//再做工作2之前我先休息10分钟
if ("WorkingTwo".equals(method.getName())) {
System.out.println("休息10分钟");
}
//工作继续进行下去
return method.invoke(working, args);
}
});
// 返回代理对象
return proxy;
}
}
1
2
3
4
5
6
7
复制代码 public static void main(String[] args) {
Working working = new WorkingImpl();
Working proxy = MyProxyUtils.getProxy(working);
proxy.wokingOne();
proxy.WorkingTwo();

}

运行的结果可想而知:

1
2
3
复制代码做任务1
休息10分钟
做任务2

CGLIB动态代理

注意:没有实现类接口

CGLIB也是一个java项目,所以要使用它就要引入CGLIB的开发的jar包,因为在Spring框架核心包(core)中已经引入了CGLIB的开发包了。所以直接引入Spring核心开发包即可

好我们同样使用上面的例子来做事例吧

1
2
3
4
5
6
7
8
9
10
11
复制代码//工作类
public class Working {

public void wokingOne() {
System.out.println("做任务1");
}

public void WorkingTwo() {
System.out.println("做任务2");
}
}

new一个对象调用任务1 任务2得到的结果这个就没必要说了。我们重点看下怎么使用CGLIB来在不改变源码的情况下,做任务之前休息十分钟。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public static Working getProxy(){
// 创建CGLIB核心的类
Enhancer enhancer = new Enhancer();
// 设置父类
enhancer.setSuperclass(Working.class);
// 设置回调函数
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
if("WorkingTwo".equals(method.getName())){
// 休息10分钟
System.out.println("休息10分钟...");
}
return methodProxy.invokeSuper(obj, args);
}
});
// 生成代理对象
Working proxy = (Working) enhancer.create();
return proxy;
}
1
2
3
4
5
6
7
复制代码 public static void main(String[] args) {
Working working = new WorkingImpl();
Working proxy = MyCglibUtils.getProxy(working);
proxy.wokingOne();
proxy.WorkingTwo();

}

最后的运行结果也是相同的:

1
2
3
复制代码做任务1
休息10分钟
做任务2

分析

我上面简单分析了一下spring AOP的底层实现的两种方式:JDK动态代理,CGLIB。在Spring框架中它会自动选择使用哪一种方式。如果有接口实现类,那就使用jdk动态代理,没有接口就使用CGLIB。有了这个功能那么我们修改东西就方便多了。假设我再做任务一前我需要记录下日志。我就直接写一个切面类,直接去记录日志。都不用修改本身的源码。

结尾

作者刚刚学习spring中,记录一些知识点,梳理了一下自己的理解,如有错误希望各位大神提出。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…933934935…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%