SpringCloud-3-服务拆分

1.微服务拆分的起点

1.1如何拆分微服务?

现在微服务概念炒得很热,关于如何拆分微服务,有以下几点。

  1. 1.先明白起点和终点
    起点:既有架构的形态。
    终点:好的架构不是设计出来的,而是进化而来的。
  2. 典型架构

1.2 适合上微服务么?

业务形态不适合的
1.系统中包含很多很多强事务场景的(因为微服务是分布式的,分布式强事务CAP最多也就能达到最终一致性)
2.业务相对稳定、迭代周期长
3.访问压力不大、可用性要求不高(中小型企业的OA)

2.康威定律和微服务

2.1 康威定律

除了业务形态不适合的,其实还有其他条件,很可能导致不适合迁移到微服务中去,首先我们看一下微服务理论基础:康威定律。

一句话概括就是:沟通的问题会影响系统设计
所以微服务都是强调小团队开发,大的系统拆分成微服务时候,大的团队随机也会拆分成小的团队。

2.2 微服务和团队结构
  1. 微服务特点
  2. 传统vs微服务

3.点餐业务服务拆分分析

点餐系统分为:买家端 和 卖家端

3.1 服务拆分


上面:服务两种方式拆分:
第一种(按照终端):买家端(手机端)的ui单独为一个服务放到nginx里面,卖家端(PC端)的ui单独为一个服务,两个服务同时向后端的通用服务请求数据
第二种(按照业务):将订单ui,商品ui,支付ui都放到一个边缘服务。

以上两种都不对,如果只是自己运营的的点餐应用:团队只有一个,并且业务变化也不大,没有微服务化的必要。如果是一个快速发展的IT公司点餐部门,业务快速发展,需求不断提出,所以需要拆分成微服务。

所以说:起点和团队结构,沟通方式都会决定微服务的设计。

3.2 服务拆分方法论

下图出自<<可扩展的艺术>>书籍

  1. X轴 水平复制:通过应用程序扩展,通过负载均衡运行多个父本一样的应用程序
  2. Z轴 数据分区:将服务按数据分区、每个服务器负责一个数据子集、每个服务器运行的代码是一样的
  3. Y轴 功能解耦:不同职责模块分成不同服务

通过以上模型,知道服务拆分的两个关键职责:功能和数据

3.2.1 如何拆”功能”
  1. 1.单一职责(每个服务负责业务功能的单独一部分)、松耦合(服务之间耦合度低,修改一个服务不用导致另一个服务修改)、高内聚(服务内部相关行为都聚集在一个服务内,而不是分散在不同服务中心)。
  2. 2.关注点分离:-按职责 -按通用性 -按粒度级别(微服务并不是越小越好,要合适)
3.2.2 如何拆”数据”

拆分功能和拆分数据是有先后顺序

  1. 1.先考虑业务功能、再考虑业务功能对应的数据
  2. 2.无状态服务(状态:如果一个数据要被多个服务共享才能完成一个请求,那么这个数据就称为状态,进而依赖这个状态数据的服务称为有状态服务)

    如上所示把业务数据存放到有状态服务的数据库,缓存中实现前端微服务和后端微服务质检无状态服务,数据间没有太大耦合性。

如何拆数据:
1.每个微服务都有单独数据存储(微服务共有数据库,有可能其中一个数据库出现问题会影响其他微服务,一个服务要获取另一个服务的数据,不能直接连接库去请求,而是调用另一个服务接口去获取数据,服务之间有隔离)
2.依据服务特点选择不同结构的数据库类型(如果:数据基于搜索的,那么es合适,如果是非机构化数据,那么nosql的mongodb合适)
3.难点在确定边界

3.2.3 点餐业务服务拆分分析


1.不要期望服务拆分一次就正确,微服务是不断演进的

4.商品服务

4.1 商品服务api和sql介绍

商品列表

1
GET /product/list

参数

1

返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"code": 0,
"msg": "成功",
"data": [
{
"name": "热榜",
"type": 1,
"foods": [
{
"id": "123456",
"name": "皮蛋粥",
"price": 1.2,
"description": "好吃的皮蛋粥",
"icon": "http://xxx.com",
}
]
},
{
"name": "好吃的",
"type": 2,
"foods": [
{
"id": "123457",
"name": "慕斯蛋糕",
"price": 10.9,
"description": "美味爽口",
"icon": "http://xxx.com",
}
]
}
]
}
```
sql

– 类目
CREATE TABLE product_category (
category_id INT NOT NULL AUTO_INCREMENT,
category_name VARCHAR(64) NOT NULL COMMENT ‘类目名字’,
category_type INT NOT NULL COMMENT ‘类目编号’,
create_time TIMESTAMP NOT NULL COMMENT ‘创建时间’,
update_time TIMESTAMP NOT NULL COMMENT ‘修改时间’,
PRIMARY KEY (category_id),
UNIQUE KEY uqe_category_type (category_type)
);
– 商品
CREATE TABLE product_info (
product_id VARCHAR(32) NOT NULL,
product_name VARCHAR(64) NOT NULL COMMENT ‘商品名称’,
product_price DECIMAL(8,2) NOT NULL COMMENT ‘单价’,
product_stock INT NOT NULL COMMENT ‘库存’,
product_description VARCHAR(64) COMMENT ‘描述’,
product_icon VARCHAR(512) COMMENT ‘小图’,
product_status TINYINT(3) DEFAULT ‘0’ COMMENT ‘商品状态,0正常1下架’,
category_type INT NOT NULL COMMENT ‘类目编号’,
create_time TIMESTAMP NOT NULL COMMENT ‘创建时间’,
update_time TIMESTAMP NOT NULL COMMENT ‘修改时间’,
PRIMARY KEY (product_id)
);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
##### 4.2 商品服务编码
###### 4.2.1 创建工程
1. 创建一个Eureka客户端服务,并修改pom.xml文件 保持版本一致性。
2. 修改项目名称并注册到Eureka注册中心
3. 添加注解:@EnableDiscoveryClient
###### 4.2.2 启动项目,查看Eureka Server是否有注册的服务
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/73.png)
###### 4.2.3 书写业务
1. 1.添加mysql驱动和jpa依赖
2. application.yml中配置数据源
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/74.png)
#### 5.订单服务
##### 5.1 业务逻辑分析
### 创建订单

POST /order/create

1
2
参数

name: “张三”
phone: “18868822111”
address: “总部”
openid: “ew3euwhd7sjw9diwkq” //用户的微信openid
items: [{
productId: “1423113435324”,
productQuantity: 2 //购买数量
}]

1
2
3
返回

{
“code”: 0,
“msg”: “成功”,
“data”: {
“orderId”: “147283992738221”
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
订单服务业务逻辑思路:
1.参数校验
2.查询商品信息(此时信息有可能不在订单系统中,可能在商品微服务中,则调用商品服务查询)
3.计算总价
4.扣库存(调用商品服务)
5.订单入库
##### 5.2 编码实践
###### 5.2.1 创建应用 注册到注册中心
略(按照:client客户端创建)
###### 5.2.2 dao
订单服务业务逻辑思路:
1.参数校验
2.查询商品信息(此时信息有可能不在订单系统中,可能在商品微服务中,则调用商品服务查询)
3.计算总价
4.扣库存(调用商品服务)
5.订单入库
###### 5.2.3 service
1. 由于传递的参数包含:买家信息和订单信息,所以创建订单的service需要做一个数据转换 然后insert到对应买家和订单表里面,创建dto(Data Transfer Object)包,创建OrderDTO(买家和订单详情是一对多的关系,所以里面OrderDTO里是一个买家信息加许多商品信息)
2. OrderService里面创建订单的参数和返回为:OrderDTO
3. OrderServiceImpl里面实现创建订单:分为以下四步:1.查询商品 2.计算总价 3.扣除库存 4.订单入库。由于前3步都需要调用其他商品服务,所以我们写为:TODO
4. 创建OrderMaster时候,我们需要设置买家状态:orderStatus,payStatus我们用枚举实现如下:
```
public enum OrderStatusEnum {
NEW(0, "新订单"),
FINISHED(1, "完结"),
CANCEL(2, "取消"),
;
private Integer code;
private String message;
OrderStatusEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
public enum PayStatusEnum {
WAIT(0, "等待支付"),
SUCCESS(1, "支付成功"),
;
private Integer code;
private String message;
PayStatusEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
```
5. OrderId是主键,我们用简单的KeyUtil生成工具生成

public class KeyUtil {
/**

 * 生成唯一的主键
 * 格式: 时间+随机数
 */
public static synchronized  String  genUniqueKey(){
    Random random = new Random();
    Integer number = random.nextInt(900000)+100000;
    return System.currentTimeMillis()+String.valueOf(number);
}

}

1
2
3
4
5
6
7
8
6. service逻辑如下:
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/82.png)
###### 5.2.4 controller
1. controller里面需要实现的业务逻辑大部分都是在service中,此处只是多了个参数校验,所以分为以下几步:1.参数校验 2.查询商品信息(调用商品服务) 3.计算总价 4.扣库存(调用商品服务) 5.订单入库
2. 涉及很多参数时候,我们封装前端参数,此处叫:OrderForm,并在里面添加了参数校验,不用在controller方法里面(使用@NotEmpty)校验。OrderForm此处省略getter,setter也可以用lombok加上@Data注解。

public class OrderForm {
/**

 * 买家姓名
 */
@NotEmpty(message = "姓名必填")
private String name;

/**
 * 买家手机号
 */
@NotEmpty(message = "手机号必填")
private String phone;

/**
 * 买家地址
 */
@NotEmpty(message = "地址必填")
private String address;

/**
 * 买家微信openid
 */
@NotEmpty(message = "openid必填")
private String openid;

/**
 * 购物车
 */
@NotEmpty(message = "购物车不能为空")
private String items;

}

1
2
3
4
5
3. 在create方法中添加@Valid注解
4. 如果参数校验有错误时候抛出异常(自定义异常)OrderException(Integer code, String message),其中code,message不使用硬编码,定义一个枚举:ResultEnum

public class OrderException extends RuntimeException {
private Integer code;
public OrderException(Integer code, String message){
super(message);
this.code = code;
}
public OrderException(ResultEnum resultEnum) {
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}
}

@Getter
public enum ResultEnum {
PARAM_ERROR(1, “参数错误”),
CART_EMPTY(2, “购物车为空”)
;

private Integer code;

private String message;

ResultEnum(Integer code, String message) {
    this.code = code;
    this.message = message;
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
5. 在controller里面将校验过的参数:OrderForm转换成OrderDTO,此处转换封装成工具类处理(其中json转换成数据使用Gson),转换失败时候抛出异常,打印日志
```
@Slf4j
public class OrderForm2OrderDTOConverter {
public static OrderDTO convert(OrderForm orderForm) {
Gson gson = new Gson();
OrderDTO orderDTO = new OrderDTO();
orderDTO.setBuyerName(orderForm.getName());
orderDTO.setBuyerPhone(orderForm.getPhone());
orderDTO.setBuyerAddress(orderForm.getAddress());
orderDTO.setBuyerOpenid(orderForm.getOpenid());
List<OrderDetail> orderDetailList = new ArrayList<>();
try {
orderDetailList = gson.fromJson(orderForm.getItems(),
new TypeToken<List<OrderDetail>>() {
}.getType());
} catch (Exception e) {
log.error("【json转换】错误, string={}", orderForm.getItems());
throw new OrderException(ResultEnum.PARAM_ERROR);
}
orderDTO.setOrderDetailList(orderDetailList);
return orderDTO;
}
}

  1. 接口返回的结果data里面只有一个字段,所以不需要创建一个对象,一个封装一个map返回就行
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 1.参数校验
     * 2.查询商品信息(调用商品服务)
     * 3.计算总价
     * 4.扣库存(调用商品服务)
     * 5.订单入库
     */
    @PostMapping("/create")
    private Object create(@Valid OrderForm orderForm,
                          BindingResult bindingResult){
        //检验不通过抛出自定义异常
        if (bindingResult.hasErrors()){
            //https://www.cnblogs.com/weiapro/p/7633645.html
           log.error("【创建订单】参数不正确, orderForm={}", orderForm);
            throw new OrderException(ResultEnum.PARAM_ERROR.getCode(),
                    bindingResult.getFieldError().getDefaultMessage());
        }
        //orderForm->orderDTO
        OrderDTO orderDTO = OrderForm2OrderDTOConverter.convert(orderForm);
        if(CollectionUtils.isEmpty(orderDTO.getOrderDetailList())){
            log.error("【创建订单】购物车信息为空");
            throw new OrderException(ResultEnum.CART_EMPTY);
        }
        OrderDTO result = orderService.create(orderDTO);
        Map<String, String> map = new HashMap<>();
        map.put("orderId", result.getOrderId());
        return ResultVOUtil.success(map);
    }
}

毕业于<br>相信技术可以改变人与人之间的生活<br>码农一枚