海量订单系统微服务开发

订单系统是电商平台中一个非常重要的组成部分,而且它还是一个具有巨大流量和高并发访问的系统,与订单相关的服务涉及库存、支付、物流等。在设计订单系统时,我们选择使用支持海量数据的NoSQL 数据库MongoDB,配合使用反应式的Spring Data MongoDB,实现高并发设计。

本章实例项目代码可从本书源代码中下载,在IDEA 中检出,或通过页面直接下载使用。检出后请获取分支版本V2.1。在这个分支中包含以下几个模块:

  • order-object:订单公共对象设计。
  • order-restapi:订单微服务接口应用设计。
  • order-web:订单后台管理应用设计。

使用MongoDB支持海量数据

MongoDB是一个分布式数据库,对于开发调试,我们只需一个单机版即可。

使用 Mongo插件

如果使用的是IDEA开发工具,则为了方便查询数据库,也可以安装一个Mongo客户端插件。打开 IDEA 设置,在插件上搜索Mongo进行安装即可,安装完成后,如图8-1所示。




mongodb 单表亿级别数据 查询耗时 mongodb海量数据查询_微服务


安装插件之后,就可以在设置中通过Other Settings连接 MongoDB,使用客户端来查询数据。图8-2是一个本地数据库连接的配置实例。


mongodb 单表亿级别数据 查询耗时 mongodb海量数据查询_Data_02


MongoDB数据源相关配置

我们在模块 order-restapi中进行MongoDB的设计,首先在项目对象模型pom.xml中引入相关依赖引用,代码如下所示:

org.springframework.bootspring-boot-starter-data-mongodb-reactive

这里引用的是反应式Spring Data MongoDB组件,它可以支持无事务的高并发非阻塞的异步请求调用。

在模块的配置文件 applicaption.yml 中,设定连接MongoDB服务器的数据源配置,代码如下所示:

#datasourcespring:data:mongodb:host: localhostport: 27017#矫正Mongo查询时间jackson:timezone: GMT+8

这里是开发环境的一个本地连接的简单配置,如果是生产环境,则可以设置用户名和密码,并且指定使用的数据库名称。

这里是开发环境的一个本地连接的简单配置,如果是生产环境,则可以设置用户名和密码,并且指定使用的数据库名称。

因为MongoDB使用了格林尼治时间(GMT),所以为了显示东八区的正确时间,我们在数据查询时做了“GMT+8”的配置。


mongodb 单表亿级别数据 查询耗时 mongodb海量数据查询_微服务_03


订单文档建模

订单数据主要由订单及其明细数据组成,由于订单从生成开始到交易结束,会发生一系列状态变化,而这些状态一般可以固定下来,所以可以使用一个枚举类来实现。

订单及其明细数据

订单文档的建模由Order类实现,代码如下所示:

@Document@Data@NoArgsConstructorpublic class Order {//订单ID@Idprivate String id;//订单号@Indexed (name = "index orderNo")private String orderNo;//用户编号private Long userid;//商家编号private Long merchantid;//订单金额private Double amount;//订单状态(0:未付款,1:已付款,2:已发货,3:已收货,4:已评价,-1:已撤销,-2:已退款)private Integer status;//创建时间@DateTimeFormat (pattern= "yyyy-MM-dd HH:mm : ss")private Date created;//操作员private string operator;//修改时间@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date modify;//订单明细private List orderDetails = new ArrayList<>();}

在上面的代码中,各个字段的属性已经有注释说明。注解@Data为各个字段自动生成getter/setter 方法。另外,注解@Id可由数据库自动生成ID,并且是文档的唯一索引;注解@Indexed为订单编号创建了一个索引,从而提高了以订单号进行查询的性能。

订单明细的定义在类 OrderDetail中,代码如下所示:

@ Datapublic class OrderDetail {//商品编号private Long goodsid;//商品名称private String goodsname;//商品图片private String photo;//购买数量private Integer nums;//单价private Double price;//金额private Double money;//时间戳@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")private Date created;}

在订单明细设计中,对于商品名称和图片数据等字段,使用冗余设计的方法,可以减少对库存管理中商品接口的调用。

订单明细虽然是一个独立的类,但它不是一个独立的文档。订单明细将与订单一起组成一个文档。这一点与关系数据库的设计不同,如果是MySQL,则订单明细会使用另一个表结构,在查询时再使用关联关系获取数据,这样一来必将是很耗性能的。

订单状态枚举

订单状态在订单文档中保存时是一个整型字段,它对应订单的一个状态信息。一般来说,这种状态都较为固定,所以我们使用一个枚举定义StatusEnum来实现,这样在订单的查询设计中,就可以对各个订单状态进行转换,同时在订单的编辑中也可以列举出所有状态进行选择。代码如下所示:

public enum StatusEnum {UNPAID(Integer.valueOf(0),"未付款"),PAID(Integer.valueOf(1),"已付款"),SHIPPED(Integer.valueOf(2),"已发货"),RECEIVED(Integer.valueOf(3), "已收货"),EVALUATED(Integer.valueof(4),"已评价"),REVOKED(Integer.valueOf(-1),"已撤销"),REFUNDED(Integer.valueOf(-2),"已退款");private Integer code;private String name;StatusEnum(Integer code, String name) {this.code = code;this.name = name;public static boolean contains (Integer code) throws NullPointerException {if(null -= code){throw new NullPointerException ("constant code is null");]else {StatusEnum[] varl = values();int var2=var1. length;for(int var3 =0; var3 < var2; ++var3) {StatusEnum eum = varl[var3];if(code.equals(eum.getCode())) {return true;return false;public static StatusEnum valueof(Integer code) throws NullPointerException,EnumConstantNotPresentException {if(null == code) {throw new NullPointerException ("constant code is null");]else {StatusEnum[] var1 =values();int var2 = var1 . length;for(int var3 =0;var3 < var2; ++var3) {StatusEnum statusEnum= var1 [var3];if(code.equals(statusEnum.getCode())) {return statusEnum;throw new EnumConstantNotPresentException (StatusEnum.class,code.toString());)public Integer getCode( {return this.code;}public string getName(){return this.name;}}

反应式 MongoDB编程设计

反应式编程设计是Spring Boot 2.0及以上版本提供的一个新功能,这是一个非阻塞的异步调用设计,可以适应高并发的请求调用。在反应式编程中有两个基本概念:Flux和 Mono。Flux表示的是包含0到N个元素的异步序列,在该序列中可以包含三种不同类型的消息通知:正常的包含元素的消息、序列结束的消息和序列出错的消息。当消息通知产生时,订阅者中对应的方法 onNext()、onComplete()和 onError()会被调用。Mono表示的是包含0或者1个元素的异步序列,在该序列中,包含的消息通知的类型与Flux相同。

基于Spring Data的存储库接口设计

Spring Data MongoDB和Spring Data一样,有一个统一的规范设计。前面我们在Spring DataJPA中使用过这种规范,所以接下来的代码,读者会觉得很熟悉。

订单的存储库接口是 OrderRepository,实现代码如下所示:

@RepositoryePrimarypublic interface OrderRepository extends ReactiveMongoRepository{Mono<0rder> findByOrderNo (String orderNo);}

动态分页查询设计

在存储库接口设计中,可以使用注解@Query灵活地定义复杂的查询。对于订单的分页查询,我们使用了如下所示的动态查询设计:

@Query ("I 'userid':?#(([0] == null)?{$exists:true}:[0]},"+" 'merchantid':?#{([1] == null)?{$exists:true}:[1]},"+" 'status' :?#{([2] == null)?{$exists:true} :[2]1,"+" 'created':?#{([3] == null) and ([4] == null)?{Sexists:true}:( $gte:[3],$lte: [4]}}}")Flux findAll (Long userid, Long merchantid, Integer status, Date start,Date end, Sort sort) ;

这里我们提供了几个查询条件,它们分别是:用户编号(userid) 、商家编号(merchantid)、订单状态(status)和订单创建日期(created)。这些查询条件如果值为空,则忽略不计,否则按提供的数值进行限定查询。其中,对于订单的创建日期的条件查询,使用了大于或等于(Sgte)开始日期和小于或等于($Ite)结束日期的条件限制。最后,还可以对查询结果进行排序。

针对分页的查询接口声明,我们在服务类OrderService中使用了如下所示的设计:

@servicepublic class OrderService {@Autowiredprivate OrderRepository orderRepository;public Flux<0rder> findAll (0rderQo orderQo){try{Sort sort = Sort.by (new Sort.Order(Sort.Direction.DESC, "created"));return orderRepository. findAll(orderQo.getUserid(),orderQo-getMerchantid(),orderQo.getStatus(),orderQo.getStart(),orderQo.getEnd(),sort).skip(orderQo.getPage() * orderQo.getSize()).limitRequest (orderQo.getSize());}catch(Exception e){e.printstackTrace();return null;public Mono getCount(){return orderRepository. count();}}

首先对订单创建日期进行倒序排序,然后使用查询对象OrderQo传输查询参数,并对查询结果使用分页方式输出。需要注意的是,这里的输出结果是一个异步序列Flux,它包含了订单的列表数据。如果是单个对象的数据输出,则可以使用异步序列Mono,如上面代码中对订单总数查询的输出使用了Mono序列。

Mongo单元测试

针对前面的纯数据库方面的设计,我们可以使用一个单元测试进行验证。一个生成订单数据的测试用例如下所示:

@RunWith(SpringRunner.class)@ContextConfiguration(classes =(0rderRestApiApplication.class))@SpringBootTest@Sl4jpublic class OrderTest {@Autowiredprivate orderService orderService;@Testpublic void insertData(){OrderDetail orderDetail1 =new OrderDetail();orderDetail1.setGoodsname("测试商品1");orderDetail1.setGoodsid(1L);orderDetaill.setPrice(12.20D);orderDetail1.setNums (1);orderDetail1.setMoney(12.20D);orderDetail1 .setPhoto( "../images/demo1 .png") ;OrderDetail orderDetail2 = new OrderDetail();orderDetail2.setGoodsname("测试商品2");orderDetail2.setGoodsid(2L);orderDetail2 .setPrice(20.00D);orderDetail2.setNums (2);orderDetail2.setMoney(40.00D);orderDetai12.setPhoto ("../images/demo2.png");Order order = new Order();order.setorderNo ( "123456");order.setUserid(1213L);order. setMerchantid(2222L);order.setAmount (52.20D);order.setStatus(1);order.setCreated (new Date());List<0rderDetail> orderDetails = new ArrayEist<>();orderDetails.add(orderDetail1);orderDetails.add(orderDetail2);order.setOrderDetails(orderDetails);Mono<0rder> response = orderService.save (order);Assert.notNull(response, "save erro");log.info("返回结果:{}",new Gson ().toJson(response.block()));}}

在这个测试用例设计中生成了一个订单,并为这个订单的明细数据生成了两个记录。如果打开MongoDB的调试日志,就可以从控制台中看到如下输出:

Inserting Document containing fields:[orderNo,userid,merchantid,amount,status,created,orderDetails, class]in collection: order

另外,为了更加清晰地看到测试结果,我们还在日志输出中通过“返回结果:0}”将这条生成的订单信息打印出来。

这时,也可以借助MongoDB的客户端查询测试的结果。

因为测试是在线程中执行反应式的数据操作,所以对于异步序列,必须在最后执行类似block()这样的阻塞处理,才能完成反应式的调用过程,否则不可能达到预期的结果。

在接下来的各种增删改查的测试用例设计中,最后都进行了阻塞处理设计。例如,对分页查询的测试,我们使用如下所示的设计:

@Testpublic void findAl1() throws Exception{OrderQo orderQo = new OrderQo();List<0rder> list = orderService.findAll(orderQo) . collectList().block();Assert.notEmpty(list, "list is empty");log.info("总数:{;列表:{}",list.size(),new Gson() .toJson (list));}

执行这个测试用例后,可以在控制台日志中看到 MongoDB的日志输出,如下所示:

find using query:{ "userid" :{ "Sexists" :true }, "merchantid":{ "$exists":true }, "status":{"Sexists" : true ], "created":( "$exists" : true }I fields:Document{{} for class: class com.demo.order.restapi.domain.0rder in collection:order

因为这里没有提供查询参数的数值,所示这是一个没有条件限制的查询,它会按分页结果查出订单的所有记录。

当我们为这些查询参数指定数据时,即可看到如下所示的查询日志输出:

find using query: "userid" : 1213, "merchantid" :2222, "status" :1, "created":["$gte":{"$date" :1564538018885 }, "$lte":( "$date" : 1567130018886]HIfields: Document{{ for class: class com.demo,order.restapi.domain.0rder incollection: order

本文给大家讲解的内容

SpringCloud微服务架构实战:海量订单系统微服务开发,使用MongoDB支持海量数据、 订单文档建模、反应式MongoDB编程设计、Mongo单元测试

  1. 下篇文章给大家讲解的是SpringCloud微服务架构实战:海量订单系统微服务开发,订单接口微服务开发、订单的分布式事务管理、 订单管理后台微服务开发、集成测试
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!