1.微服务拆分的起点 1.1如何拆分微服务? 现在微服务概念炒得很热,关于如何拆分微服务,有以下几点。
1.先明白起点和终点 起点:既有架构的形态。 终点:好的架构不是设计出来的,而是进化而来的。
典型架构
1.2 适合上微服务么? 业务形态不适合的 1.系统中包含很多很多强事务场景的(因为微服务是分布式的,分布式强事务CAP最多也就能达到最终一致性) 2.业务相对稳定、迭代周期长 3.访问压力不大、可用性要求不高(中小型企业的OA)
2.康威定律和微服务 2.1 康威定律 除了业务形态不适合的,其实还有其他条件,很可能导致不适合迁移到微服务中去,首先我们看一下微服务理论基础:康威定律。
一句话概括就是:沟通的问题会影响系统设计 所以微服务都是强调小团队开发,大的系统拆分成微服务时候,大的团队随机也会拆分成小的团队。
2.2 微服务和团队结构
微服务特点
传统vs微服务
3.点餐业务服务拆分分析 点餐系统分为:买家端 和 卖家端
3.1 服务拆分 上面:服务两种方式拆分: 第一种(按照终端):买家端(手机端)的ui单独为一个服务放到nginx里面,卖家端(PC端)的ui单独为一个服务,两个服务同时向后端的通用服务请求数据 第二种(按照业务):将订单ui,商品ui,支付ui都放到一个边缘服务。
以上两种都不对,如果只是自己运营的的点餐应用:团队只有一个,并且业务变化也不大,没有微服务化的必要。如果是一个快速发展的IT公司点餐部门,业务快速发展,需求不断提出,所以需要拆分成微服务。
所以说:起点和团队结构,沟通方式都会决定微服务的设计。
3.2 服务拆分方法论 下图出自<<可扩展的艺术>>书籍
X轴 水平复制:通过应用程序扩展,通过负载均衡运行多个父本一样的应用程序
Z轴 数据分区:将服务按数据分区、每个服务器负责一个数据子集、每个服务器运行的代码是一样的
Y轴 功能解耦:不同职责模块分成不同服务
通过以上模型,知道服务拆分的两个关键职责:功能和数据
3.2.1 如何拆”功能”
1.单一职责(每个服务负责业务功能的单独一部分)、松耦合(服务之间耦合度低,修改一个服务不用导致另一个服务修改)、高内聚(服务内部相关行为都聚集在一个服务内,而不是分散在不同服务中心)。
2.关注点分离:-按职责 -按通用性 -按粒度级别(微服务并不是越小越好,要合适)
3.2.2 如何拆”数据” 拆分功能和拆分数据是有先后顺序
1.先考虑业务功能、再考虑业务功能对应的数据
2.无状态服务(状态:如果一个数据要被多个服务共享才能完成一个请求,那么这个数据就称为状态,进而依赖这个状态数据的服务称为有状态服务) 如上所示把业务数据存放到有状态服务的数据库,缓存中实现前端微服务和后端微服务质检无状态服务,数据间没有太大耦合性。
如何拆数据: 1.每个微服务都有单独数据存储(微服务共有数据库,有可能其中一个数据库出现问题会影响其他微服务,一个服务要获取另一个服务的数据,不能直接连接库去请求,而是调用另一个服务接口去获取数据,服务之间有隔离) 2.依据服务特点选择不同结构的数据库类型(如果:数据基于搜索的,那么es合适,如果是非机构化数据,那么nosql的mongodb合适) 3.难点在确定边界
3.2.3 点餐业务服务拆分分析 1.不要期望服务拆分一次就正确,微服务是不断演进的
4.商品服务 4.1 商品服务api和sql介绍 商品列表
参数
返回
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
name: “张三” phone: “18868822111” address: “总部” openid: “ew3euwhd7sjw9diwkq” //用户的微信openid items: [{ productId: “1423113435324”, productQuantity: 2 //购买数量 }]
{ “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;
}
}
接口返回的结果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);
}
}