前言:本课程是在慕课网上学习Spring Cloud微服务实战 第四章 服务拆分 时所做的笔记,供本人复习之用.
文章主要讲述了什么样的项目适合上微服务,以及服务拆分的方法,比如说把一整个服务拆成订单服务与商品服务等,但是不涉及服务之间的交互,具体的交互方法要到后面的文章中涉及.
代码地址https://github.com/springcloud-demo
目录
第一章 微服务拆分的起点
首先明白拆分的起点和终点,另外要明白需要考虑的因素与坚持的原则.
起点即当前架构的形态.我们学微服务最终都是要来用的,我们要用无非就两种情况,要从现在一个已有的架构转换到微服务架构,另一个原因是新系统上来要用微服务架构.
终点:好的架构不是设计出来的,而是进化出来的.
下图是Dubbo图里的经典架构,我们要考虑我们现在的架构是处在哪个位置上,原始的技术栈又是什么样子的,公司的架构如果已经是SOA服务,微服务与SOA之间只差了一个ESB即企业服务总线.它和处于单体架构的系统迁移到微服务是不一样的,微服务的系统很可能是异构的,那么当前系统中java占有多少部分呢?有没有已经包含服务发现,负载均衡的组件呢?这些是弃用还是保留如何以最小的代价切换过去,还要考虑是否系统真的适合做微服务架构呢?系统中包含很多强事务场景的,因为微服务是分布式的,基本CAP中都只会达到两个,采用最终一致性,如果系统真的是用强事务的话,微服务并不是一个好选择.业务相对稳定,迭代周期长,系统当前已经是一个很成熟的系统了,没有什么变更迭代,几个月都不会更新一次,切换成微服务代价还是有点大.访问压力不大,可用性要求也不高,比如中小型企业内部的OA系统,没什么访问量,偶尔出什么问题,一小时不能用也没什么,这样的系统上微服务是杀鸡用牛刀.
第二章 康威定律和微服务
传说微服务架构的理论基础,康威定律.
有点抽象,比较易懂的意思是:
沟通的问题会影响系统的设计,在开发团队中,如果你需要与许多人沟通,有开不完的会,回不完的邮件,必然会拖慢你的开发效率,如果团队很大,分布在不同的地点,甚至已经跨时区了,那么协调成本会急剧增加,很可能导致你下意识的减少沟通,比如浪费太多时间沟通,没有时间写代码,干脆就不沟通了吧.这样的话迭代的速度就会降低,甚至停止变更,这样是不希望我们看到的,所以微服务提倡小团队,由小团队负责整个系统的设计与实现.团队内部可以有频繁的细粒度的沟通.
反过来呢,业务的架构也与组织架构相匹配,当你把一个大的系统拆分成小的服务的时候,团队也会随之变化.
所以上不上微服务已经不是单纯的某个技术栈的技术问题了,已经上升到和一个团队结构相关的管理上的问题了,很多人认为,微服务在实践过程中,最大的问题的是团队之间的运作和管理问题,左图是传统的架构模式,人员组织是项目模式,项目启动的时候从不同技能资源池中抽取不需要的资源组建成一个团队,项目结束后把资源释放掉,回到原来的资源池里面.右边是微服务架构下,人员组织是产品模式,倾向于让团队负责整个服务的生命周期,以便提供更优质的服务,这个小团队自己要去处理从前端到后端,从开发到运维到部署的所有事情.所以在开发的过程中,开发人员和运维人员的角色发生了变化.开发者开始承担起业务的整个生命周期的责任,包括部署和监控.
第三章 点餐业务服务拆分分析
我们有两种方式可以拆分服务.
第一种,我们这个服务分为买家端和卖家端,买家端这块可以把vueapp这块的ui放到nginx上作为一个边缘服务,用来做买家端ui需要的接口,卖家端也就是PC端由freemarker做的html类型的页面,作为另外一个边缘服务,两个服务同时向后端的后端通用服务请求数据,等同于按照手机端和PC端来划分.
第二种,可以把订单ui放到一个边缘服务里面,商品支付这些都放到一个单独的边缘服务里面,等于ui是按照业务来,不按终端来划分.
如果这个点餐系统就是一个个人项目,BOSS,开发,小二都是一个人,上线了微信点餐,每月卖个百八十万,小日子过的挺滋润,这个样的项目就没有拆分的必要.团队就一个,迭代也不大,搞成微服务部署上就麻烦很多.
如果是一个快速发展的it公司的点餐部门,业务快速增长,需求不断提出,确实需要微服务化,应该审视一下公司当前人员的技术栈,看看前端人员的技术栈和水平,适不适合将ui这一部分单独拆成一个微服务,由前端的同志们单独维护,由前端团队精通nodejs,vue但是团队人数不是那么的多,可能倾向于单独将vueapp接口部分拆成一个单独的边缘服务,倾向于第一种方案.
如果当前可以遇见的情况是服务增长异常迅速,不论是订单支付广告商品增长都特别的块,前后端开发人员也特别充足,那可能就会依据业务,分成多个小组,一个小组负责一个微服务,有自己单独的前端人员,第二种拆分方法是一个更好的选择.
总的来说,起点和团队结构,沟通方式,真的会影响在软件设计上的取舍.
3.1 服务拆分具体的方法论
下图是一个扩展立方模型.出自《可扩展的艺术》.
x轴水平复制:通过副本扩展.将应用程序水平复制.通过负载均衡运行程序的多个一样的副本的方式来实现应用程序的伸缩性.提高应用程序的容量和可用度.
z轴数据分区:每个服务器负责一个数据子集,每个服务器运行的代码是一样的.
y轴功能解耦:将不同职责的模块分成不同的服务,
通过上面我们可以了解,服务拆分的关键地方,功能和数据.
拆功能:贯彻单一职责,松耦合,高内聚.某个服务只负责业务职责单一的部分.松耦合是指服务之间耦合度低,修改一个服务不用导致另外一个服务跟着修改,高内聚指的是服务内部相关的行为都聚集在一个服务内,而不是分散在不同的服务中,这样需要修改一个行为时,只要修改一个微服务即可.
关注点分离:按职责分离关注点,按通用性分离关注点,按粒度级别分离关注点.
按职责分离可以理解成,给我们的服务进行分类,比如一些明显的可以按业务领域划分出的服务.职责比较单一,比如订单商品这类服务,还有比如网站的前端服务,app的服务接口,这些很明显可以划分成边缘服务,按通用性分离是指一些基础的组件与具体的业务无关的也可以划分出来作为一个单独的服务.比如消息服务用户服务等,同时前端按职责(业务)领域划分出来的服务我们也应将其不同组件模块进行拆分,比如把公共组件拆分成独立的原子服务.形成相对独立的原子服务层.最后我们要靠考虑服务的粒度,微服务并不是越小越好,粒度并不好把握,比如点餐服务中有一个订单服务,初期比较合适,后来随着业务增大,功能增加,会导致变成的很胖,很可能需要我们再次拆分,拆分成订单服务和支付服务.
3.1.1 服务和数据的关系
拆分功能和拆分数据是有先后顺序的,应该先考虑拆分业务功能,再考虑拆分业务数据.
无状态服务:如果一个数据需要被多个服务共享,才能完成一个请求,那么这个数据就可以称为状态,进而依赖状态的服务称为有状态的服务,反之称为无状态的服务,无状态服务并不是说微服务架构里不允许存在这种状态,而是说要把有状态的服务改变成无状态的服务,比如以前在本地中建立的业务缓存,session缓存,就应该把这些数据迁移到分布式缓存中存储,让业务服务变成一个无状态的计算节点.迁移后后端服务就可以按需动态伸缩.不用考虑缓存数据如何同步的问题.
3.2 点餐业务服务拆分分析
原始的点餐业务买家部分的前端ui是vueapp,它通过api接口向后端springboot服务请求数据,数据对应的功能有以下几点.
商品主要是查询商品
订单可以下单和查询订单
广告有店铺的介绍之类的.
用户有买家用户之类的
支付原本是微信支付,根据业务发展会想到要接入其它的支付.
卖家部分的前端ui原始是freemarker提供的模板引擎
后端部分:有对商品更细粒度的查询,同时还有增删改查,用户的卖家登陆与各种支付渠道的后端逻辑.
如果要改成微服务架构,这与人还有关系,假设公司里有4-6个java水平比较好,但是vue掌握的一般的,那么vue这个可以作为一个单独的服务放在nginx上,原始卖家端ui除去支付以外,单独成为一个边缘服务,而订单用户商品成为独立的后端服务,就是说商品服务和订单服务既能为spring cloud体系外部放置在nginx上的vue app所需要的接口提供数据,也能为springcloud这个卖家端的边缘服务提供数据.而支付服务由于它的特点还包含ui和原始业务实现在一起还是一个服务,这有利于它不断的增加新的支付渠道.
如果是放在另一个环境下,比如在业务高速发展的背景下,点餐服务目测的业务扩展可能新增的功能可能有:
短信服务,日志服务,积分服务,优惠券服务.
像短信这种明显与业务无关的,会作为基础服务单独抽离出来供其它基础服务调用.同样类似的有消息系统,用户的部分,还有reddis缓存,很多公司对reddis会抽取成一个单独的基础服务,在开展微服务的时候不要妄图一步到位,好的架构是演进出来的.正确的做法是快速出一个微服务,快速的放到生产环境中去检验问题.
下面我们选用订单与商品服务为例子进行介绍.主要说明服务拆分后的样子.至于服务之间的通信交互,将在后面几篇文章中说明.
第四章 服务拆分之商品服务
4.1 API介绍
url:
GET /product/list
返回消息(根据类别划分的商品):
{
"code": 0,
"msg": "成功",
"data": [
{
"name": "热榜",
"type": 1,
"foods": [
{
"id": "123456",
"name": "皮蛋粥",
"price": 1.2,
"description": "好吃的皮蛋粥",
"icon": "http://xxx.com",
}
]
}
]
}
4.2 商品服务配置概要
新建一个Springboot项目,版本2.1.4
group:com.imooc
artifact:product
依赖:
<dependency>
<!--这个是添加依赖时选择eureka discovery生成的-->
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
加上注解
@SpringBootApplication
@EnableDiscoveryClient
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
配置properties
spring.application.name=product
eureka.client.service-url.defaultZone = http://localhost:8761/eureka
服务端的配置
eureka.client.service-url.defaultZone = http://localhost:8761/eureka/
eureka.client.register-with-eureka=false
eureka.server.enable-self-preservation=false
spring.application.name=eureka
server.port=8761
然后客户端就可以启动了,至于服务端更具体的配置与启动可以看入门(一)
4.3 商品服务业务代码
就不放具体的代码了,能懂大概的意思就行了,总的来说就是取出后端的商品与类别,并进行4.1方式的组合,最后返回给前端其需要的数据.
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@Autowired
private CategoryService categoryService;
@GetMapping("/list")
public ResultVO<ProductVO> list(){
//从商品服务取得所有已上架的商品
List<ProductInfo> productInfoList = productService.findUpAll();
/*拉姆达表达式*/
//取出上架商品中的所有类别id
List<Integer> categoryTypeList = productInfoList.stream()
.map(ProductInfo::getCategoryType)
.collect(Collectors.toList());
//根据取出的类别id去数据库中查询具体的类别集合
List<ProductCategory> categoryList = categoryService.findByCategoryTypeIn(categoryTypeList);
//新建我们要向前端返回的数据
List<ProductVO> productVOList = new ArrayList<>();
//将取出的商品与取出的类别进行组合,以满足前端的需求
for (ProductCategory productCategory:categoryList){
ProductVO productVO = new ProductVO();
productVO.setCategoryname(productCategory.getCategoryName());
productVO.setCategoryType(productCategory.getCategoryType());
//此类别对应的商品集合,将商品存入集合
List<ProductInfoVO> productInfoVOList = new ArrayList<>();
for(ProductInfo productInfo:productInfoList){
//当商品id与类别id相等时
if(productInfo.getCategoryType().equals(productCategory.getCategoryType())){
ProductInfoVO productInfoVO = new ProductInfoVO();
BeanUtils.copyProperties(productInfo,productInfoVO);
productInfoVOList.add(productInfoVO);
}
}
productVO.setProductInfoVOList(productInfoVOList);
productVOList.add(productVO);
}
//将组合完的数据返回前端
return ResultVOUtil.success(productVOList);
}
}
第五章 服务拆分之订单服务
5.1 API介绍
POST请求创建一个订单
url:POST /order/create
参数
name: "张三"
phone: "18868822111"
address: "慕课网总部"
openid: "ew3euwhd7sjw9diwkq" //用户的微信openid
items: [{
productId: "1423113435324",
productQuantity: 2 //购买数量
}]
返回
{
"code": 0,
"msg": "成功",
"data": {
"orderId": "147283992738221"
}
}
流程:
1.参数校验
2.查询商品信息(调用商品服务)
3.计算出总价
4.扣库存(调用商品服务)
5.订单入库
5.2 订单服务配置概要
版本Springboot 2.1.4
groupId:com.imooc
artifactId:order
依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
加上注解:
@SpringBootApplication
@EnableDiscoveryClient
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
application.properties配置:
spring.application.name=order
#datasource
spring.datasource.url=jdbc:mysql://localhost:3306/SpringCloud_Sell?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#Jpa
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
eureka.client.service-url.defaultZone = http://localhost:8761/eureka
server.port=8081
服务器配置同上.
然后客户端就可以启动了,至于服务端更具体的配置与启动可以看入门(一)
4.4 订单服务代码
就不放具体的代码了,能懂大概的意思就行了,总的来说就是注释中的几步
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
/*
1.参数校验
2.查询商品信息(调用商品服务)
3.计算出总价
4.扣库存(调用商品服务)
5.订单入库
*/
@Autowired
private OrderService orderService;
@PostMapping("/create")
public ResultVO<Map<String,String>> create(@Valid OrderForm orderForm, BindingResult bindingResult){
if(bindingResult.hasErrors()){
log.error("创建订单参数不正确 ={}",orderForm);
throw new OrderException(1,bindingResult.getFieldError().getDefaultMessage());
}
//在这一步中,查询商品信息,计算总价,扣库存
OrderDTO orderDTO = OrderForm2OrderDTO.convert(orderForm);
//判断购物车是否为空
if(CollectionUtils.isEmpty(orderDTO.getOrderDetailList())){
log.error("创建订单信息失败,购物车为空");
throw new OrderException(-1,"购物车为空");
}
OrderDTO result = orderService.create(orderDTO);
Map<String,String> map = new HashMap<>();
map.put("orderId",result.getOrderId());
return ResultVOUtil.success(map);
}
}