主项目发布者项目

介绍微服务和MongoDB

在这些课堂笔记中,我将向您介绍一种重要的服务器架构,称为微服务架构。我还将以今天的示例为借口,演示非关系数据库MongoDB的使用。我们的教科书在第八章中介绍了使用MongoDB的基础知识。

什么是微服务?

我们一直在构建的拍卖赢博体育程序使用传统的单片架构。单片赢博体育程序的特点是一个服务器赢博体育程序处理系统的赢博体育工作。单片赢博体育程序在规模方面存在几个问题。首先,随着我们的系统变得越来越复杂,构建一个能够处理赢博体育事情的大型赢博体育程序变得越来越困难。第二个问题是,如果我们只运行服务器赢博体育程序的一个副本,当我们的用户数量增加时,它将难以管理流量。

这两个问题的解决方案是切换到微服务架构。以下是这种新架构风格的一些关键方面:

迈向微服务架构的第一步

作为我们的第一个微服务示例,我将分离出原始拍卖服务器的一部分功能,并将其移动到微服务中。这个微服务将是一个独立的Spring Boot服务器,称为Publisher,我们可以与主服务器同时运行。

我将要构建的微服务将从原始赢博体育程序中分离出一个基本服务。该服务正在处理到达URL /auctions的GET请求。因为我可以预料到这个特定的请求将是我们的系统要处理的最流行的请求,所以在它自己单独的服务中处理这个特定的请求是有意义的。此外,我将从头开始构建新的服务器赢博体育程序,使我们能够同时运行该服务的多个副本。这将使它更容易扩展我们的系统,以处理更多的用户在未来。

请注意,新服务将只处理获取拍卖列表的请求。原始赢博体育程序仍将处理其他赢博体育事情,包括处理POST请求以将新拍卖发布到系统。当原始系统收到发布新拍卖的请求时,它会在自己的数据库中存储有关新拍卖的一些有限信息,但随后它也会向新服务发送消息,让它知道新拍卖已经出现。然后,新服务将在自己的数据库中保存新拍卖的副本。

为了使这两个赢博体育程序能够通信,我将使用一个消息代理和Spring Cloud Stream模块。消息代理是一个独立的服务器赢博体育程序,它接收从赢博体育程序发送的消息,然后将这些消息分发给表示有兴趣接收这些消息的任何赢博体育程序。对于我们的示例,我将使用RabbitMQ消息代理。Spring Cloud Stream模块是Spring的一个子项目,它使Spring赢博体育程序更容易与RabbitMQ这样的消息代理进行通信。

我还将利用这个示例演示非关系数据库的使用。具体来说,我将要构建的新微服务将使用MongoDB数据库来存储有关拍卖的信息。MongoDB是一个以文档为导向的数据库:这种新风格的数据库将为我们在存储和检索拍卖方面提供额外的灵活性。

使用MongoDB

我们一直在使用的Spring Data模块非常灵活,并提供了对各种数据存储方案的支持。特别是,Spring Data支持使用MongoDB来代替传统的关系数据库。要在我们的微服务中使用MongoDB,我只需要做一些简单的更改。

首先,我需要在运行两个服务器赢博体育程序的同一台计算机上安装MongoDB数据库。

接下来,我需要替换对MySQL驱动程序的依赖,并使用MongoDB依赖:

<依赖> < groupId > org.springframework。boot</groupId> <artifactId>spring-boot-start -data-mongodb</artifactId> </dependency>

最后,我还需要对赢博体育程序做一个小的修改。属性文件。在平常的地方

spring.datasource。url = jdbc: mysql: / / localhost: 3306 /拍卖spring.datasource。用户名=学生spring.datasource.password = Cmsc250 !spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver

用来连接MySQL的,我们将使用

spring.data.mongodb.database =拍卖

这还需要我们在MongoDB中设置一个Auction数据库。为此,我们使用Compass赢博体育程序,它充当MySQL Workbench赢博体育程序的MongoDB版本。在Compass中,我们设置了一个名为“Auction”的新数据库,并让它包含一个名为“Auctions”的集合。

当我们使用MongoDB和Spring Data时,我们还需要对我们的类做一些小的调整。

代替实体类,MongoDB使用文档类。下面是用于拍卖的Document类:

@Document(collection="Auctions") public class Auction {@Id private String auctionid;私人字符串卖家;私有字符串项;私人储备;private int highBid;private String打开;private String关闭;私有布尔值完成;private Map<String,Object> details;public Auction() {} public Auction(AuctionDTO core) {auctionid = core.getAuctionid();这一点。卖方= core.getSeller();这一点。item = core.getItem();这一点。reserve = core. gereserve ();这一点。highBid = 0;这一点。open = core.getOpens();这一点。close = core. getclose ();this.completed = core.getCompleted();this.details = core.getDetails();}公共字符串getAuctionid(){返回auctionid;}公共无效setAuctionid(字符串拍卖id){这。Auctionid = Auctionid;}公共字符串getSeller(){返回卖家;}公共无效setSeller(字符串卖家){这个。卖方=卖方;}公共字符串getItem(){返回项目;}公共无效setItem(字符串项){此。Item = Item;}公共int getReserve(){返回储备金;}公共无效的setReserve(int reserve){这。储备=储备;}公共int getHighBid(){返回highBid;}公共无效setHighBid(int highBid){这个。highBid = highBid;}公共字符串getOpens(){返回打开;}公共无效setOpens(字符串打开){这。打开=打开;}公共字符串getclose(){返回关闭;}公共无效setclose(字符串关闭){此。close =关闭;}公共布尔isCompleted(){返回完成;}公共无效setCompleted(布尔完成){this.completed =已完成;}公共Map<String,对象> getDetails(){返回详细信息;}公共无效setDetails(Map<String, Object> details) {this.details = details;}}

它有许多与原始Auction Entity类相同的成员变量。请注意,由于这个类被设计为在微服务中独立操作,因此它不包含与其他类的关联。例如,in place of

@OneToOne私人用户卖家;

我们现在有简单的

私人字符串卖家;

另一个小变化是我们如何设置存储库来使用MongoDB而不是MySQL。下面是我们的微服务将要使用的新的AuctionRepository:

公共接口AuctionRepository扩展MongoRepository<Auction,String>{}

模式的灵活性

MongoDB提供的最有趣的新特性之一是模式灵活性。在传统的关系数据库中,我们必须从固定的表结构开始。在数据库中设置拍卖表时,我们必须指定拍卖可以使用的字段列表,然后坚持只使用这些字段。另一方面,在MongoDB文档集合中,我们可以插入包含不同字段的文档。在拍卖的情况下,我指定每个Auction文档包含一个details字段,该字段又将包含一个表示为属性/值对列表的对象。细节对象的有趣之处在于,我不会提前指定细节对象将包含哪些属性。这给我们带来了很大的灵活性。例如,如果我们正在设置一个拍卖出售一台计算机的细节对象看起来像

{制造商:“Dell”,屏幕尺寸:“16in”,年份:2020}

如果我们正在设置一个拍卖出售音频CD的细节对象,而不是看起来像

{艺术家:“Maurizio”发行:“m系列”,状态:“非常好”}

在上面的Auction Document类中,我们将details成员变量实现为一个泛型Map,它将String属性名映射到这些属性的Object值。

既然我们已经向Auction文档类添加了这种灵活性,我们还需要向AuctionDTO类添加类似的特性。下面是这个类的新版本:

公共类AuctionDTO{私有字符串auctionid;私人字符串卖家;私有字符串项;私人储备;private String打开;private String关闭;私有布尔值完成;private Map<String, Object> details;@JsonAnySetter public void setDetail(String key, Object value) {if (details == null) {details = new HashMap<>();}的细节。put(关键字,值);} @JsonAnyGetter公共Map<String, Object> getDetails(){返回详细信息;} public AuctionDTO() {} public AuctionDTO(Auction a) {auctionid = a.getAuctionid().toString();这一点。卖家= a.t getseller ();这一点。item = a.getItem();这一点。reserve = a.g ereserve ();这一点。打开= a.getOpens ();这一点。close = a.t getclose ();This.completed = false;details = a.t getdetails ();}公共字符串getAuctionid(){返回auctionid;}公共无效setAuctionid(字符串拍卖id){这。Auctionid = Auctionid;}公共字符串getSeller(){返回卖家;}公共无效setSeller(字符串卖家){这个。卖方=卖方;}公共字符串getItem(){返回项目;}公共无效setItem(字符串项){此。Item = Item;}公共int getReserve(){返回储备金;}公共无效的setReserve(int reserve){这。储备=储备;}公共字符串getOpens(){返回打开;}公共无效setOpens(字符串打开){这。打开=打开;}公共字符串getclose(){返回关闭;}公共无效setclose(字符串关闭){此。close =关闭;}公共布尔getCompleted(){返回完成;}公共无效setCompleted(布尔完成){this.completed =已完成;}}

这种灵活性现在扩展到了控制器,用户可以在控制器上发布auctiondto,允许用户发布不同类型物品的拍卖信息,每次拍卖的细节都与他们想要出售的特定物品相匹配。我们将在稍后测试我们的新系统时看到这一点。

使用RabbitMQ

我们的新系统将由主赢博体育程序和这个新服务组成。为了使我们能够将消息从主系统传递到新的赢博体育程序,我将使用RabbitMQ消息代理。为了使它更容易与RabbitMQ通信,我将使用Spring Cloud Stream包。

为了使用Spring Cloud Stream,我首先在两个项目中添加一些新的依赖项:

<依赖> < groupId > org.springframework。cloud</groupId> <artifactId>spring-cloud-stream</artifactId> <version>4.1.1</version> </dependency> <dependency> <groupId>org.springframework。cloud</groupId> <artifactId>spring-cloud-start -stream-rabbit</artifactId> <version>4.1.1</version> </dependency>

我将修改主赢博体育程序,以便在有人向系统发布新的拍卖时向辅助赢博体育程序发送消息。

以下是主系统中AuctionController的相关代码:

公共类AuctionController {@Autowired StreamBridge桥;私人拍卖服务;public AuctionController(AuctionService){这个。auctionService = auctionService;} @PostMapping public ResponseEntity<String> save(Authentication Authentication,@RequestBody AuctionDTO auction) {AuctionUserDetails details = (AuctionUserDetails) Authentication . getprincipal ();auction.setSeller (details.getUsername ());字符串键;try {key = auctionService.save(auction);auction.setAuctionid(关键);bridge.send(“auctionSupplier-out-0”,拍卖);} catch(WrongUserException ex){返回ResponseEntity.status(HttpStatus.UNAUTHORIZED)。body(“不良卖家Id”);}返回ResponseEntity.status(HttpStatus.CREATED).body(key);}}

这里有几件事是新的。第一个是新的成员变量

@Autowired StreamBridge桥;

StreamBridge类由Spring Cloud Stream提供。它提供了一种向消息代理发送消息的简单方法。

我们将这个新的StreamBridge用于一件非常具体的事情。当用户发布新的AuctionDTO时,我们将使用StreamBridge将AuctionDTO的副本发送到消息代理:

bridge.send(“auctionSupplier-out-0”,拍卖);

发送方法的第一个参数指定要将消息发送到的通道。这个特定的通道是由赢博体育程序中的一些附加项配置的。属性文件:

spring.cloud.stream.bindings.auctionsupplier -出- 0. - 0. =拍卖spring.cloud.stream.function.bindings.auctionsupplier -目的地——-目的地=拍卖

这些设置表明名为“auctionprovider -out-0”的通道应该将其数据发送到RabbitMQ中的“auction”交换器。为了实现这个功能,我们使用RabbitMQ管理控制台在RabbitMQ上创建一个类型为“topic”的新交换。要访问管理控制台,我们打开浏览器并将其指向URL

http://localhost:15672

要创建一个新的交易所,我们单击Exchanges选项卡,输入交易所的详细信息,然后单击底部的Add exchange按钮。

接收端

我们插入到交换器中的消息将进入RabbitMQ消息队列。为了在Publisher赢博体育程序中接收这些消息,我们需要为计划接收的每种类型的消息设置一个Consumer bean。下面是一个Configuration类的代码,它提供了两个必要的bean:

@Configuration类AuctionReceiver {@Autowired AuctionService AuctionService;@Bean public Consumer<AuctionDTO> readAuctions() {return (auction) -> {auctionService.save(auction);};} @Bean公共消费者<BidDTO> readBids() {return (bid) -> {auctionService.updateBid(bid);};}}

Consumer是一种特殊类型的函数。具体来说,它将作为一个回调函数,用于接收来自RabbitMQ的消息。为了将这些Consumer bean连接到正确的交换,我们还需要在赢博体育程序中提供一些配置信息。属性文件中的发布器项目:

spring.cloud.stream.bindings.readAuctions-in .destination=竞价

这里的第一个设置告诉赢博体育程序查找具有给定名称的函数bean。第二个设置自动将消息通道与这些功能关联起来,然后将它们链接到RabbitMQ服务器中的拍卖和出价交换。请注意,这里的命名约定告诉系统,我们将从这两个交换中提取数据。

听取投标

我向系统添加的另一个特性是,当从Publisher赢博体育程序提供有关拍卖的信息时,能够显示有关当前最高出价的信息。为了准备添加该特性,我向Publisher赢博体育程序中的Auction类添加了一个highBid成员变量,并向Publisher赢博体育程序中的AuctionDTO类添加了一个highBid成员变量。

在主赢博体育程序端,我在处理投标的控制器方法中添加了将传入投标发布到Publisher的代码:

@PostMapping("/{id}/bids") public ResponseEntity<String> saveBid(认证认证,@PathVariable UUID id,@RequestBody BidDTO bid) {AuctionUserDetails细节= (AuctionUserDetails) Authentication . getprincipal ();bid.setBidder (details.getUsername ());尝试{auctionService。saveBid (id、报价);bid.setAuction (id.toString ());桥。发送(“bidSupplier-out-0”,报价);} catch(WrongAuctionException ex){返回ResponseEntity.status(HttpStatus.BAD_REQUEST)。主体(“拍卖不存在”);} catch(WrongUserException ex){返回ResponseEntity.status(HttpStatus.FORBIDDEN)。body(“用户不存在”);}返回ResponseEntity.status(HttpStatus.CREATED)。身体(“输入”);}

这段代码将把BidDTO发送给发布者。

在接收端我们有

@Bean公共消费者<BidDTO> readBids() {return (bid) -> {auctionService.updateBid(bid);};}

它将BidDTO传递给这个方法:

public void updateBid(BidDTO bid){可选<Auction> maybeAuction = auctionRepository.findById(bid. getauction ());if(maybeAuction.isPresent()){拍卖拍卖= maybeAuction.get();if(auction.getHighBid() < bid.getBid()) {auction.setHighBid(bid.getBid());auctionRepository.save(拍卖);}}

尽管Publisher使用MongoDB数据库,但这段代码看起来相当普通。由于我们使用Spring Data与该数据库通信,因此我们最终能够使用非常标准的Spring Data存储库方法完成这里的赢博体育工作。