如果系统采用了微服务的架构模式,随着微服务数量的不断增多,服务之间的调用关系会变得纵横交错,以纯人工手动的方式来管理这些微服务以及微服务之间的调用关系是及其复杂的,也是极度不可取的。
所以,需要引入服务治理的功能。服务治理也是在微服务架构模式下的一种最核心和最基本的模块,主要用来实现各个微服务的自动注册与发现
引入服务治理后,微服务项目总体上可以分为三个大的模块:服务提供者、服务消费者和注册中心,三者的关系如下图所示:
这里需要注意的是:服务消费者一般会从注册中心中获取到所有服务提供者的信息,根据具体情况实现对具体服务提供者的实例进行访问
从上面的分析可以看出,微服务实现服务治理的关键就是引入了注册中心,它是微服务架构模式下一个非常重要的组件,主要实现了服务注册与发现,服务配置和服务的健康检测等功能
注册中心会定期检测存储的服务列表中服务提供者的健康状况,例如服务提供者超过一定的时间没有上报心跳信息,则注册中心会认为对应的服务提供者不可用,就会将服务提供者踢出服务列表
能够实现注册中心功能的组件有很多,但是常用的组件大概包含:Zookeeper、Eureka、Consul、Etcd、Nacos 等。这里,就给大家简单介绍下这些能够实现注册中心功能的框架或组件:
Zookeeper
:接触过分布式或者大数据开发的小伙伴应该都知道,Zookeeper 是 Apache Hadoop 的一个子项目,它是一个分布式服务治理框架,主要用来解决应用开发中遇到的一些数据管理问题,例如:分布式集群管理、元数据管理、分布式配置管理、状态同步和统一命名管理等。在高并发环境下,也可以通过 Zookeeper 实现分布式锁功能Eureka
:Eureka 是 Netflix 开源的 SpringCloud 中支持服务注册与发现的组件,但是后来闭源了Consul
:Consul 是 HashiCorp 公司推出的开源产品,用于实现分布式系统的服务发现、服务隔离、服务配置,这些功能中的每一个都可以根据需要单独使用,也可以同时使用所有功能Etcd
:etcd 是一个高度一致的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。它可以优雅地处理网络分区期间的领导者选举,即使在领导者节点中也可以容忍机器故障Nacos
:我们重点说下 Nacos。Nacos 是阿里巴巴开源的一款更易于构建云原生应用的支持动态服务发现、配置管理和服务管理的平台,其提供了一组简单易用的特性集,能够快速实现动态服务发现、服务配置、服务元数据及流量管理,主要如下所示:
这里,我们选用的注册中心就是阿里巴巴开源的 Nacos。
下载 Nacos 地址:下载 Nacos 地址
下载后,安装教程:Windows下Nacos的安装与使用
我这里下载的安装包为:nacos-server-2.1.0.zip
注意:nacos 默认是以集群的方式启动的。如果以单机方式启动有两种方法:①命令行加参数 ② 修改配置文件
解压 Nacos 安装包,并在命令行进入到 Nacos 的 bin 目录下执行如下命令以单机的方式启动 Nacos
startup.cmd -m standalone
在nacos/bin
目录下,打开 startup.cmd
文件,修改第 26 行:
set MODE="cluster"
改为:
set MODE="standalone"
然后双击 startup.cmd
,运行项目。启动成功后,访问:
http://192.168.10.88:8848/nacos
账号密码默认都是:nacos
界面如下:
我们进入到 Nacos 的服务管理-服务列表菜单下,如下所示:
可以看到,在 Nacos 的服务管理-服务列表菜单下还没有任何服务,接下来,我们就对项目的代码进行改造
引入 Nacos 注册中心时,我们需要对项目的代码进行一定的改造,以便利用 Nacos 实现服务的注册与发现功能。这里需要三个步骤:
pom.xml
引入 Nacos 依赖application.yml
配置 nacos@EnableDiscoveryClient
接下来按照这三个步骤改造微服务。
1、在用户微服务的 pom.xml 文件中添加 nacos 的服务注册与发现依赖,如下所示:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
2、在用户微服务的 resources 目录下的 application.yml 文件中添 Nacos 注册中心的服务地址配置,如下所示
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
3、在用户微服务的启动类上标注 @EnableDiscoveryClient
注解,如下
所示:
@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan(value = {"com.zzc.user.mapper"})
@EnableDiscoveryClient
public class ShopUserApplication {
public static void main(String[] args) {
SpringApplication.run(ShopUserApplication.class, args);
}
}
此时,就完成了对用户微服务的代码改造
4、启动用户微服务,并刷新 Nacos 页面,如下所示:
我们可以用同样的方式来改造商品微服务和订单微服务的代码,改造好之后,分别启动商品微服务和订单微服务,并再次刷新 Nacos 的页面,如下所示:
可以看到,用户微服务、商品微服务和订单微服务都已成功注册到 Nacos
按照整个项目的执行流程:用户执行下单操作时,订单微服务会调用用户微服务的接口获取用户的基本信息,会调用商品微服务的接口获取商品的基本信息。在订单微服务中校验用户的合法性和校验商品库存是否充足,如果用户合法并且商品库存充足,就会向订单数据表中记录订单信息并调用商品微服务的接口来扣减商品的库存
用户微服务和商品微服务作为服务的提供者,而订单微服务作为服务的消费者,如果要实现服务的发现功能,我们还需要对订单微服务的代码进行改造。将订单微服务中硬编码的用户微服务和商品微服务的 IP 地址和端口号修改成从 Nacos 中获取
OrderServiceImpl
类中创建一个从 Nacos 中通过服务名称获取 IP 和端口号的方法 getServiceUrl()
,并在 getServiceUrl()
方法中将 IP 和端口号拼接成 IP:PORT
的形式,如下所示:
@Autowired
private DiscoveryClient discoveryClient;
// 用户的微服务名称
private String userServer = "server-user";
// 商品的微服务名称
private String productServer = "server-product";
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
的形式
userServer 的值需要与用户微服务下的 application.yml 文件中的如下配置的值相同
spring:
application:
name: server-user
productServer 也同理
最后,OrderServiceImpl
类代码为:
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
@Autowired
private OrderItemMapper orderItemMapper;
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
private String userServer = "server-user";
private String productServer = "server-product";
@Override
@Transactional(rollbackFor = Exception.class)
public void saveOrder(OrderParamVo orderParamVo) {
if (orderParamVo.isEmpty()){
throw new RuntimeException("参数异常: " + JSONObject.toJSONString(orderParamVo));
}
//从Nacos服务中获取用户服务与商品服务的地址
String userUrl = this.getServiceUrl(userServer);
String productUrl = this.getServiceUrl(productServer);
User user = restTemplate.getForObject("http://"+ userUrl + "/user/get/" + orderParamVo.getUserId(), User.class);
if (user == null){
throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParamVo));
}
Product product = restTemplate.getForObject("http://" + productUrl + "/product/get/" + orderParamVo.getProductId(), Product.class);
if (product == null){
throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParamVo));
}
if (product.getProStock() < orderParamVo.getCount()){
throw new RuntimeException("商品库存不足: " + JSONObject.toJSONString(orderParamVo));
}
Order order = new Order();
order.setAddress(user.getAddress());
order.setPhone(user.getPhone());
order.setUserId(user.getId());
order.setUsername(user.getUsername());
order.setTotalPrice(product.getProPrice().multiply(BigDecimal.valueOf(orderParamVo.getCount())));
baseMapper.insert(order);
OrderItem orderItem = new OrderItem();
orderItem.setNumber(orderParamVo.getCount());
orderItem.setOrderId(order.getId());
orderItem.setProId(product.getId());
orderItem.setProName(product.getProName());
orderItem.setProPrice(product.getProPrice());
orderItemMapper.insert(orderItem);
Result<Integer> result = restTemplate.getForObject("http://" + productUrl + "/product/update_count/" + orderParamVo.getProductId() + "/" + orderParamVo.getCount(), Result.class);
if (result.getCode() != HttpCode.SUCCESS){
throw new RuntimeException("库存扣减失败");
}
log.info("库存扣减成功");
}
private String getServiceUrl(String serviceName){
ServiceInstance serviceInstance = discoveryClient.getInstances(serviceName).get(0);
return serviceInstance.getHost() + ":" + serviceInstance.getPort();
}
}
可以看到,订单微服务调用商品微服务的扣减商品库存接口时,不再是硬编码商品微服务的 IP 地址和端口号了
至此,整个项目就改造完成了。