海量订单系统微服务开发:使用MongoDB支持海量数据

2022-10-28 16:40:36 浏览数 (1)

海量订单系统微服务开发

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

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

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

使用MongoDB支持海量数据

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

使用 Mongo插件

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

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

MongoDB数据源相关配置

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

代码语言:javascript复制
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>

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

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

代码语言:javascript复制
#datasourcespring:
data:
mongodb:
host: localhostport: 27017
#矫正Mongo查询时间jackson:
timezone: GMT 8

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

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

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

订单文档建模

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

订单及其明细数据

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

代码语言:javascript复制
@Document
@Data
@NoArgsConstructorpublic class Order {
//订单ID
@Id
private 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<OrderDetail> orderDetails = new ArrayList<>();
}

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

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

代码语言:javascript复制
@ Data
public 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来实现,这样在订单的查询设计中,就可以对各个订单状态进行转换,同时在订单的编辑中也可以列举出所有状态进行选择。代码如下所示:

代码语言:javascript复制
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,实现代码如下所示:

代码语言:javascript复制
@Repository
ePrimary
public interface OrderRepository extends ReactiveMongoRepository<Order, String>{
Mono<0rder> findByOrderNo (String orderNo);
}

动态分页查询设计

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

代码语言:javascript复制
@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<Order> findAll (Long userid, Long merchantid, Integer status, Date start,
Date end, Sort sort) ;

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

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

代码语言:javascript复制
@service
public class OrderService {
@Autowired
private 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<Long> getCount(){
return orderRepository. count();
}
}

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

Mongo单元测试

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

代码语言:javascript复制
@RunWith(SpringRunner.class)
@ContextConfiguration(classes =(0rderRestApiApplication.class))@SpringBootTest
@Sl4j
public class OrderTest {
@Autowired
private orderService orderService;
@Test
public 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的调试日志,就可以从控制台中看到如下输出:

代码语言:javascript复制
Inserting Document containing fields:[orderNo,userid,merchantid,amount,status,created,orderDetails, class]in collection: order

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

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

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

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

代码语言:javascript复制
@Test
public 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的日志输出,如下所示:

代码语言:javascript复制
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

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

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

代码语言:javascript复制
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. 感谢大家的支持!

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

0 人点赞