Akka学习指南(Java版)——4.构建分布式系统Demo

1.目标

我们将构建一个服务和一个客户端。也就是数据库和与之通信的数据库客户端。要通过网络在客户端和服务之间发送消息,我们的两个项目需要共享相同的消息。

首先,我们将扩展前面的服务器项目,定义希望数据库接受的所有消息。接着,我们将针对这些消息分别实现数据库中的相关功能。

在构建了这些基本操作之后,我们将编写一个 main()方法来运行数据库。启动应用程序后,我们将构建一个 ActorSystem 以及一个在该 ActorSystem 中的 Actor,这就构成了我们的第一个 Akka 微服务。

我们还将创建一个数据库客户端,用于展示如何请求服务器,以及如何从远程 Actor中获取 Future。服务器端的服务接收到客户端的请求后将返回 Future。这样我们就已经编写了一个可以使用的键值存储数据库(和 redis 很类似)以及一个可以使用该数据库的远程客户端。
在这里插入图片描述

2.准备数据库与消息

首先,我们要构造几种消息。

  • Get 消息:如果 key 存在,就返回 value;
  • Key Not Found 异常消息:如果 key 不存在,就返回该异常;
  • Set 消息:设置某个键值对,返回状态。

我们将在服务器端实现这些消息及其行为,以及用于启动该数据库的 main 函数。需要注意的是,我们将使用前面的demo项目,并在这基础上添加本章介绍的功能,比如返回响应及失败情况的处理。

2.1.消息

由于我们将在通过网络连接的独立应用程序之间远程发送消息,因此需要能够对所有的消息进行序列化,这样 Akka 就能够将表示这些消息的对象转换成能够在网络应用程序之间传输的表示形式。我们将实现 SetRequest,GetRequest 和 KeyNotFound-Exception。

public class SetRequest implements Serializable {
	public final String key;
	public final Object value;
	public SetRequest(String key, Object value) {
		this.key = key;
		this.value = value;
	}
}
public class GetRequest implements Serializable {
	public final String key;
	public GetRequest(String key) {
		this.key = key;
	}
}
public class KeyNotFoundException extends Exception implements
		Serializable {
	public final String key;
	public KeyNotFoundException(String key) {
		this.key = key;
	}
}

这些都是很简单的类。由于消息是不可变的,所以我们没有使用 Java 的 getter 方法,而是直接将成员变量设为 public。当然如果读者想要添加 getter 方法的话也可以。

注意:消息始终都应该是不可变的。

2.2.实现数据库功能

请求获取的键值不包含在键值存储中时返回失败响应。

下面是 Java 的 createReceive 语句:

createReceive(receiveBuilder()
		.match(SetRequest.class, message -> {
		log.info("Received Set request: {}", message);
		map.put(message.key, message.value);
		sender().tell(new Status.Success(message.key), self());
		})
		.match(GetRequest.class, message -> {
		log.info("Received Get request: {}", message);
		String value = map.get(message.key);
		Object response = (value != null)
		? value
		: new Status.Failure(new KeyNotFoundException(message.key));
		sender().tell(response, self());
		})
		.matchAny(o ->
		sender().tell(new Status.Failure(new ClassNotFoundException()), self())
		)
		.build()
		);

如果 Actor 接收到一个SetRequest,就将键值存储到 map 中。如果接收到的是 GetRequest,Actor 就会尝试从 map 中获取结果。如果找到 key,就将 value 返回。如果没有找到,就返回一个包含 KeyNotFoundException 的失败消息。最后,当接收到未知消息时,会返回一个包含了ClassNotFoundException 的错误消息。

3.支持远程

3.1.添加依赖

我们需要支持远程应用程序通过网络远程访问上面定义的 Actor。幸运的是,这是件很简单的事。我们只需要在 pox.xml 中添加用于远程访问的依赖即可:

<dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-remote_2.12</artifactId>
    <version>2.5.31</version>
</dependency>

3.2.增加配置文件

接着,添加配置文件,就可以启用 Actor 的远程访问功能了。在 src/main/resources 文件夹中新建一个文件,命名为application.conf,然后把下面的配置内容添加到该文件中,其中包含了要监听的主机和端口。

Akka 负责解析 application.conf。这是一个HOCON 文件,是一种类型安全的配置文件,格式和 JSON 类似,与其他配置文件格式一样,使用方便。Akka 文档中经常提到该格式的配置文件,笔者认为如果要配置多个属性,HOCON 是一种相当不错的用于配置文件的格式。

要注意的是,如果将其命名为application.properties并且使用属性配置文件的格式(比如 keypath.key=value),Akka 也是可以解析的。

下面是 application.conf 的内容:

akka {
	actor {
		provider = "akka.remote.RemoteActorRefProvider"
		}
	remote {
		enabled-transports = ["akka.remote.netty.tcp"]
		netty.tcp {
			hostname = "127.0.0.1"
			port = 2552
		}
	}
}

4.Main函数

最后,我们需要为数据库添加一个 main 方法,启动 Actor 系统,并创建 Actor。

在 Java 中,我们将添加一个类:com.akkademy.Main:

public class Main {
   public static void main(String... args) {
   	ActorSystem system = ActorSystem.create("akkademy");
   	system.actorOf(Props.create(AkkademyDb.class), "akkademy-db");
   }
}

我们只需要创建ActorSystem,然后在该Actor系统中创建一个Actor。注意到我们给Actor指定了一个名字:akkademy-db。有了这个名字,客户端就能够很容易地查询到Actor。同时由于Akka会在发生错误时把Actor的名字记录到日志中,指定名字也使得调试变得更容易。

5.发布消息

现在我们就要在本地发布这些消息,这样就能够在客户端项目中使用它们了。

最后,需要在发布的内容中排除application.conf,防止客户端也能够试图启动远程服务器。当然,更好的做法是把消息放在一个单独的库中,这里只是为了简单才这么做。

启动数据库接下来我们将构建客户端,并且通过编写一些集成测试来展示如何将客户端与服务器进行集成,因此需要服务器保持运行。现在启动数据库:

Akka会输出日志,表明其正在监听远程连接,并且给出当前服务器的地址(我们马上会在客户端中使用该地址):

[Remoting]Remotingnowlistensonaddresses:[akka.tcp://[email protected]:2552]

6.编写客户端

我们已经发布了消息,键值数据库也已经处于运行中。现在就可以编写一个客户端,连接并使用服务器提供的服务。我们将以此来结束第一个分布式应用程序。

除了导入上面发布的jar,在Java项目中,还需要添加一个scala-java8-compat库,用于将Actor生成的Scala Future转换成Java的CompletionStage

构建客户端在这一小节中,我们将构建客户端,连接远程Actor,然后分别实现用于处理SetRequest和GetRequest消息的方法。

首先,将Java代码放在com.akkademy.JClient中:

public class JClient {
	private final ActorSystem system = ActorSystem
			.create("LocalSystem");
	private final ActorSelection remoteDb;
	public JClient(String remoteAddress) {
		remoteDb = system.actorSelection("akka.tcp://akkademy@" +
				remoteAddress + "/user/akkademy-db");
	}
	public CompletionStage set(String key, Object value) {
		return toJava(ask(remoteDb, new SetRequest(key, value),
				2000));
	}
	public CompletionStage<Object> get(String key){
		return toJava(ask(remoteDb, new GetRequest(key), 2000));
	}
}

代码相当简单。首先创建一个本地的ActorSystem,然后通过构造函数中提供的地址得到指向远程Actor的引用。接着,分别为GetRequest和SetRequest两种消息创建方法。我们向远程Actor发送本项目中导入的消息,然后得到返回的Future。注意到我们在发起的请求中随意使用了一个超时参数值。理想情况下,这个超时参数最好是可以配置的。

在Java代码中,我们将scala.concurrent.Future转换成CompletionStage,然后返回CompletionStage。这样能够给库的用户提供一个更好的JavaAPI。

接下来,我们将编写一个简单的测试用例来进行集成测试。

7.测试

由于要编写的是集成测试,所以需要数据库服务器保持运行。在这个例子中,我们将在远程数据库中创建一条记录,然后获取该记录。

public class JClientIntegrationTest {
	JClient client = new JClient("127.0.0.1:2552");
	@Test
	public void itShouldSetRecord() throws Exception {
		client.set("123", 123);
		Integer result = (Integer) ((CompletableFuture) client.
				get("123")).get();
		assert(result == 123);
	}
}

在测试我们编写的 API 时,需要用到在介绍 Future 时学到的知识。由于这只是个测试用例,所以使用了 Await 和 get。

虽然这只是一个简单的demo,但是现在已经完全可以证明,使用 Akka 来编写分布式应用程序是切实可行的。

猜你喜欢

转载自blog.csdn.net/monokai/article/details/107393384