今天,我们将通过Apache Kafkatopic构建一些彼此异步通信的微服务。我们使用Micronaut框架,它为与Kafka集成提供专门的库。让我们简要介绍一下示例系统的架构。我们有四个微型服务:订单服务,行程服务,司机服务和乘客服务。这些应用程序的实现非常简单。它们都有内存存储,并连接到同一个Kafka实例。
我们系统的主要目标是为客户安排行程。订单服务应用程序还充当网关。它接收来自客户的请求,保存历史记录并将事件发送到orderstopic。所有其他微服务都在监听orders这个topic,并处理order-service发送的订单。每个微服务都有自己的专用topic,其中发送包含更改信息的事件。此类事件由其他一些微服务接收。架构如下图所示。
![e06746a6fa2d2362f9fef589db470eeb.png](https://i-blog.csdnimg.cn/blog_migrate/1842712e329faffec84629aa35d9bed9.jpeg)
在阅读本文之前,有必要熟悉一下Micronaut框架。您可以阅读之前的一篇文章,该文章描述了通过REST API构建微服务通信的过程:使用microaut框架构建微服务的快速指南。
1. 运行Kafka
要在本地机器上运行Apache Kafka,我们可以使用它的Docker映像。最新的镜像是由https://hub.docker.com/u/wurstmeister共享的。在启动Kafka容器之前,我们必须启动kafka所用使用的ZooKeeper服务器。如果在Windows上运行Docker,其虚拟机的默认地址是192.168.99.100。它还必须设置为Kafka容器的环境。
Zookeeper和Kafka容器都将在同一个网络中启动。在docker中运行Zookeeper以zookeeper的名称提供服务,并在暴露2181端口。Kafka容器需要在环境变量使用KAFKA_ZOOKEEPER_CONNECT的地址。
$ docker network create kafka$ docker run -d --name zookeeper --network kafka -p 2181:2181 wurstmeister/zookeeper$ docker run -d --name kafka -p 9092:9092 --network kafka --env KAFKA_ADVERTISED_HOST_NAME=192.168.99.100 --env KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181 wurstmeister/kafka
2. 引入Micronaut Kafka依赖
使用Kafka构建的microaut应用程序可以在HTTP服务器存在的情况下启动,也可以在不存在HTTP服务器的情况下启动。要启用Micronaut Kafka,需要添加micronaut-kafka库到依赖项。如果您想暴露HTTP API,您还应该添加micronaut-http-server-netty:
io.micronaut.configuration micronaut-kafkaio.micronaut micronaut-http-server-netty
3. 构建订单微服务
订单微服务是唯一一个启动嵌入式HTTP服务器并暴露REST API的应用程序。这就是为什么我们可以为Kafka提供内置Micronaut健康检查。要做到这一点,我们首先应该添加micronaut-management依赖:
io.micronaut micronaut-management
为了方便起见,我们将通过在application.yml中定义以下配置来启用所有管理端点并禁用它们的HTTP身份验证。
endpoints: all: enabled: true sensitive: false
现在,可以在地址http://localhost:8080/health下使用health check。我们的示例应用程序还将暴露添加新订单和列出所有以前创建的订单的简单REST API。下面是暴露这些端点的Micronaut控制器实现:
@Controller("orders")public class OrderController { @Inject OrderInMemoryRepository repository; @Inject OrderClient client; @Post public Order add(@Body Order order) { order = repository.add(order); client.send(order); return order; } @Get public Set findAll() { return repository.findAll(); }}
每个微服务都使用内存存储库实现。以下是订单微服务(Order-Service)中的存储库实现:
@Singletonpublic class OrderInMemoryRepository { private Set orders = new HashSet<>(); public Order add(Order order) { order.setId((long) (orders.size() + 1)); orders.add(order); return order; } public void update(Order order) { orders.remove(order); orders.add(order); } public Optional findByTripIdAndType(Long tripId, OrderType type) { return orders.stream().filter(order -> order.getTripId().equals(tripId) && order.getType() == type).findAny(); } public Optional findNewestByUserIdAndType(Long userId, OrderType type) { return orders.stream().filter(order -> order.getUserId().equals(userId) && order.getType() == type) .max(Comparator.comparing(Order::getId)); } public Set findAll() { return orders; }}
内存存储库存储Order对象实例。Order对象还被发送到名为orders的Kafkatopic。下面是Order类的实现:
public class Order { private Long id; private LocalDateTime createdAt; private OrderType type; private Long userId; private Long tripId; private float currentLocationX; private float currentLocationY; private OrderStatus status; // ... GETTERS AND SETTERS}
4. 使用Kafka异步通信
现在,让我们想一个可以通过示例系统实现的用例——添加新的行程。
我们创建了OrderType.NEW_TRIP类型的新订单。在此之后,(1)订单服务创建一个订单并将其发送到orderstopic。订单由三个微服务接收:司机服务、乘客服务和行程服务。
(2)所有这些应用程序都处理这个新订单。乘客服务应用程序检查乘客帐户上是否有足够的资金。如果没有,它就取消了行程,否则什么也做不了。司机服务正在寻找最近可用的司机,(3)行程服务创建和存储新的行程。司机服务和行程服务都将事件发送到它们的topic(drivers, trips),其中包含相关更改的信息。
每一个事件可以被其他microservices访问,例如,(4)行程服务侦听来自司机服务的事件,以便为行程分配一个新的司机
下图说明了在添加新的行程时,我们的微服务之间的通信过程。
![aec863d5d9019e397b9a2248cf38fa30.png](https://i-blog.csdnimg.cn/blog_migrate/71e3fcc1dc479abe29a354a75963987c.jpeg)
现在,让我们继续讨论实现细节。
4.1. 发送订单
首先,我们需要创建Kafka 客户端,负责向topic发送消息。我们创建的一个接口,命名为OrderClient,为它添加@KafkaClient并声明用于发送消息的一个或多个方法。每个方法都应该通过@Topic注解设置目标topic名称。对于方法参数,我们可以使用三个注解@KafkaKey、@Body或@Header。@KafkaKey用于分区,这是我们的示例应用程序所需要的。在下面可用的客户端实现中,我们只使用@Body注解。
@KafkaClientpublic interface OrderClient { @Topic("orders") void send(@Body Order order);}
4.2. 接收订单
一旦客户端发送了一个订单,它就会被监听orderstopic的所有其他微服务接收。下面是司机服务中的监听器实现。监听器类OrderListener应该添加@KafkaListener注解。我们可以声明groupId作为一个注解参数,以防止单个应用程序的多个实例接收相同的消息。然后,我们声明用于处理传入消息的方法。与客户端方法相同,应该通过@Topic注解设置目标topic名称,因为我们正在监听Order对象,所以应该使用@Body注解——与对应的客户端方法相同。
@KafkaListener(groupId = "driver")public class OrderListener { private static final Logger LOGGER = LoggerFactory.getLogger(OrderListener.class); private DriverService service; public OrderListener(DriverService service) { this.service = service; } @Topic("orders") public void receive(@Body Order order) { LOGGER.info("Received: {}