案例介绍
业务分析
模拟电商网站购物场景中的【下单】和【支付】业务
下单
- 用户请求订单系统下单
- 订单系统通过RPC调用订单服务下单
- 订单服务调用优惠券服务,扣减优惠券
- 订单服务调用调用库存服务,校验并扣减库存
- 订单服务调用用户服务,扣减用户余额
- 订单服务完成确认订单
支付
- 用户请求支付系统
- 支付系统调用第三方支付平台API进行发起支付流程
- 用户通过第三方支付平台支付成功后,第三方支付平台回调通知支付系统
- 支付系统调用订单服务修改订单状态
- 支付系统调用积分服务添加积分
- 支付系统调用日志服务记录日志
问题分析
问题1
用户提交订单后,扣减库存成功、扣减优惠券成功、使用余额成功,但是在确认订单操作失败,需要对库存、库存、余额进行回退。
如何保证数据的完整性?
使用MQ保证在下单失败后系统数据的完整性
问题2
用户通过第三方支付平台(支付宝、微信)支付成功后,第三方支付平台要通过回调API异步通知商家支付系统用户支付结果,支付系统根据支付结果修改订单状态、记录支付日志和给用户增加积分。
商家支付系统如何保证在收到第三方支付平台的异步通知时,如何快速给第三方支付凭条做出回应?
通过MQ进行数据分发,提高系统处理性能
技术分析
技术选型
- SpringBoot
- Dubbo
- Zookeeper
- RocketMQ
- Mysql
SpringBoot整合RocketMQ
下载rocketmq-spring项目
将rocketmq-spring安装到本地仓库
1 | mvn install -Dmaven.skip.test=true |
消息生产者
添加依赖
1 | <parent> |
配置文件
1 | # application.properties |
启动类
1 |
|
测试类
1 | (SpringRunner.class) |
消息消费者
添加依赖
同消息生产者
配置文件
同消息生产者
启动类
1 |
|
消息监听器
1 | 4j |
SpringBoot整合Dubbo
下载dubbo-spring-boot-starter依赖包
将
1 |
|
搭建Zookeeper集群
准备工作
- 安装JDK
- 将Zookeeper上传到服务器
- 解压Zookeeper,并创建data目录,将conf下的zoo_sample.cfg文件改名为zoo.cfg
建立
1
2
3
4```sh
/usr/local/zookeeper-cluster/zookeeper-1
/usr/local/zookeeper-cluster/zookeeper-2
/usr/local/zookeeper-cluster/zookeeper-3配置每一个 Zookeeper 的 dataDir(zoo.cfg) clientPort 分别为 2181 2182 2183
修改1
2
3
4
```shell
clientPort=2181
dataDir=/usr/local/zookeeper-cluster/zookeeper-1/data
修改/usr/local/zookeeper-cluster/zookeeper-2/conf/zoo.cfg
1 | clientPort=2182 |
修改/usr/local/zookeeper-cluster/zookeeper-3/conf/zoo.cfg
1 | clientPort=2183 |
配置集群
- 在每个 zookeeper 的 data 目录下创建一个 myid 文件,内容分别是 1、2、3 。这个文件就是记录每个服务器的 ID
- 在每一个 zookeeper 的 zoo.cfg 配置客户端访问端口(clientPort)和集群服务器 IP 列表。
集群服务器 IP 列表如下
1 | server.1=192.168.25.140:2881:3881 |
解释:server.服务器 ID=服务器 IP 地址:服务器之间通信端口:服务器之间投票选举端口
启动集群
启动集群就是分别启动每个实例。
RPC服务接口
1 | public interface IUserService { |
服务提供者
添加依赖
1 | <parent> |
配置文件
1 | # application.properties |
启动类
1 |
|
服务实现
1 |
|
服务消费者
添加依赖
1 | <parent> |
配置文件
1 | # application.properties |
启动类
1 |
|
Controller
1 |
|
环境搭建
数据库
优惠券表
Field | Type | Comment |
---|---|---|
coupon_id | bigint(50) NOT NULL | 优惠券ID |
coupon_price | decimal(10,2) NULL | 优惠券金额 |
user_id | bigint(50) NULL | 用户ID |
order_id | bigint(32) NULL | 订单ID |
is_used | int(1) NULL | 是否使用 0未使用 1已使用 |
used_time | timestamp NULL | 使用时间 |
商品表
Field | Type | Comment |
---|---|---|
goods_id | bigint(50) NOT NULL | 主键 |
goods_name | varchar(255) NULL | 商品名称 |
goods_number | int(11) NULL | 商品库存 |
goods_price | decimal(10,2) NULL | 商品价格 |
goods_desc | varchar(255) NULL | 商品描述 |
add_time | timestamp NULL | 添加时间 |
订单表
Field | Type | Comment |
---|---|---|
order_id | bigint(50) NOT NULL | 订单ID |
user_id | bigint(50) NULL | 用户ID |
order_status | int(1) NULL | 订单状态 0未确认 1已确认 2已取消 3无效 4退款 |
pay_status | int(1) NULL | 支付状态 0未支付 1支付中 2已支付 |
shipping_status | int(1) NULL | 发货状态 0未发货 1已发货 2已退货 |
address | varchar(255) NULL | 收货地址 |
consignee | varchar(255) NULL | 收货人 |
goods_id | bigint(50) NULL | 商品ID |
goods_number | int(11) NULL | 商品数量 |
goods_price | decimal(10,2) NULL | 商品价格 |
goods_amount | decimal(10,0) NULL | 商品总价 |
shipping_fee | decimal(10,2) NULL | 运费 |
order_amount | decimal(10,2) NULL | 订单价格 |
coupon_id | bigint(50) NULL | 优惠券ID |
coupon_paid | decimal(10,2) NULL | 优惠券 |
money_paid | decimal(10,2) NULL | 已付金额 |
pay_amount | decimal(10,2) NULL | 支付金额 |
add_time | timestamp NULL | 创建时间 |
confirm_time | timestamp NULL | 订单确认时间 |
pay_time | timestamp NULL | 支付时间 |
订单商品日志表
Field | Type | Comment |
---|---|---|
goods_id | int(11) NOT NULL | 商品ID |
order_id | varchar(32) NOT NULL | 订单ID |
goods_number | int(11) NULL | 库存数量 |
log_time | datetime NULL | 记录时间 |
用户表
Field | Type | Comment |
---|---|---|
user_id | bigint(50) NOT NULL | 用户ID |
user_name | varchar(255) NULL | 用户姓名 |
user_password | varchar(255) NULL | 用户密码 |
user_mobile | varchar(255) NULL | 手机号 |
user_score | int(11) NULL | 积分 |
user_reg_time | timestamp NULL | 注册时间 |
user_money | decimal(10,0) NULL | 用户余额 |
用户余额日志表
Field | Type | Comment |
---|---|---|
user_id | bigint(50) NOT NULL | 用户ID |
order_id | bigint(50) NOT NULL | 订单ID |
money_log_type | int(1) NOT NULL | 日志类型 1订单付款 2 订单退款 |
use_money | decimal(10,2) NULL | 操作金额 |
create_time | timestamp NULL | 日志时间 |
订单支付表
Field | Type | Comment |
---|---|---|
pay_id | bigint(50) NOT NULL | 支付编号 |
order_id | bigint(50) NULL | 订单编号 |
pay_amount | decimal(10,2) NULL | 支付金额 |
is_paid | int(1) NULL | 是否已支付 1否 2是 |
MQ消息生产表
Field | Type | Comment |
---|---|---|
id | varchar(100) NOT NULL | 主键 |
group_name | varchar(100) NULL | 生产者组名 |
msg_topic | varchar(100) NULL | 消息主题 |
msg_tag | varchar(100) NULL | Tag |
msg_key | varchar(100) NULL | Key |
msg_body | varchar(500) NULL | 消息内容 |
msg_status | int(1) NULL | 0:未处理;1:已经处理 |
create_time | timestamp NOT NULL | 记录时间 |
MQ消息消费表
Field | Type | Comment |
---|---|---|
msg_id | varchar(50) NULL | 消息ID |
group_name | varchar(100) NOT NULL | 消费者组名 |
msg_tag | varchar(100) NOT NULL | Tag |
msg_key | varchar(100) NOT NULL | Key |
msg_body | varchar(500) NULL | 消息体 |
consumer_status | int(1) NULL | 0:正在处理;1:处理成功;2:处理失败 |
consumer_times | int(1) NULL | 消费次数 |
consumer_timestamp | timestamp NULL | 消费时间 |
remark | varchar(500) NULL | 备注 |
项目初始化
shop系统基于Maven进行项目管理
工程浏览
- 父工程:shop-parent
- 订单系统:shop-order-web
- 支付系统:shop-pay-web
- 优惠券服务:shop-coupon-service
- 订单服务:shop-order-service
- 支付服务:shop-pay-service
- 商品服务:shop-goods-service
- 用户服务:shop-user-service
- 实体类:shop-pojo
- 持久层:shop-dao
- 接口层:shop-api
- 工具工程:shop-common
共12个系统
工程关系
Mybatis逆向工程使用
代码生成
使用Mybatis逆向工程针对数据表生成CURD持久层代码
代码导入
- 将实体类导入到shop-pojo工程
- 在服务层工程中导入对应的Mapper类和对应配置文件
公共类介绍
ID生成器
IDWorker:Twitter雪花算法
异常处理类
CustomerException:自定义异常类
CastException:异常抛出类
常量类
ShopCode:系统状态类
响应实体类
Result:封装响应状态和响应信息
下单业务
下单基本流程
接口定义
- IOrderService
1 | public interface IOrderService { |
业务类实现
1 | 4j |
校验订单
1 | private void checkOrder(TradeOrder order) { |
生成预订单
1 | private Long savePreOrder(TradeOrder order) { |
扣减库存
- 通过dubbo调用商品服务完成扣减库存
1 | private void reduceGoodsNum(TradeOrder order) { |
- 商品服务GoodsService扣减库存
1 |
|
扣减优惠券
- 通过dubbo完成扣减优惠券
1 | private void changeCoponStatus(TradeOrder order) { |
- 优惠券服务CouponService更改优惠券状态
1 |
|
扣减用户余额
- 通过用户服务完成扣减余额
1 | private void reduceMoneyPaid(TradeOrder order) { |
- 用户服务UserService,更新余额
<img src=”https://tva2.sinaimg.cn/large/006MOU0zgy1gjo12dfbc1j30ek0ucwf3.jpg" alt=”更改用户余额”referrerpolicy=”no-referrer”>
1 |
|
确认订单
1 | private void updateOrderStatus(TradeOrder order) { |
小结
1 |
|
失败补偿机制
消息发送方
- 配置RocketMQ属性值
1 | rocketmq.name-server=192.168.25.135:9876;192.168.25.138:9876 |
- 注入模板类和属性值信息
1 |
|
- 发送下单失败消息
1 |
|
1 | private void sendMessage(String topic, String tags, String keys, String body) throws Exception { |
4.2.2 消费接收方
- 配置RocketMQ属性值
1 | rocketmq.name-server=192.168.25.135:9876;192.168.25.138:9876 |
- 创建监听类,消费消息
1 | 4j |
回退库存
- 流程分析
- 消息消费者
1 | 4j |
回退优惠券
1 | 4j |
回退余额
1 | 4j |
取消订单
1 |
|
测试
准备测试环境
1 | (SpringRunner.class) |
准备测试数据
- 用户数据
- 商品数据
- 优惠券数据
测试下单成功流程
1 |
|
执行完毕后,查看数据库中用户的余额、优惠券数据,及订单的状态数据
测试下单失败流程
代码同上。
执行完毕后,查看用户的余额、优惠券数据是否发生更改,订单的状态是否为取消。
支付业务
创建支付订单
1 | public Result createPayment(TradePay tradePay) { |
支付回调
流程分析
代码实现
1 | public Result callbackPayment(TradePay tradePay) { |
线程池优化消息发送逻辑
- 创建线程池对象
1 |
|
- 使用线程池
1 |
|
处理消息
支付成功后,支付服务payService发送MQ消息,订单服务、用户服务、日志服务需要订阅消息进行处理
- 订单服务修改订单状态为已支付
- 日志服务记录支付日志
- 用户服务负责给用户增加积分
以下用订单服务为例说明消息的处理情况
配置RocketMQ属性值
1 | mq.pay.topic=payTopic |
消费消息
- 在订单服务中,配置公共的消息处理类
1 | public class BaseConsumer { |
- 接受订单支付成功消息
1 | 4j |
整体联调
通过Rest客户端请求shop-order-web和shop-pay-web完成下单和支付操作
准备工作
配置RestTemplate类
1 |
|
配置请求地址
- 订单系统
1 | server.host=http://localhost |
- 支付系统
1 | server.host=http://localhost |
下单测试
1 | (SpringRunner.class) |
支付测试
1 | (SpringRunner.class) |