### 订单系统简介
订单系统是交易平台的核心系统,涉及多个方面的复杂任务,需要仔细考虑业务需求、性能、可扩展性和安全性等因素。把订单系统拆分为订单服务、库存服务、优惠券服务、支付服务等等,每个服务都有自己独立数据库。订单处理过程中必然会涉及到分布式事务,例如创建订单与扣减库存需要保证原子性,在分布式系统中,保证这些操作的原子性,会遇到不少难题需要克服,例如`进程crash问题`、`幂等问题`、`回滚问题`、`精准补偿问题`等。
在单体服务订单系统中,使用数据库的本身支持的事务很容易解决,服务化之后必须考虑分布式系统问题,目前常见的解决分布式事务有`消息队列方案`和`状态机方案`,两种解决方案都比较重,使得订单系统变得更复杂。而[dtm](https://github.com/dtm-labs/dtm)作为另一种解决分布式事务方案,极大的简化了订单系统架构,使用dtm优雅的解决了分布式事务中的数据一致性问题。
![order-system.png](https://static.golangjob.cn/231205/203cafb0c9e8b19aca46e7c0481424f5.png)
当前端请求**grpc网关服务order_gw**提交订单api接口,服务端完成以下操作:
- **订单服务order**:在订单表中创建订单,订单id作为唯一键。
- **库存服务stock**:在库存表中扣减库存,如果库存不足,全局事务自动回滚。
- **优惠券服务coupon**:在优惠券表中标记优惠券已使用,如果优惠券无效,全局事务自动回滚。
- **支付服务pay**:在支付表中创建支付单,最后告诉用户跳转到支付页付款。
<br>
下面从0开始搭建一个简单的订单系统,这是按照下面步骤搭建的[订单系统源码](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction)。
<br>
### 准备工作
(1) 准备一个mysql服务,使用脚本[docker-compose.yaml](https://github.com/zhufuyi/sponge/blob/main/test/server/mysql/docker-compose.yaml)快速启动一个mysql服务。
<br>
(2) 把准备好的sql导入到mysql。
- dtm相关sql
- [dtmcli.barrier.mysql.sql](https://github.com/dtm-labs/dtm/blob/main/sqls/dtmcli.barrier.mysql.sql)
- [dtmsvr.storage.mysql.sql](https://github.com/dtm-labs/dtm/blob/main/sqls/dtmsvr.storage.mysql.sql)
- 订单相关sql
- [order.sql](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/order/test/sql/order.sql)
- [stock.sql](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/stock/test/sql/stock.sql)
- [coupon.sql](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/coupon/test/sql/coupon.sql)
- [pay.sql](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/pay/test/sql/pay.sql)
<br>
(3) 准备proto文件。
- [order.proto](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/order/api/order/v1/order.proto) 用来创建订单服务order。
- [stock.proto](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/stock/api/stock/v1/stock.proto) 用来创建库存服务stock。
- [coupon.proto](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/coupon/api/coupon/v1/coupon.proto) 用来创建优惠券服务coupon。
- [pay.proto](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/pay/api/pay/v1/pay.proto) 用来创建支付服务pay。
- [order_gw.proto](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/order_gw/api/order_gw/v1/order_gw.proto) 用来创建grpc网关服务order_gw。
<br>
(4) 安装工具 [sponge](https://github.com/zhufuyi/sponge/blob/main/assets/install-cn.md)。
安装完工具sponge后,执行命令打开生成代码的UI界面:
```bash
sponge run
```
<br>
(5) 启动分布式事务管理器dtm服务。
使用docker-compose.yml脚本运行一个dtm服务。
```yaml
version: '3'
services:
dtm:
image: yedf/dtm
container_name: dtm
restart: always
environment:
STORE_DRIVER: mysql
STORE_HOST: '192.168.3.37'
STORE_USER: root
STORE_PASSWORD: '123456'
STORE_PORT: 3306
#volumes:
# - /etc/localtime:/etc/localtime:ro
# - /etc/timezone:/etc/timezone:ro
ports:
- '36789:36789'
- '36790:36790'
```
修改STORE_xxx相关环境变量值,然后启动dtm服务:
```bash
docker-compose up -d
```
<br>
### 快速创建订单系统相关的微服务
#### 生成订单、库存、优惠券、支付、grpc网关5个服务代码
进入sponge的UI界面,点击左边菜单栏【Protobuf】-->【创建微服务项目】,填写参数,分别生成订单、库存、优惠券、支付服务代码。
快速创建订单服务order,如下图所示:
![order-grpc-pb-order.png](https://static.golangjob.cn/231205/28023dd147d5945b91ad0ca2e0c2dc55.png)
<br>
快速创建库存服务stock,如下图所示:
![order-grpc-pb-stock.png](https://static.golangjob.cn/231205/e13901e23f2c6c3c4372bded5a44f8f1.png)
<br>
快速创建优惠券服务coupon,如下图所示:
![order-grpc-pb-coupon.png](https://static.golangjob.cn/231205/6617614849212712723b097af84e9f9d.png)
<br>
快速创建支付服务pay,如下图所示:
![order-grpc-pb-pay.png](https://static.golangjob.cn/231205/d9134a47f45299fd8d75f457696dfca2.png)
<br>
快速创建grpc网关服务order_gw,点击左边菜单栏【Protobuf】-->【创建grpc网关服务】,填写参数,点击下载代码按钮即可,如下图所示:
![order-http-pb-order-gw.png](https://static.golangjob.cn/231205/4cfcc5f1b68535bc23f8c9e41c1901b6.png)
<br>
把生成的5个服务名称分别修改为order、stock、coupon、pay、order_gw,并打开5个终端,每个服务对应一个终端。
<br>
#### 配置和运行库存服务stock
切换到库存服务stock目录,按下面步骤操作:
(1) 生成与自动合并api接口相关代码。
```bash
make proto
```
<br>
(2) 添加连接mysql代码。
```bash
make patch TYPE=mysql-init
```
<br>
(3) 打开配置文件`configs/stock.yml`,修改mysql地址和账号信息,修改默认的grpc服务端口,主要是为了避免端口冲突。
```yaml
mysql:
dsn: "root:123456@(192.168.3.37:3306)/eshop_stock?parseTime=true&loc=Local&charset=utf8mb4"
grpc:
port: 28282
httpPort: 28283
```
<br>
(4) 在生成的模板代码上添加扣减库存和补偿库存的业务逻辑代码,点击查看代码[internal/service/stock.go](https://github.com/zhufuyi/sponge_examples/blob/main/9_order-grpc-distributed-transaction/stock/internal/service/stock.go)。
<br>
(5) 编译和启动库存服务stock:
```bash
make run
```
<br>
这是根据上面步骤完成的[库存服务stock源码](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/stock)。
<br>
#### 配置和运行优惠券服务coupon
切换到优惠券服务coupon目录,操作步骤与上面的**配置和运行库存服务stock**一样,除了业务逻辑代码。这是[优惠券服务coupon源码](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/coupon)。
配置和填写完具体的业务逻辑代码后,编译和启动优惠券服务coupon:
```bash
make run
```
<br>
#### 配置和运行支付服务pay
切换到支付服务pay目录,操作步骤与上面的**配置和运行库存服务stock**一样,除了业务逻辑代码。这是[支付服务pay源码](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/pay)。
配置和填写完具体的业务逻辑代码码后,编译和启动支付服务pay:
```bash
make run
```
<br>
#### 配置和运行订单服务order
切换到订单服务order目录,操作步骤与上面的**配置和运行库存服务stock**一样,除了业务逻辑代码。这是[订单服务order源码](https://github.com/zhufuyi/sponge_examples/tree/main/9_order-grpc-distributed-transaction/order)。
因为提交订单时候需要把订单服务order、库存服务stock、优惠券服务coupon、支付服务pay的grpc服务地址告诉dtm服务,让dtm服务协调管理分布式事务,所以需要配置这些地址,打开配置文件`configs/order.yml`,添加订单相关的服务地址和dmt服务地址配置,如下所示:
```yaml
grpcClient:
- name: "order"
host: "127.0.0.1"
port: 8282
- name: "coupon"
host: "127.0.0.1"
port: 18282
registryDiscoveryType: ""
enableLoadBalance: false
- name: "stock"
host: "127.0.0.1"
port: 28282
registryDiscoveryType: ""
enableLoadBalance: false
- name: "pay"
host: "127.0.0.1"
port: 38282
registryDiscoveryType: ""
enableLoadBalance: false
dtm:
addr: "127.0.0.1:36790"
```
配置文件添加了新字段,需要更新到对应的go结构体代码:
```bash
make update-config
```
<br>
在生成的模板代码上添加的提交订单、创建订单、取消订单业务逻辑代码,点击查看代码[internall/service/order.go](https://github.com/zhufuyi/sponge_examples/blob/main/9_order-grpc-distributed-transaction/order/internal/service/order.go)。
<br>
配置和填写完业务逻辑代码码后,编译和启动订单服务:
```bash
make run
```
<br>
#### 配置和运行grpc网关服务order_gw
(1) 生成grpc服务连接代码。
grpc网关服务order_gw作为请求入口,因为前端是http请求,而后端是grpc服务,需要把http转为grpc请求,因此需要生成连接order服务的代码,如果有必要也可以按照同样步骤添加其他服务(stock、coupon、pay)的grpc连接代码。进入sponge的UI界面,点击左边菜单栏【Public】-->【生成grpc服务连接代码】,填写参数生成grpc服务连接代码,如下图所示:
![order-grpc-conn.png](https://static.golangjob.cn/231205/2a187726ea3913d94e7ca9a22a655ca5.png)
解压代码,把internal目录移动到grpc网关服务order_gw服务目录下。
<br>
(2) 复制proto文件。
因为grpc网关服务order_gw需要知道订单服务order有哪些api接口可以调用,因此需要把订单服务order的proto文件复制过来,打开终端,切换到order_gw目录,执行命令:
```bash
make copy-proto SERVER=../order
```
<br>
(3) 打开配置文件`configs/order_gw.yml`,配置订单服务order地址。
```yaml
grpcClient:
- name: "order"
host: "127.0.0.1"
port: 8282
registryDiscoveryType: ""
enableLoadBalance: false
```
<br>
(4) 生成与自动合并api接口相关代码。
```bash
make proto
```
<br>
(5) 填写业务逻辑代码,也就是http请求转为grpc请求,这里可以直接使用已经生成的模板代码示例即可。点击查看代码[internal/service/order_gw.go](https://github.com/zhufuyi/sponge_examples/blob/main/9_order-grpc-distributed-transaction/order_gw/internal/service/order_gw.go)。
<br>
配置和填写完业务逻辑代码码后,编译和启动grpc网关服务order_gw:
```bash
make run
```
<br>
### 测试分布式事务
在浏览器打开swagger界面 `http://localhost:8080/apis/swagger/index.html`,测试提交订单api接口。
在dtm的管理界面 `http://localhost:36789` 可以查看分布式事务状态和详情。
在各个服务的终端可以查看日志信息了解dtm协调调用的api接口情况。
<br>
#### 测试成功提交订单场景
在swagger界面上,填写请求参数。
![order-http-pb-order-gw-swagger.png](https://static.golangjob.cn/231205/39b4ff691251eff5eac6e8ec31221349.png)
点击Execute按钮进行测试,提交订单成功,从dtm的管理界面和各个服务日志可以看到。
<br>
#### 测试失败提交订单场景
(1) 优惠券无效造成订单失败。
在请求参数不变情况下,
```json
{
"userId": 1,
"productId": 1,
"amount": 1100,
"productCount": 1,
"couponId": 1
}
```
直接点击Execute按钮测试,虽然返回了订单id(这不表示订单成功,实际需要获取到订单成功状态再执行后面操作),从dtm的管理界面和优惠券服务coupon日志可以看到,订单状态是失败的,因为优惠券已经被使用,返回了`Aborted`错误,dtm收到`Aborted`错误信息之后,会对已经**创建订单**和**扣减库存**分支事务进行补偿,保证数据最终一致。
<br>
(2) 库存不足造成订单失败。
填写请求参数,字段productCount值为1000确定大于了库存数量,把参数couponId设置为0表示不使用优惠券。
```json
{
"userId": 1,
"productId": 1,
"amount": 1100000,
"productCount": 1000,
"couponId": 0
}
```
点击Execute按钮测试,虽然返回了订单id(这不表示订单成功),从dtm的管理界面和库存服务stock日志可以看到,订单状态是失败的,因为库存不足原因,返回了`Aborted`错误,dtm收到`Aborted`错误信息之后,会对**创建订单**分支事务进行补偿,保证数据最终一致。
后续添加支付业务逻辑代码之后,可以测试账号余额不足导致订单失败,dtm会补偿分支事务确保数据最终一致。
<br>
#### 测试模拟进程crash,恢复后成功提交订单场景
停止库存服务stock,然后在swagger界面填写请求参数:
```json
{
"userId": 1,
"productId": 1,
"amount": 1100,
"productCount": 1,
"couponId": 0
}
```
点击Execute按钮测试,虽然返回了订单id(这不表示订单成功),从dtm的管理界面看到订单状态是`submitted`状态,dtm会一直重试连接库存服务stock,重试默认是指数退避算法,可以修改为固定时间间隔重试。启动库存服务,dtm连接库存服务成功之后,接着完成后续分支事务,成功完成订单,保证数据最终一致。根据业务需求也可以做超时主动强制取消订单处理,dtm收到强制取消订单后,会对**创建订单**分支事务进行补偿,也保证数据最终一致。
<br>
### 总结
本文介绍了从0开始快速搭建一个简单的订单系统,使用sponge很容易构建微服务,使用dtm优雅的解决提交订单的分布式事务,开发一个订单系统变得很简单,让开发人员把精力用在业务开发上。
各个服务的常用服务治理功能也是具备的,例如服务注册与发现、限流、熔断、链路跟踪、监控、性能分析、资源统计、CICD等,这些功能统一在yml配置文件开启或关闭。
这不是完整的订单系统,只有一个提交订单业务逻辑,如果需要构建一个自己的订单系统,可以作为一个参考。根据上面操作步骤,很容易添加商品服务product、物流服务logistics、用户服务user等服务模块组成一个电商平台。
<br>
有疑问加站长微信联系(非本文作者))