「这是我参与2022首次更文挑战的第33天,活动详情查看:2022首次更文挑战」
欢迎阅读本专题内的文章,专题地址:juejin.cn/column/7053…
本文项目代码gitee地址: gitee.com/wei_rong_xi…
前面我们搭建了一个抢年货的微服务环境,并且实现了下单和支付的功能,本篇我们来测试下整体环境的性能如何。看看有哪些地方时可以优化的点,最终我们再次验证优化后,性能是否有所提升呢?
一、环境简介
以下所有服务都是单机部署的。
-
本文直接采用本地环境测试了,我的电脑配置如下:
cpu 内存 系统 8核 16g win10 64位 -
需要启动的基础服务如下:
名称 版本 mysql 5.6.48 nacos 2.0.3 jdk 2.0.3 -
部署的服务:
名称 描述 jvm参数 rob-necessities-gateway 网关 -Xmx128m -Xms128m rob-necessities-order 订单服务 -Xmx128m -Xms128m rob-necessities-user 用户服务 -Xmx128m -Xms128m rob-necessities-account 账户 -Xmx128m -Xms128m rob-necessities-trading 交易服务 -Xmx128m -Xms128m rob-necessities-goods 商品服务 -Xmx128m -Xms128m
二、编写测试方法
在rob-necessities-test服务当中,编写测试方法,使用多线程方式调用下单和支付的接口,代码如下:
@GetMapping("/concurrent/order")
public Long concurrentOrder() {
// 使用cyclicBarrier模拟200线程同时到达
CyclicBarrier cyclicBarrier = new CyclicBarrier(200);
for (int i = 0; i < 200; i++) {
new Thread(() -> {
try {
cyclicBarrier.await();
log.info("开始时间:{}", LocalDateTime.now());
Long orderId = orderService.order();
concurrentHashMap.put(orderId, orderId);
tradingService.pay(orderId);
log.info("完成时间:{},订单id: {}", LocalDateTime.now(), orderId);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
return null;
}
使用cyclicBarrier
,等待200个线程同时到达后,进行下单操作,每完成一个订单,直接调用支付服务,打印每个线程的任务开始结束时间。
三、测试验证
3.1 执行测试
测试接口:http://localhost:8010/test/concurrent/order
使用上面的接口,执行测试程序,然后查看结果,会打印很多日志,我们只关注最先开始和最终结束的:
2022-02-17 14:27:59 INFO Thread-62 com.wjbgn.test.controller.TestController 开始时间:2022-02-17T14:27:59.680
... ...
2022-02-17 14:28:19 INFO Thread-91 com.wjbgn.test.controller.TestController 完成时间:2022-02-17T14:28:19.899,订单id: 200
上面的结果表明,最终的200个线程完成全部任务,大约用时:20秒
,平均完成一个订单需要0.1秒。
3.2 数据验证
因为我们是在多线程的场景下,模拟200线程并发的情况,那么会不会出现数据问题呢?我们直接到数据库看结果,我写了如下的查询sql,用来验证结果:
-- 验证订单金额
select sum(order_amount) from rob_order;
-- 验证订单详情金额
select sum(goods_num * goods_unit_price) from rob_order_detail;
-- 验证支付金额
select sum(trading_amount) from rob_trading;
-- 验证用户账号扣款金额
select COUNT(*) * 3000 - sum(user_amount) from rob_user_account where user_amount < 3000;
-- 验证扣款用户数
select count(*) from rob_user_account where user_amount <3000;
-- 验证商品售出库存
select 1000000* count(*) - sum(goods_inventory) from rob_goods;
-- 验证订单购买商品数量
select sum(goods_num) from rob_order_detail
经过验证,数据没有出现并发问题,其实是可以预想的到的,因为在订单和支付接口,都增加了同步机制,我使用的是ReentrantLock
,以订单接口为例,如下所示:
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, OrderDO> implements OrderService {
@Autowired
private GoodsClient goodsClient;
@Autowired
private OrderDetailService orderDetailService;
private ReentrantLock lock = new ReentrantLock();
@Override
public Result saveOrder(OrderDTO orderDTO) {
OrderDO orderDO = new OrderDO();
// 锁,防止下单数据错误
lock.lock();
try {
// 获取商品
Result info = goodsClient.info(orderDTO.getGoodsId());
// 扣减库存
GoodsDTO goodsDTO = JSONObject.toJavaObject(JSONObject.parseObject(JSONObject.toJSONString(info.getData())), GoodsDTO.class);
if (goodsDTO.getGoodsInventory() > orderDTO.getGoodsNum()) {
goodsDTO.setGoodsInventory(goodsDTO.getGoodsInventory() - orderDTO.getGoodsNum());
Result update = goodsClient.update(goodsDTO);
if (update.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
return Result.failed("扣除商品库存失败");
}
//计算订单总金额
double amount = goodsDTO.getGoodsPrice() * orderDTO.getGoodsNum();
orderDTO.setOrderAmount(amount);
orderDTO.setCreateUser(1L);
orderDTO.setOrderStatus(OrderStatusEnum.NO_PAY);
orderDO = OrderDoConvert.dtoToDo(orderDTO);
// 保存订单主表
boolean save = this.save(orderDO);
if (!save) {
return Result.failed("生成主订单失败");
}
// 处理订单详情数据
OrderDetailDO orderDetailDO = new OrderDetailDO();
orderDetailDO.setCreateUser(1L);
orderDetailDO.setOrderId(orderDO.getId());
orderDetailDO.setGoodsId(orderDTO.getGoodsId());
orderDetailDO.setGoodsNum(orderDTO.getGoodsNum());
orderDetailDO.setGoodsUnitPrice(goodsDTO.getGoodsPrice());
// 保存订单详情
boolean detail = orderDetailService.save(orderDetailDO);
if (!detail) {
return Result.failed("订单详情保存失败");
}
} else {
return Result.failed("商品库存不足");
}
} catch (Exception e) {
log.info("生成订单异常,msg:{}", e);
} finally {
lock.unlock();
}
return Result.success(orderDO);
}
}
3.3 最大并发验证
关于并发这个说法,只能说基于本次环境下的测试,包括后续的优化都是基于此环境,所以大家不要怀疑是否实验结果准确,只能说相对于我的环境,每次测试的结果是准确的。
上面的200并发没有问题,那么我们逐渐加大并发数试下,此处过程省略,我直接给出结论:我在添加到500个并发
时,就会出现如下问题了,在交易服务当中抛出异常:
2022-02-17 15:29:00 INFO http-nio-8005-exec-167 com.wjbgn.trading.service.impl.TradingServiceImpl 支付异常,msg:{}
feign.RetryableException: Read timed out executing GET http://rob-necessities-order/order/info/getById?id=1
描述很清晰,在支付时,调用订单查询接口超时了。
而出现的时机是在订单正在创建的时候,当订单都创建完成后,只有支付业务在跑时,就不会出现此异常了。
我们看看数据的状态:
- 订单总金额:
329316.54
- 支付总金额:
327581.49
- 未支付金额:
1735.05
刚好有两个订单未支付,金额匹配,如下所示:
并且执行500的订单的时间大约是:48
秒。
四、性能优化
在此章节,我们进行一些性能优化,看看能不能够正常的完成500订单的数量,同时将时间有效的缩短。
4.1 mysql优化
本文当中的源码全部使用的单表查询,以java代码处理逻辑,所以不存在关联查询所带来的问题。
另外本文只优化肉眼可见的内容,对于mysql服务的配置,等等,暂时不作为优化点。
优化结果其实不明显,因为sql的性能不是主要的问题。但是流程还是要走的
4.1.1 索引
相信一说到sql的优化,那么大家都想到的索引
,没错,我们接下来也来看看代码当中是否有可以添加索引的位置。
Result listResult = userAccountClient.list(new UserAccountDTO(orderDTO.getUserId()));
在查询方面,除了上面这一出,都是使用主键进行查询的,索引我们在上面的位置,给用户账户表的user_id
创建索引。
未添加索引时,走了全表扫描:
添加索引后:
除了以上一处,我没有发现另外一处,可以建立索引的点了,而且基于目前的数据量,以及前面创建订单处出现的问题,效果似乎不是很明显,只能算是锦上添花。
4.1.2 慢sql查询
比较常见的mysql问题查询,慢sql是一个重点。
我的慢sql已经开启了,可以使用下面的命令查看日志位置和状态:
show variables like 'slow_query%';
未开启的使用下面的命令开启,注意这是临时修改,重启失效,永久需要修改my.ini文件:
set global slow_query_log=on;
另外,需要看下你的慢sql时间阈值,我设置的0.5秒:
show variables like 'long_query_time';
最终结果,没有慢sql,因为都是根据主键查询的,效率很快。
4.2 代码优化
经过mysql的优化,我们发现没有什么可以优化,且造成较大问题的优化点。所以我们从最根本的代码上来找问题。
4.2.1 ReentrantLock优化
4.2.1.1 创建订单
我们在创建订单、支付的代码中使用了ReentrantLock,并且是直接锁住所有的业务代码,此处看看是否可以降低锁的粒度,来获得更大的并发度。
public Result saveOrder(OrderDTO orderDTO) {
OrderDO orderDO = new OrderDO();
// 锁,防止下单数据错误
lock.lock();
try {
// 获取商品
Result info = goodsClient.info(orderDTO.getGoodsId());
// 扣减库存
GoodsDTO goodsDTO = JSONObject.toJavaObject(JSONObject.parseObject(JSONObject.toJSONString(info.getData())), GoodsDTO.class);
if (goodsDTO.getGoodsInventory() > orderDTO.getGoodsNum()) {
goodsDTO.setGoodsInventory(goodsDTO.getGoodsInventory() - orderDTO.getGoodsNum());
Result update = goodsClient.update(goodsDTO);
if (update.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
return Result.failed("扣除商品库存失败");
}
//计算订单总金额
double amount = goodsDTO.getGoodsPrice() * orderDTO.getGoodsNum();
orderDTO.setOrderAmount(amount);
orderDTO.setCreateUser(1L);
orderDTO.setOrderStatus(OrderStatusEnum.NO_PAY);
orderDO = OrderDoConvert.dtoToDo(orderDTO);
// 保存订单主表
boolean save = this.save(orderDO);
if (!save) {
return Result.failed("生成主订单失败");
}
// 处理订单详情数据
OrderDetailDO orderDetailDO = new OrderDetailDO();
orderDetailDO.setCreateUser(1L);
orderDetailDO.setOrderId(orderDO.getId());
orderDetailDO.setGoodsId(orderDTO.getGoodsId());
orderDetailDO.setGoodsNum(orderDTO.getGoodsNum());
orderDetailDO.setGoodsUnitPrice(goodsDTO.getGoodsPrice());
// 保存订单详情
boolean detail = orderDetailService.save(orderDetailDO);
if (!detail) {
return Result.failed("订单详情保存失败");
}
} else {
return Result.failed("商品库存不足");
}
} catch (Exception e) {
log.info("生成订单异常,msg:{}", e);
} finally {
lock.unlock();
}
return Result.success(orderDO);
}
在扣除库存的位置,我们使用的是业务代码计算,之后更新数据库的库存值:
goodsDTO.setGoodsInventory(goodsDTO.getGoodsInventory() - orderDTO.getGoodsNum());
Result update = goodsClient.update(goodsDTO);
如果此处不加锁,那么一定会出现库存问题。整体看下此处代码,会出现问题的似乎只有一处扣减库存的位置,而新增订单和订单详情的时候,不会产生并发问题。所以我们在此处只对扣减库存加锁,降低锁的粒度:
public Result saveOrder(OrderDTO orderDTO) {
GoodsDTO goodsDTO = new GoodsDTO();
// 锁,防止下单数据错误
lock.lock();
try {
// 获取商品
Result info = goodsClient.info(orderDTO.getGoodsId());
// 扣减库存
goodsDTO = JSONObject.toJavaObject(JSONObject.parseObject(JSONObject.toJSONString(info.getData())), GoodsDTO.class);
if (goodsDTO.getGoodsInventory() > orderDTO.getGoodsNum()) {
goodsDTO.setGoodsInventory(goodsDTO.getGoodsInventory() - orderDTO.getGoodsNum());
Result update = goodsClient.update(goodsDTO);
if (update.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
return Result.failed("扣除商品库存失败");
}
} else {
return Result.failed("商品库存不足");
}
} catch (Exception e) {
log.info("生成订单异常,msg:{}", e);
} finally {
lock.unlock();
}
//计算订单总金额
double amount = goodsDTO.getGoodsPrice() * orderDTO.getGoodsNum();
orderDTO.setOrderAmount(amount);
orderDTO.setCreateUser(1L);
orderDTO.setOrderStatus(OrderStatusEnum.NO_PAY);
OrderDO orderDO = OrderDoConvert.dtoToDo(orderDTO);
// 保存订单主表
boolean save = this.save(orderDO);
if (!save) {
return Result.failed("生成主订单失败");
}
// 处理订单详情数据
OrderDetailDO orderDetailDO = new OrderDetailDO();
orderDetailDO.setCreateUser(1L);
orderDetailDO.setOrderId(orderDO.getId());
orderDetailDO.setGoodsId(orderDTO.getGoodsId());
orderDetailDO.setGoodsNum(orderDTO.getGoodsNum());
orderDetailDO.setGoodsUnitPrice(goodsDTO.getGoodsPrice());
// 保存订单详情
boolean detail = orderDetailService.save(orderDetailDO);
if (!detail) {
return Result.failed("订单详情保存失败");
}
return Result.success(orderDO);
}
结果发现,速度快了很多,相比于前面的48秒
,只用了31秒
,但是还是有支付失败的订单。
其中创建订单的过程只用了1秒多
,支付用了25秒
左右,大概消失的5秒,就是在超时等待了:
4.2.1.2 支付订单
那么接下来的重点就是优化支付订单的位置了。在支付的过程中,实际只有扣除用户的账户金额是需要加锁的,而其他的部分都是根据账单的id,更新账单状态:
lock.lock();
try {
// 用户账号扣款
Result listResult = userAccountClient.list(new UserAccountDTO(orderDTO.getUserId()));
if (ObjectUtil.isEmpty(listResult.getData())) {
log.info("未查询到当前用户,支付失败,订单id :{}", tradingDO.getOrderId());
return Result.failed("未查询到当前用户,支付失败");
}
final Double[] userAmount = {0d};
final Long[] userAccountId = {0L};
JSONObject.parseArray(JSONObject.toJSONString(listResult.getData())).forEach(json -> {
JSONObject userAccount = JSONObject.parseObject(JSONObject.toJSONString(json));
userAmount[0] = userAccount.getDouble("userAmount");
userAccountId[0] = userAccount.getLong("id");
});
if (userAmount[0] < orderDTO.getOrderAmount()) {
//修改用户订单状态 - 支付失败
orderClient.update(new OrderDTO(tradingDO.getOrderId(), OrderStatusEnum.PAY_FAILED.toString()));
log.info("订单支付失败,用户余额不足,订单id :{}", tradingDO.getOrderId());
return Result.failed("订单支付失败,用户余额不足");
}
Result userAccount = userAccountClient.update(new UserAccountDTO(userAccountId[0], userAmount[0] - orderDTO.getOrderAmount()));
if (userAccount.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
log.info("用户账户扣款失败,订单id :{}", tradingDO.getOrderId());
return Result.failed("用户账户扣款失败");
}
} catch (Exception e) {
log.info("支付异常,msg:{}", e);
return Result.failed("支付失败");
} finally {
lock.unlock();
}
时间是又一次的大幅度提升了,但是新问题又出现了,未成功支付的订单刚好达到了200
,支付成功的订单使用了7秒
,那么换算一下,全部成功就需要大概12或13秒
吧。总共可能也就需要15秒
左右。相比于前面第一版代码的48秒
,只用了其三分之一。
结论:看来锁的使用,带来性能的损耗是巨大的。
4.2.2 替换ReentrantLock为update语句
那么我们能否直接使用mysql的锁来控制呢,因为在多实例的微服务场景下,如ReentrantLock,Synchronized这种锁也是不适用的。
4.2.2.1 创建订单
我对创建订单进行了修改,将扣减库存的功能变成通过mysql的update语句去实现
@Update("update rob_goods set goods_inventory = goods_inventory - #{num, jdbcType=INTEGER} where id = #{id, jdbcType=BIGINT} and goods_inventory >= #{num, jdbcType=INTEGER}")
int inventoryDeducting(Long id, Integer num);
订单业务代码如下:
@Override
public Result saveOrder(OrderDTO orderDTO) {
GoodsDTO goodsDTO = new GoodsDTO(orderDTO.getGoodsId(), orderDTO.getGoodsNum());
// 扣减库存
Result result = goodsClient.inventoryDeducting(goodsDTO);
if (result.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
return Result.failed("扣除商品库存失败");
}
//获取商品
Result info = goodsClient.info(orderDTO.getGoodsId());
// 扣减库存
goodsDTO = JSONObject.toJavaObject(JSONObject.parseObject(JSONObject.toJSONString(info.getData())), GoodsDTO.class);
//计算订单总金额
double amount = goodsDTO.getGoodsPrice() * orderDTO.getGoodsNum();
orderDTO.setOrderAmount(amount);
orderDTO.setCreateUser(1L);
orderDTO.setOrderStatus(OrderStatusEnum.NO_PAY);
OrderDO orderDO = OrderDoConvert.dtoToDo(orderDTO);
// 保存订单主表
boolean save = this.save(orderDO);
if (!save) {
return Result.failed("生成主订单失败");
}
// 处理订单详情数据
OrderDetailDO orderDetailDO = new OrderDetailDO();
orderDetailDO.setCreateUser(1L);
orderDetailDO.setOrderId(orderDO.getId());
orderDetailDO.setGoodsId(orderDTO.getGoodsId());
orderDetailDO.setGoodsNum(orderDTO.getGoodsNum());
orderDetailDO.setGoodsUnitPrice(goodsDTO.getGoodsPrice());
// 保存订单详情
boolean detail = orderDetailService.save(orderDetailDO);
if (!detail) {
return Result.failed("订单详情保存失败");
}
return Result.success(orderDO);
}
执行代码,结果发现扣减库存有时候会超时导致订单生成失败,这个问题是压力过大,我们后面会专门去解决。
经过上面的修改后,我们发现整体时间在17秒
左右,基本变化不大,下面我们继续去修改支付订单。
4.2.2.1 支付订单
支付订单,同样使用mysql的update语句去更新用户的账户金额。
更新账户sql:
@Update("update rob_user_account set user_amount = user_amount - #{num} where user_id = #{userId} and user_amount >= #{num}")
int accountDeducting(Long userId,Double num);
注意:根据用户id更新,不要忘记了对用户id添加索引, 其实前面优化的时候我们已经添加过了。
支付业务代码:
public Result trading(TradingDO tradingDO) {
// 获取订单信息
Result info = orderClient.info(tradingDO.getOrderId());
if (ObjectUtil.isEmpty(info.getData())) {
log.info("订单不存在,支付失败,订单id :{}", tradingDO.getOrderId());
return Result.failed("订单不存在,支付失败", tradingDO.getOrderId());
}
OrderDTO orderDTO = JSONObject.parseObject(JSONObject.toJSONString(info.getData())).toJavaObject(OrderDTO.class);
//已完成订单不能再次支付
if (orderDTO.getOrderStatus().equals(OrderStatusEnum.FINNISH.getCode())) {
log.info("订单已完成,不能再次支付,订单id :{}", tradingDO.getOrderId());
return Result.failed("订单已完成,不能再次支付");
}
//修改用户订单状态 - 支付中
Result update = orderClient.update(new OrderDTO(tradingDO.getOrderId(), OrderStatusEnum.PAYING.toString()));
if (update.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
log.info("支付失败,修改订单状态失败,订单id :{}", tradingDO.getOrderId());
return Result.failed("支付失败,修改订单状态失败");
}
// 扣减用户账户金额
Result result = userAccountClient.accountDeducting(new UserAccountDTO(orderDTO.getUserId(), orderDTO.getOrderAmount()));
if (result.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
log.info("用户账户扣款失败,订单id :{}", tradingDO.getOrderId());
return Result.failed("用户账户扣款失败");
}
// 生成支付订单
tradingDO.setCreateUser(1L);
tradingDO.setTradingAmount(orderDTO.getOrderAmount());
tradingDO.setTradingStatus(TradingStatusEnum.SUCCESS);
tradingDO.setUserId(orderDTO.getUserId());
boolean save = this.save(tradingDO);
if (!save) {
//修改用户订单状态 - 支付失败
orderClient.update(new OrderDTO(tradingDO.getOrderId(), OrderStatusEnum.PAY_FAILED.toString()));
//TODO 回滚用户的账户金额
}
//修改用户订单状态 - 订单完成
orderClient.update(new OrderDTO(tradingDO.getOrderId(), OrderStatusEnum.FINNISH.toString()));
return Result.success("订单完成");
}
本次的执行时间有了大幅度的提升,全部完成时间大概是7秒
左右。
但是仍然存在订单支付失败的问题,同时存在用户账户扣库存成功,但是支付失败的情况,那是因为我没有做回滚操作导致的。
上面的问题其实是连接超时所导致的,说到底还是业务处理不过来了。
4.2.3 代码逻辑
那么除了上面的优化以外,我们还可以对代码逻辑进行优化。
如下所示:
扣减库存后,又去查询,执行了两次http请求,同时请求了两次数据库,其实我们可以将其变成一次http请求,更新后同时返回商品信息,修改更新库存位置如下所示:
@Override
public Result inventoryDeducting(Long goodsId, Integer num) {
int i = this.baseMapper.inventoryDeducting(goodsId, num);
if (i > 0) {
return Result.success(this.getById(goodsId));
} else {
return Result.failed("库存不足");
}
}
总结来说,尽量减少网络IO,能够提升性能和系统的稳定性。
经过前面的优化过程,500个订单从下单到支付完成,从初版代码的48秒,已经优化为当前的6~7秒,效果还是显而易见的。我们没有使用任何的中间件,如redis,mq。es等等,否则还可以有更多的优化空间,后面我们会逐渐的将他们引入进来。
五、解决问题
从前面的初版代码一路走来,我们还遗留了几个问题,最后来解决一下。
-
订单并发创建时,会有更新库存失败的情况,最终导致下单失败。其实这个问题至少不会造成超卖,订单金额错误的问题,因为订单根本没有创建。
-
订单在进行支付时,回查订单数据,或更新订单状态为支付中时失败,因为订单服务访问量高,导致订单回查失败,最终支付失败。
-
用户扣款成功,但是支付失败。
问题1解决方案: 此问题是由于更新库存失败,报出超时异常,我们可以在此处进行异常捕获,然后重新去创建订单,此处就重试创建一次:
// 扣减库存
Result result = null;
try {
result = goodsClient.inventoryDeducting(goodsDTO);
if (result.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
return Result.failed("扣除商品库存失败");
}
} catch (Exception e) {
log.info("商品库存扣减失败,请重试");
// 临时方案,重试一次
result = goodsClient.inventoryDeducting(goodsDTO);
if (result.getCode() != CommonReturnEnum.SUCCESS.getCode()) {
return Result.failed("扣除商品库存失败");
}
}
如下所示,虽然失败了一次,但是仍然生成了500个订单:
2022-02-18 14:52:50 INFO http-nio-8002-exec-561 com.wjbgn.order.service.impl.OrderServiceImpl 商品库存扣减失败,请重试
问题2解决方案: 此问题与第一个问题是一样的,我们只需要捕获到异常后进行重试就可以了。不同在于支付是必须要成功的,订单已经是存在的,所以我们采用死循环的重试,直到成功为止:
try {
orderDTO = this.updateOrder(tradingDO);
if (ObjectUtil.isNull(orderDTO)) {
log.info("支付失败,修改订单状态失败,订单id :{}", tradingDO.getOrderId());
return Result.failed("支付失败,修改订单状态失败");
}
} catch (Exception e) {
for (; ; ) {
orderDTO = this.updateOrder(tradingDO);
if (ObjectUtil.isNotNull(orderDTO)) {
break;
}
log.info("支付失败,修改订单状态失败,订单id :{}", tradingDO.getOrderId());
}
}
结果很理想,经验证订单全部完成了,总时间仍然维持在6秒
左右。
问题3解决方案: 这个就是比较严重的问题了,金额对不上了。在我们单体服务的时候只需要事务就可以很完美的解决了,那是因为数据库帮你做了这件麻烦事。
在分布式环境下,数据的事务时无效的,我们需要使用分布式事务,如阿里背书的seata
等,使用起来会导致系统整体性能下降,且规模变庞大,我们小型系统,需要尽量去避免分布式事物的出现。当然我们也可以使用RocketMq
实现可靠消息最终一致性
的方案。
可以参考分布式事务专题:juejin.cn/column/7020…
我们这里只能使用手动回滚的方式了,当捕获到支付失败的时候,需要去将用户账户的金额退还;或用户账户金额扣除成功,支付失败,那么需要去重复支付。
六、总结
关于整个微服务系统的测试,优化,以及问题解决就暂时写到这里吧,不知不觉四五千字了。无聊的同学们可以慢慢看看,自己尝试一下,多少会有些帮助的。
预告下后面的内容,此系统会集成一些监控总监,用来监控整体系统的运行状态,毕竟出现问题不能用脑袋猜。引入监控组件可以更好地发现系统的瓶颈在哪里。
欢迎阅读本专题内的文章,专题地址:juejin.cn/column/7053…
本文项目代码gitee地址: gitee.com/wei_rong_xi…