项目搭建和实现服务的自动注册和发现
项目搭建
服务规划
每个微服务占用的端口和访问的基础路径是不同的。这里整理成表所示:
服务名称 | 项目名称 | 占用端口 | 访问的基础路径 | 备注 |
---|---|---|---|---|
用户微服务 | shop-user | 8060 | /user | 提供用户信息的增删改查 |
商品微服务 | shop-product | 8070 | /product | 提供商品信息的增删改查服务 |
订单微服务 | shop-order | 8080 | /order | 提供订单信息的增删改查 |
服务治理
微服务项目总体上可以分为三个大的模块:服务提供者、服务消费者和注册中心,三者的关系如下图所示。
注册中心
它是微服务架构模式下一个非常重要的组件,主要实现了服务注册与发现,服务配置和服务的健康检测等功能。
(1)服务注册:注册中心提供保存服务提供者和服务消费者的相关信息。
(2)服务发现:也可以理解为服务订阅,服务调用者也就是服务消费者,向注册中心订阅服务提供者的信息,注册中心会向服务消费者推送服务提供者的信息。
服务配置
(1)配置订阅:服务的提供者和消费者都可以向注册中心订阅微服务相关的配置信息。
(2)配置下发:注册中心能够将微服务相关的配置信息主动推送给服务的提供者和消费者。
服务健康检测
注册中心会定期检测存储的服务列表中服务提供者的健康状况,例如服务提供者超过一定的时间没有上报心跳信息,则注册中心会认为对应的服务提供者不可用,就会将服务提供者踢出服务列表。
常见的注册中心
常用的组件大概包含:Zookeeper、Eureka、Consul、Etcd、Nacos等。
(1)Zookeeper
接触过分布式或者大数据开发的小伙伴应该都知道,Zookeeper是Apache Hadoop的一个子项目,它是一个分布式服务治理框架,主要用来解决应用开发中遇到的一些数据管理问题,例如:分布式集群管理、元数据管理、分布式配置管理、状态同步和统一命名管理等。在高并发环境下,也可以通过Zookeeper实现分布式锁功能。
(2)Eureka
Eureka是Netflix开源的SpringCloud中支持服务注册与发现的组件,但是后来闭源了。
(5)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环境
在nacos的bin目录下打开cmd
startup.cmd -m standalone windows下nacos单机启动。
浏览器访问http://localhost:8848/nacos, 账号和密码都默认为nacos。
项目集成Nacos注册中心
- 微服务的pom.xml文件中添加nacos的服务注册与发现依赖,如下所示。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 微服务的resources目录下的application.yml文件中添加Nacos注册中心的服务地址配置,如下所示。
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
- 启动类上标注@EnableDiscoveryClient注解,如下所示。
@SpringBootApplication
@MapperScan(value = { "io.binghe.shop.order.mapper" })
@EnableTransactionManagement(proxyTargetClass = true)
@EnableDiscoveryClient
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
此时,就完成了对用户微服务的代码改造。
- 启动用户微服务,并刷新Nacos页面,如下所示。
实现服务发现
- 在业务服务类中注入DiscoveryClient类的对象
@Autowired
private DiscoveryClient discoveryClient;
- 创建动态服务地址方法
从Nacos中通过服务名称 获取到IP和端口号,将其拼接成IP:PORT
private String getServiceUrl(String serviceName){
ServiceInstance serviceInstance = discoveryClient.getInstances(serviceName).get(0);
return serviceInstance.getHost() + ":" + serviceInstance.getPort();
}
- 定义服务提供者名称
private String userServer = "server-user";
private String productServer = "server-product";
- 提交订单逻辑
@Override
@Transactional(rollbackFor = Exception.class)
public void saveOrder(OrderParams orderParams) {
//user用户微服务的url
String userUrl = this.getServiceUrl(userServer); // server-user == 127.0.0.1:8060
String productUrl = this.getServiceUrl(productServer); // server-product == 127.0.0.1:8080
System.out.println(userUrl);
System.out.println(productUrl);
if (orderParams.isEmpty()) {
throw new RuntimeException("参数异常: " + JSONObject.toJSONString(orderParams));
}
// url中使用注册到nacos的服务名
User user =
restTemplate.getForObject("http://"+ userUrl +"/user/get/" + orderParams.getUserId(), User.class);
if (user == 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));
}
//proStock count
if (product.getProStock() < orderParams.getCount()) {
throw new RuntimeException("商品库存不足: " + JSONObject.toJSONString(orderParams));
}
//创建订单 address address username proPrice
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(orderParams.getCount())));
orderMapper.insert(order);
//订单条目
OrderItem orderItem = new OrderItem();
orderItem.setNumber(orderParams.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/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
logger.info("返回的结果为 {}", result);
if (result.getCode() != HttpCode.SUCCESS){
throw new RuntimeException("库存扣减失败");
}
logger.info("库存扣减成功");
}
- 浏览器访问 http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=10,返回的数据为success表示下单成功。
(1)再次查询id为1001的商品信息。
id为1001的商品库存由原来的100变更为90,减少了10个库存。
至此,项目的测试完毕。