交易性能优化
jmeter测压
交易验证完全依赖数据库
库存行锁
后置处理逻辑
1、测试下单接口
现在我将项目都放到了本地虚拟机(3台虚拟机,整体架构为改变)上,所以吞吐量 990/s 。
订单接口代码:
@Override @Transactional public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId, Integer amount) throws BusinessException { //1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确 ItemModel itemModel = itemService.getItemById(itemId); if(itemModel == null){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"商品信息不存在"); } UserModel userModel = userService.getUserById(userId); if(userModel == null){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"用户信息不存在"); } if(amount <= 0 || amount > 99){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"数量信息不正确"); } //校验活动信息 if(promoId != null){ //(1)校验对应活动是否存在这个适用商品 if(promoId.intValue() != itemModel.getPromoModel().getId()){ throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"活动信息不正确"); //(2)校验活动是否正在进行中 }else if(itemModel.getPromoModel().getStatus().intValue() != 2) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"活动信息还未开始"); } } //2.落单减库存 boolean result = itemService.decreaseStock(itemId,amount); if(!result){ throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH); } //3.订单入库 OrderModel orderModel = new OrderModel(); orderModel.setUserId(userId); orderModel.setItemId(itemId); orderModel.setAmount(amount); if(promoId != null){ orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice()); }else{ orderModel.setItemPrice(itemModel.getPrice()); } orderModel.setPromoId(promoId); orderModel.setOrderPrice(orderModel.getItemPrice().multiply(new BigDecimal(amount))); //生成交易流水号,订单号 orderModel.setId(generateOrderNo()); OrderDO orderDO = convertFromOrderModel(orderModel); orderDOMapper.insertSelective(orderDO); //加上商品的销量 itemService.increaseSales(itemId,amount); //4.返回前端 return orderModel; }
2、交易验证优化
用户风控策略优化:策略缓存模型化
验证用户是否合法性,等一系列操作,这里比较简单
活动校验策略优化:引入活动发布流程,模型缓存化,紧急下线能力
1、加入Redis缓存,简单的查询缓存功能
@Override public UserModel getUserByIdInCache(Integer id) { UserModel userModel = (UserModel) RedisUtils.get(RedisConstant.KEY_PRE + "user_validate:" + id); if (null == userModel) { userModel = this.getUserById(id); RedisUtils.set(RedisConstant.KEY_PRE + "user_validate:" + id, userModel); } return userModel; }
@Override public ItemModel getItemByIdInCache(Integer id) { ItemModel itemModel = (ItemModel) RedisUtils.get(RedisConstant.KEY_PRE + "item_validate:" + id); if (null == itemModel) { itemModel = this.getItemById(id); RedisUtils.set(RedisConstant.KEY_PRE + "item_validate:" + id, itemModel); } return itemModel; }
3、库存行锁优化
在商品库存扣减的时候最终会走到 decreaseStock 方法上。将对应的商品减去购买数量,条件参数为 itemId 和 amount
数据库会在 itemId 的地方加上数据库的行锁,加上行锁的条件时:item_id 必须是有索引的,如果没有索引的话,锁定对应的一个表
1、串行扣减库存是有瓶颈。如何优化?
扣减库存缓存化
方案:发布秒杀活动同步库存进缓存
手动调用发布秒杀接口
缓存中就有相应的库存信息
下单交易减缓存库存
浏览器页面下一次单,库存扣减成功(缓存)
到目前为止,这个代码是有问题的:
数据库记录出现不一致的情况,若缓存发生了问题,我们必须要有一个不能影响用户的回源数据库交易,这个时候数据库库存是没有被减去的。
采用异步同步数据库方案:
RabbitMQ
库存数据库最终一致性保证:
使用 RabbitMQ ,保证消息 100% 投递成功下,异步扣减库存。
详细的 RabbitMQ 的环境搭建和项目编码由于太多,就不在这里说明了。
在 createOrder 方法末尾调用 消息投递。保证了在这个方法之前的代码要是发生错误,在事务中还可以回滚之前的数据库操作,如果放在订单入库的前面,万一订单入库操作发生错误,那这条消息投递又成功了,导致商品没有购买成功,但是消费者消费了消息,扣减库存的情况。
生产者:
消费者逻辑:
还有一种事务性消息操作(它会在最近一个事务提交后执行操作,也就是当前方法上的 @Transactional)
问题:
Redis 不可用时如何处理
4、业务场景决定高可用技术实现
在一个分布式高可用的场景下面,是很难保证所有状态都能给用户最佳的体验。因为有最终一致性问题存在,因此在最极端的情况下需要考虑用户的操作 block 还是 等待,还是告诉用户正在处理中,稍后过来查询。因此在一个高可用的技术方案里面应该根据不同的业务场景去确定不同的策略。
1、设计原则:
宁可少买,不能超卖 ——因为如果少买,最多商家的货卖不出去,但是产生超卖的话可能这个商家会产生客诉,发不出货尴尬的情况下,自己倒贴。
但是有又有的电商公司不一样追求极致的销量,宁可超卖,也不少买。因此不同的电商公司不同的活动策略都是不一样的,在设计系统的时候要和产品、运营一起讨论一下高可用模型在极端的情况下会产生什么样的问题。携手定义对应高可用技术的实现才是最靠谱的。
这里我们采用的是 宁可少买,不能超卖。
2、方案:
redis 可以比实际数据库中少
超时释放
5、库存售罄
库存售罄标识 —— 这个标识并非扣减 Redis 失败后得来的,而是通过一种机制标识打到对应的内存上或者缓存上
售罄后不去操作后续流程 —— 如果商品库存已售罄,什么操作也不做了,直接返回前端 下单失败,库存售罄
售罄后通知各系统售罄 —— 售罄之后也需要通知各个系统售罄的流程,因为有可能前端系统或者一个 web 接入的系统对这个商品是否已售罄的状态做一次缓存,因为库存更新并非是实时的,而且上游系统对这个库存包括商品模型做一个缓存。
因此售罄的标识没有办法很快的展示给前端。
当库存售罄之后我们需要通过一个类似于 RabbitMQ 异步消息通知的方式,通知到各个系统由各个系统清除自己的商品信息的缓存。再次回源获取数据库这个商品库存数字的状态。那就可以得到售罄后的状态,自然而然商品就可以打上一个售罄的标识。
回补上新 —— 商家是有对商品库存回补动作的,一旦回补之后自然就要把售罄的标识干掉,不然这个商品的售罄就永远存在。
在商品扣减库存地方进行修改,stock > 0 标识更新库存成功, == 0 标识库存已售罄。
@Override @Transactional(rollbackFor = Exception.class) public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException { // 自增并返回更新后的值 Long stock = RedisUtils.addAndGet(RedisConstant.KEY_PRE + "item_stock:" + itemId, amount * -1); if (stock > 0) { //更新库存成功 return true; } else if (stock == 0) { // 打上库存售罄的标识 // 如果这条信息存在,并且为 true 的话,就表示该商品已售罄 RedisUtils.set(RedisConstant.KEY_PRE + "item_stock_invalid:" + itemId, "true"); //更新库存成功 return true; } else { //更新库存失败 return false; } }
在 controller 层 验证当前商品库存是否已售罄
6、后置流程
1、销量逻辑异步化
以商品(item)销量字段 +1 ,因此这个销量字段 +1 也会跟库存面临一个 item_id 销量级别的行锁。可以将销量 +1 异步化操作。
2、交易单逻辑异步化
当冻结库存成功之后,直接返回交易成功,并且将生成订单和对应的一些销量等等的一些操作全部做成异步化操作。因此我们这种操作方式是将交易单逻辑异步化处理。
3、生成交易单 sequence 后直接异步返回
对应库存完成冻结之后,任然需要有一步生成交易订单号的状态,因为前端下单操作可以异步,但是支付操作无法异步。必须先有交易单,才能有支付相关的一些信息,才能进行支付。
因此对应生成交易单的时候必须返回交易单号,用来前端使用轮询查询这个交易单号是否有生成。生成之后才能做支付操作。
其实可以看到,这种方式的异步下单其实是一个假的异步下单。让用户提前感知到下单成功,前端采用轮询查询订单。这种体验其实是非常的差得,因此把这个交易方式叫做:假异步化。因此不太会采用异步下单模型。
而是会采用异步同步库存模型。
4、前端轮询查询异步订单状态