但是在实现的过程中,存在一个很明显的问题:那就是将用户微服务所在的IP和端口,以及商品微服务所在的IP和端口硬编码到订单微服务的代码中了。这样的做法存在着非常多的问题。
硬编码的问题
如果将用户微服务和商品微服务所在的IP地址和端口号硬编码到订单微服务中,会存在非常多的问题,其中,最明显的问题有三个,如下所示。
(1)如果用户微服务和商品微服务的IP地址或者端口号发生了变化,则订单微服务将变得不可用,需要对同步修改订单微服务中调用用户微服务和商品微服务的IP地址和端口号。
(2)如果系统中提供了多个用户微服务和商品微服务,则无法实现微服务的负载均衡功能。
(3)如果系统需要支持更高的并发,需要部署更多的用户微服务和商品微服务以及订单微服务,如果将用户微服务和商品微服务的IP地址和端口硬编码到订单微服务,则后续的维护会变得异常复杂。
所以,在微服务开发的过程中,需要引入服务治理功能,实现微服务之间的动态注册与发现。
服务治理
如果系统采用了微服务的架构模式,随着微服务数量的不断增多,服务之间的调用关系会变得纵横交错,以纯人工手动的方式来管理这些微服务以及微服务之间的调用关系是及其复杂的,也是极度不可取的。
所以,需要引入服务治理的功能。服务治理也是在微服务架构模式下的一种最核心和最基本的模块,主要用来实现各个微服务的自动注册与发现。
引入服务治理后,微服务项目总体上可以分为三个大的模块:服务提供者、服务消费者和注册中心,三者的关系如下图所示。
(1)服务提供者会将自身提供的服务注册到注册中心,并向注册中心发送心跳信息来证明自己还存活,其中,心跳信息中就会包含服务提供者自身提供的服务信息。
(2)注册中心会存储服务提供者上报的信息,并通过服务提供者发送的心跳来更新服务提供者最后的存活时间,如果超过一段时间没有收到服务提供者上报的心跳信息,则注册中心会认为服务提供者不可用,会将对应的服务提供者从服务列表中剔除。
(3)服务消费者会向注册中心订阅自身监听的服务,注册中心会保存服务消费者的信息,也会向服务消费者推送服务提供者的信息。
(4)服务消费者从注册中心获取到服务提供者的信息时,会直接调用服务提供者的接口来实现远程调用。
这里需要注意的是:服务消费者一般会从注册中心中获取到所有服务提供者的信息,根据具体情况实现对具体服务提供者的实例进行访问。
注册中心
从上面的分析可以看出,微服务实现服务治理的关键就是引入了注册中心,它是微服务架构模式下一个非常重要的组件,主要实现了服务注册与发现,服务配置和服务的健康检测等功能。
服务注册与发现
(1)服务注册:注册中心提供保存服务提供者和服务消费者的相关信息。
(2)服务发现:也可以理解为服务订阅,服务调用者也就是服务消费者,向注册中心订阅服务提供者的信息,注册中心会向服务消费者推送服务提供者的信息。
服务配置
(1)配置订阅:服务的提供者和消费者都可以向注册中心订阅微服务相关的配置信息。
(2)配置下发:注册中心能够将微服务相关的配置信息主动推送给服务的提供者和消费者。
服务健康检测
注册中心会定期检测存储的服务列表中服务提供者的健康状况,例如服务提供者超过一定的时间没有上报心跳信息,则注册中心会认为对应的服务提供者不可用,就会将服务提供者踢出服务列表。
常见的注册中心
能够实现注册中心功能的组件有很多,但是常用的组件大概包含:Zookeeper、Eureka、Consul、Etcd、Nacos等。这里,就给大家简单介绍下这些能够实现注册中心功能的框架或组件。
(1)Zookeeper
接触过分布式或者大数据开发的小伙伴应该都知道,Zookeeper是Apache Hadoop的一个子项目,它是一个分布式服务治理框架,主要用来解决应用开发中遇到的一些数据管理问题,例如:分布式集群管理、元数据管理、分布式配置管理、状态同步和统一命名管理等。在高并发环境下,也可以通过Zookeeper实现分布式锁功能。
(2)Eureka
Eureka是Netflix开源的SpringCloud中支持服务注册与发现的组件,但是后来闭源了。
(3)Consul
Consul 是 HashiCorp 公司推出的开源产品,用于实现分布式系统的服务发现、服务隔离、服务配置,这些功能中的每一个都可以根据需要单独使用,也可以同时使用所有功能。
(4)Etcd
etcd 是一个高度一致的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。它可以优雅地处理网络分区期间的领导者选举,即使在领导者节点中也可以容忍机器故障。
(5)Nacos
这里,我们重点说下Nacos。Nacos是阿里巴巴开源的一款更易于构建云原生应用的支持动态服务发现、配置管理和服务管理的平台,其提供了一组简单易用的特性集,能够快速实现动态服务发现、服务配置、服务元数据及流量管理,主要如下所示。
- 服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如IP地址、端口等信 息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
- 服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。
- 服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
- 服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清 单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地存。
- 服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。
这里,我们选用的注册中心就是阿里巴巴开源的Nacos。
搭建Nacos环境
(1)到链接
https://github.com/alibaba/nacos/releases 下载Nacos的安装包,我这里下载的安装包为:nacos-server-1.4.3.zip。
(2)解压Nacos安装包,并在命令行进入到Nacos的bin目录下执行如下命令以单机的方式启动Nacos。
startup.cmd -m standalone
注意:如果需要以单机的方式启动Nacos,则需要添加 -m standalone 参数,否则,Nacos会以集群的方式启动。
(3)启动Nacos之后,在浏览器中输入链接http://localhost:8848/nacos
来访问Nacos的管理界面,默认的用户名和密码都是Nacos,如下所示。
输入用户名和密码进入Nacos的管理界面,如下所示。
这里,我们进入到Nacos的服务管理-服务列表菜单下,如下所示。
可以看到,在Nacos的服务管理-服务列表菜单下还没有任何服务,接下来,我们就对项目的代码进行改造。
集成Nacos注册中心
引入Nacos注册中心时,我们需要对项目的代码进行一定的改造,以便利用Nacos实现服务的注册与发现功能。
改造用户微服务
(1)在用户微服务的pom.xml文件中添加nacos的服务注册与发现依赖,如下所示。
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
(2)在用户微服务的resources目录下的application.yml文件中添加Nacos注册中心的服务地址配置,如下所示。
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848
(3)在用户微服务的启动类io.binghe.shop#UserStarter上标注@EnableDiscoveryClient注解,如下所示。
/** * @author binghe * @version 1.0.0 * @description 启动用户服的类 */ @SpringBootApplication @EnableTransactionManagement(proxyTargetClass = true) @MapperScan(value = { "io.binghe.shop.user.mapper" }) @EnableDiscoveryClient public class UserStarter { public static void main(String[] args){ SpringApplication.run(UserStarter.class, args); } }
此时,就完成了对用户微服务的代码改造。
(4)启动用户微服务,并刷新Nacos页面,如下所示。
可以看到,用户微服务已经成功注册到Nacos中。
改造其他微服务
我们可以用同样的方式来改造商品微服务和订单微服务的代码,改造好之后,分别启动商品微服务和订单微服务,并再次刷新Nacos的页面,如下所示。
可以看到,用户微服务、商品微服务和订单微服务都已成功注册到Nacos。
实现服务发现
按照整个项目的执行流程,用户执行下单操作时,订单微服务会调用用户微服务的接口获取用户的基本信息,会调用商品微服务的接口获取商品的基本信息。
在订单微服务中校验用户的合法性和校验商品库存是否充足,如果用户合法并且商品库存充足,就会向订单数据表中记录订单信息并调用商品微服务的接口来扣减商品的库存。
用户微服务和商品微服务作为服务的提供者,而订单微服务作为服务的消费者,如果要实现服务的发现功能,我们还需要对订单微服务的代码进行改造。将订单微服务中硬编码的用户微服务和商品微服务的IP地址和端口号修改成从Nacos中获取。
为了让小伙伴们能够更好的对比修改前和修改后的代码,这里,并没有在订单微服务的 io.binghe.shop.order.service.impl#OrderServiceImpl
类上直接修改,还是将其重命名为 io.binghe.shop.order.service.impl.OrderServiceV1Impl
类,同时,再次将其复制一份并命名为io.binghe.shop.order.service.impl.OrderServiceV2Impl
类,在后续的开发过程中,如果涉及到大的代码变动,都会以这种方式进行更新。
注入服务发现类
(1)在io.binghe.shop.order.service.impl.OrderServiceV2Impl
类中首先注入DiscoveryClient类的对象,如下所示。
@Autowired private DiscoveryClient discoveryClient;
创建动态服务地址方法
在io.binghe.shop.order.service.impl.OrderServiceV2Impl
类中创建一个从Nacos中通过服务名称获取IP和端口号的方法getServiceUrl(),并在getServiceUrl()方法中将IP和端口号拼接成IP:PORT
的形式,如下所示。
private String getServiceUrl(String serviceName){ ServiceInstance serviceInstance = discoveryClient.getInstances(serviceName).get(0); return serviceInstance.getHost() + ":" + serviceInstance.getPort(); }
具体的实现方式就是调用DiscoveryClient对象的getInstances()方法,并传入服务的名称,从Nacos注册中心中获取一个ServiceInstance类型的List集合,从List集合中获取第1个元素,也就是从List集合中获取到一个ServiceInstance对象,从ServiceInstance对象中获取到IP地址和端口号,并将其拼接成IP:PORT
的形式。
定义服务提供者名称
在io.binghe.shop.order.service.impl.OrderServiceV2Impl
类中定义两个成员变量userServer和productServer,表示用户微服务和商品微服务的服务名称,并将其分别复制为server-user
和server-product
。
private String userServer = "server-user"; private String productServer = "server-product";
注意:userServer的值需要与用户微服务下的application.yml文件中的如下配置的值相同。
spring: application: name: server-user
productServer的值需要与商品微服务下的application.yml文件中的如下配置的值相同。
spring: application: name: server-product
修改提交订单逻辑
在io.binghe.shop.order.service.impl.OrderServiceV2Impl
类的saveOrder()方法中,将硬编码的用户微服务和商品微服务的IP和端口修改成从Nacos注册中心中获取,涉及改动的代码片段如下所示。
(1)添加获取用户微服务与商品微服务的IP和端口号的代码片段,如下所示。
//从Nacos服务中获取用户服务与商品服务的地址 String userUrl = this.getServiceUrl(userServer); String productUrl = this.getServiceUrl(productServer);
(2)修改使用restTemplate获取用户信息的代码片段,修改前的代码片段如下所示。
User user = restTemplate.getForObject("http://localhost:8060/user/get/" + orderParams.getUserId(), User.class); if (user == null){ throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams)); }
修改后的代码片段如下所示。
User user = restTemplate.getForObject("http://" + userUrl + "/user/get/" + orderParams.getUserId(), User.class); if (user == null){ throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams)); }
可以看到,订单微服务获取用户微服务信息时,不再是硬编码用户微服务的IP地址和端口号了。
(3)修改使用restTemplate获取商品信息的代码片段,修改前的代码片段如下所示。
Product product = restTemplate.getForObject("http://localhost:8070/product/get/" + orderParams.getProductId(), Product.class); if (product == null){ throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams)); }
修改后的代码片段如下所示。
Product product = restTemplate.getForObject("http://" + productUrl + "/product/get/" + orderParams.getProductId(), Product.class); if (product == null){ throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams)); }
可以看到,订单微服务获取商品微服务信息时,不再是硬编码商品微服务的IP地址和端口号了。
(4)修改使用restTemplate扣减商品库存的代码片段,修改前的代码片段如下所示。
Result<Integer> result = restTemplate.getForObject("http://localhost:8070/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class); if (result.getCode() != HttpCode.SUCCESS){ throw new RuntimeException("库存扣减失败"); }
修改后的代码片段如下所示。
Result<Integer> result = restTemplate.getForObject("http://" + productUrl + "/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class); if (result.getCode() != HttpCode.SUCCESS){ throw new RuntimeException("库存扣减失败"); }
可以看到,订单微服务调用商品微服务的扣减商品库存接口时,不再是硬编码商品微服务的IP地址和端口号了。
注意:修改后的io.binghe.shop.order.service.impl.OrderServiceV2Impl
类的完整源码,小伙伴们可自行查看项目代码,冰河在这里不再赘述。
至此,整个项目就改造完成了。接下来,我们进行测试。
测试项目
开发完成后,我们对快速搭建并开发完成的三大微服务进行简单的测试,在测试之前我们需要先在数据表中添加一些测试数据。
添加测试数据
(1)在用户表中添加一条id为1001的记录,如下所示。
INSERT INTO `shop`.`t_user`(`id`, `t_username`, `t_password`, `t_phone`, `t_address`) VALUES (1001, 'binghe', 'c26be8aaf53b15054896983b43eb6a65', '13212345678', '北京');
(2)在商品数据表中添加几条商品记录,如下所示。
INSERT INTO `shop`.`t_product`(`id`, `t_pro_name`, `t_pro_price`, `t_pro_stock`) VALUES (1001, '华为', 2399.00, 100); INSERT INTO `shop`.`t_product`(`id`, `t_pro_name`, `t_pro_price`, `t_pro_stock`) VALUES (1002, '小米', 1999.00, 100); INSERT INTO `shop`.`t_product`(`id`, `t_pro_name`, `t_pro_price`, `t_pro_stock`) VALUES (1003, 'iphone', 4999.00, 100);
测试库存不足的情况
(1)分别启动用户微服务、商品微服务和订单微服务。
(2)查询id为1001的商品信息,如下所示。
mysql> select * from t_product where id = 1001; +------+------------+-------------+-------------+ | id | t_pro_name | t_pro_price | t_pro_stock | +------+------------+-------------+-------------+ | 1001 | 华为 | 2399.00 | 100 | +------+------------+-------------+-------------+ 1 row in set (0.00 sec)
可以看到,id为1001的商品的库存为100。
(3)查询订单表和订单条目表中的数据,如下所示。
- 查询订单表
mysql> select * from t_order; Empty set (0.00 sec)
可以看到,订单数据表的数据为空。
- 查询订单条目表
mysql> select * from t_order_item; Empty set (0.00 sec)
可以看到,订单条目数据表的数据为空。
(4)在浏览器中调用订单微服务的下单接口,传入的商品数量为1001,如下所示。
可以看到,返回的信息中,code为500,codeMsg输出的信息为执行失败,data返回的结果为商品库存不足,并且输出了提交的参数信息。
(5)再次查询id为1001的商品信息,如下所示。
mysql> select * from t_product where id = 1001; +------+------------+-------------+-------------+ | id | t_pro_name | t_pro_price | t_pro_stock | +------+------------+-------------+-------------+ | 1001 | 华为 | 2399.00 | 100 | +------+------------+-------------+-------------+ 1 row in set (0.00 sec)
可以看到,商品id为1001的商品库存仍为100,并没有减少。
(6)再次查询订单表和订单条目表中的数据,如下所示。
- 查询订单表
mysql> select * from t_order; Empty set (0.00 sec)
可以看到,订单数据表的数据为空。
- 查询订单条目表
mysql> select * from t_order_item; Empty set (0.00 sec)
可以看到,订单条目数据表的数据为空。
综上,当提交订单时传入的商品数量大于商品的库存数量时,系统会抛出异常,并不会执行提交订单和扣减库存的操作。
测试正常下单的情况
(1)在测试库存不足的情况的基础上,我们将调用提交订单的接口时传入的商品数量修改为10,如下所示。
可以看到,当商品库存充足时,调用订单微服务的下单接口,返回的数据为success表示下单成功。
(2)再次查询id为1001的商品信息,如下所示。
mysql> select * from t_product where id = 1001; +------+------------+-------------+-------------+ | id | t_pro_name | t_pro_price | t_pro_stock | +------+------------+-------------+-------------+ | 1001 | 华为 | 2399.00 | 90 | +------+------------+-------------+-------------+ 1 row in set (0.00 sec)
可以看到,id为1001的商品库存由原来的100变更为90,减少了10个库存。
(3)再次查询订单表和订单条目表中的数据,如下所示。
- 查询订单表
mysql> select * from t_order; +------------------+-----------+-------------+-------------+-----------+---------------+ | id | t_user_id | t_user_name | t_phone | t_address | t_total_price | +------------------+-----------+-------------+-------------+-----------+---------------+ | 3270016896208896 | 1001 | binghe | 13212345678 | 北京 | 23990.00 | +------------------+-----------+-------------+-------------+-----------+---------------+ 1 row in set (0.00 sec)
可以看到,订单数据表中成功记录了订单的信息
- 查询订单条目表
mysql> select * from t_order_item; +------------------+------------------+----------+------------+-------------+----------+ | id | t_order_id | t_pro_id | t_pro_name | t_pro_price | t_number | +------------------+------------------+----------+------------+-------------+----------+ | 3270017277890560 | 3270016896208896 | 1001 | 华为 | 2399.00 | 10 | +------------------+------------------+----------+------------+-------------+----------+ 1 row in set (0.00 sec)
可以看到,订单条目数据表中成功记录了订单条目的信息。
至此,项目的测试完毕。
另外,小伙伴们在【冰河技术】知识星球获取到源码后,可以自行测试其他异常情况,比如商品不存在的异常和用户不存在的异常,或者自行验证几个其他的异常情况,看提交是否提交,商品库存是否扣减了。
这次,我们只是使用SpringBoot快速搭建了三个微服务项目,从下一篇开始,我们就要逐渐接入SpringCloud Alibaba技术了,小伙伴们,你们准备好了吗?加入【冰河技术】知识星球,一起搞定《SpringCloud Alibaba实战》吧,加油!!
关于星球
最近,冰河创建了【冰河技术】知识星球,《SpringCloud Alibaba实战》专栏的源码获取方式会放到知识星期中,同时在微信上会创建专门的知识星球群,冰河会在知识星球上和星球群里解答球友的提问。
星球提供的服务
冰河整理了星球提供的一些服务,如下所示。
加入星球,你将获得:
1.学习SpringCloud Alibaba实战项目—从零开发微服务项目
2.学习高并发、大流量业务场景的解决方案,体验大厂真正的高并发、大流量的业务场景
3.学习进大厂必备技能:性能调优、并发编程、分布式、微服务、框架源码、中间件开发、项目实战
4.提供站点 https://binghe001.github.io 所有学习内容的指导、帮助
5.GitHub:https://github.com/binghe001/BingheGuide - 非常有价值的技术资料仓库,包括冰河所有的博客开放案例代码
6.可以发送你的简历到我的邮箱,提供简历批阅服务
7.提供技术问题、系统架构、学习成长、晋升答辩等各项内容的回答
8.定期的整理和分享出各类专属星球的技术小册、电子书、编程视频、PDF文件
9.定期组织技术直播分享,传道、授业、解惑,指导阶段瓶颈突破技巧