订单系统简介
订单系统是交易平台的核心系统,涉及多个方面的复杂任务,需要仔细考虑业务需求、性能、可扩展性和安全性等因素。把订单系统拆分为订单服务、库存服务、优惠券服务、支付服务等等,每个服务都有自己独立数据库。订单处理过程中必然会涉及到分布式事务,例如创建订单与扣减库存需要保证原子性,在分布式系统中,保证这些操作的原子性,会遇到不少难题需要克服,例如进程crash问题
、幂等问题
、回滚问题
、精准补偿问题
等。
在单体服务订单系统中,使用数据库的本身支持的事务很容易解决,服务化之后必须考虑分布式系统问题,目前常见的解决分布式事务有消息队列方案
和状态机方案
,两种解决方案都比较重,使得订单系统变得更复杂。而dtm作为另一种解决分布式事务方案,极大的简化了订单系统架构,使用dtm优雅的解决了分布式事务中的数据一致性问题。
当前端请求grpc网关服务order_gw提交订单api接口,服务端完成以下操作:
- 订单服务order:在订单表中创建订单,订单id作为唯一键。
- 库存服务stock:在库存表中扣减库存,如果库存不足,全局事务自动回滚。
- 优惠券服务coupon:在优惠券表中标记优惠券已使用,如果优惠券无效,全局事务自动回滚。
- 支付服务pay:在支付表中创建支付单,最后告诉用户跳转到支付页付款。
下面从0开始搭建一个简单的订单系统,这是按照下面步骤搭建的订单系统源码。
准备工作
(1) 准备一个mysql服务,使用脚本docker-compose.yaml快速启动一个mysql服务。
(2) 把准备好的sql导入到mysql。
- dtm相关sql
- 订单相关sql
(3) 准备proto文件。
- order.proto 用来创建订单服务order。
- stock.proto 用来创建库存服务stock。
- coupon.proto 用来创建优惠券服务coupon。
- pay.proto 用来创建支付服务pay。
- order_gw.proto 用来创建grpc网关服务order_gw。
(4) 安装工具 sponge。
安装完工具sponge后,执行命令打开生成代码的UI界面:
bash
sponge run
(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
快速创建订单系统相关的微服务
生成订单、库存、优惠券、支付、grpc网关5个服务代码
进入sponge的UI界面,点击左边菜单栏【Protobuf】-->【创建微服务项目】,填写参数,分别生成订单、库存、优惠券、支付服务代码。
快速创建订单服务order,如下图所示:
快速创建库存服务stock,如下图所示:
快速创建优惠券服务coupon,如下图所示:
快速创建支付服务pay,如下图所示:
快速创建grpc网关服务order_gw,点击左边菜单栏【Protobuf】-->【创建grpc网关服务】,填写参数,点击下载代码按钮即可,如下图所示:
把生成的5个服务名称分别修改为order、stock、coupon、pay、order_gw,并打开5个终端,每个服务对应一个终端。
配置和运行库存服务stock
切换到库存服务stock目录,按下面步骤操作:
(1) 生成与自动合并api接口相关代码。
bash
make proto
(2) 添加连接mysql代码。
bash
make patch TYPE=mysql-init
(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
(4) 在生成的模板代码上添加扣减库存和补偿库存的业务逻辑代码,点击查看代码internal/service/stock.go。
(5) 编译和启动库存服务stock:
bash
make run
这是根据上面步骤完成的库存服务stock源码。
配置和运行优惠券服务coupon
切换到优惠券服务coupon目录,操作步骤与上面的配置和运行库存服务stock 一样,除了业务逻辑代码。这是优惠券服务coupon源码。
配置和填写完具体的业务逻辑代码码后,编译和启动优惠券服务coupon:
bash
make run
配置和运行支付服务pay
切换到支付服务pay目录,操作步骤与上面的配置和运行库存服务stock 一样,除了业务逻辑代码。这是支付服务pay源码。
配置和填写完具体的业务逻辑代码码后,编译和启动支付服务pay:
bash
make run
配置和运行订单服务order
切换到订单服务order目录,操作步骤与上面的配置和运行库存服务stock 一样,除了业务逻辑代码。这是订单服务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
在生成的模板代码上添加的提交订单、创建订单、取消订单业务逻辑代码,点击查看代码internall/service/order.go。
配置和填写完业务逻辑代码码后,编译和启动订单服务:
bash
make run
配置和运行grpc网关服务order_gw
(1) 生成grpc服务连接代码。
grpc网关服务order_gw作为请求入口,因为前端是http请求,而后端是grpc服务,需要把http转为grpc请求,因此需要生成连接order服务的代码,如果有必要也可以按照同样步骤添加其他服务(stock、coupon、pay)的grpc连接代码。进入sponge的UI界面,点击左边菜单栏【Public】-->【生成grpc服务连接代码】,填写参数生成grpc服务连接代码,如下图所示:
解压代码,把internal目录移动到grpc网关服务order_gw服务目录下。
(2) 复制proto文件。
因为grpc网关服务order_gw需要知道订单服务order有哪些api接口可以调用,因此需要把订单服务order的proto文件复制过来,打开终端,切换到order_gw目录,执行命令:
bash
make copy-proto SERVER=../order
(3) 打开配置文件configs/order_gw.yml
,配置订单服务order地址。
yaml
grpcClient:
- name: "order"
host: "127.0.0.1"
port: 8282
registryDiscoveryType: ""
enableLoadBalance: false
(4) 生成与自动合并api接口相关代码。
bash
make proto
(5) 填写业务逻辑代码,也就是http请求转为grpc请求,这里可以直接使用已经生成的模板代码示例即可。点击查看代码internal/service/order_gw.go。
配置和填写完业务逻辑代码码后,编译和启动grpc网关服务order_gw:
bash
make run
测试分布式事务
在浏览器打开swagger界面 http://localhost:8080/apis/swagger/index.html
,测试提交订单api接口。
在dtm的管理界面 http://localhost:36789
可以查看分布式事务状态和详情。
在各个服务终端可以查看日志信息了解dtm调用的api接口情况。
测试成功提交订单场景
在swagger界面上,填写请求参数。
点击Execute按钮进行测试,提交订单成功,从dtm的管理界面和各个服务日志可以看到。
测试失败提交订单场景
(1) 优惠券无效造成订单失败。
在请求参数不变情况下,
json
{
"userId": 1,
"productId": 1,
"amount": 1100,
"productCount": 1,
"couponId": 1
}
直接点击Execute按钮测试,虽然返回了订单id(这不表示订单成功,实际需要获取到订单成功状态再执行后面操作),从dtm的管理界面和优惠券服务coupon日志可以看到,订单状态是失败的,因为优惠券已经被使用,返回了Aborted
错误,dtm收到Aborted
错误信息之后,会对已经创建订单 和扣减库存分支事务进行补偿,保证数据最终一致。
(2) 库存不足造成订单失败。
填写请求参数,字段productCount值为1000确定大于了库存数量,把参数couponId设置为0表示不使用优惠券。
json
{
"userId": 1,
"productId": 1,
"amount": 1100000,
"productCount": 1000,
"couponId": 0
}
点击Execute按钮测试,虽然返回了订单id(这不表示订单成功),从dtm的管理界面和库存服务stock日志可以看到,订单状态是失败的,因为库存不足原因,返回了Aborted
错误,dtm收到Aborted
错误信息之后,会对创建订单分支事务进行补偿,保证数据最终一致。
后续添加支付业务逻辑代码之后,可以测试账号余额不足导致订单失败,dtm会补偿分支事务确保数据最终一致。
测试模拟进程crash,恢复后成功提交订单场景
停止库存服务stock,然后在swagger界面填写请求参数:
json
{
"userId": 1,
"productId": 1,
"amount": 1100,
"productCount": 1,
"couponId": 0
}
点击Execute按钮测试,虽然返回了订单id(这不表示订单成功),从dtm的管理界面看到订单状态是submitted
状态,dtm会一直重试连接库存服务stock,重试默认是指数退避算法,可以修改为固定时间间隔重试。启动库存服务,dtm连接库存服务成功之后,接着完成后续分支事务,成功完成订单,保证数据最终一致。根据业务需求也可以做超时主动强制取消订单处理,dtm收到强制取消订单后,会对创建订单分支事务进行补偿,也保证数据最终一致。
总结
本文介绍了从0开始快速搭建一个简单的订单系统,使用sponge很容易构建微服务,使用dtm优雅的解决提交订单的分布式事务,开发一个订单系统变得很简单,让开发人员把精力用在业务开发上。
各个服务的常用服务治理功能也是具备的,例如服务注册与发现、限流、熔断、链路跟踪、监控、性能分析、资源统计、CICD等,这些功能统一在yml配置文件开启或关闭。
这不是完整的订单系统,只有一个提交订单业务逻辑,如果需要构建一个自己的订单系统,可以作为一个参考。根据上面操作步骤,很容易添加商品服务product、物流服务logistics、用户服务user等服务模块组成一个电商平台。