SpringBoot电商秒杀项目优化知识点总结(第八章)

8-1 事务型消息(上)

缺点:在createOrder()的订单入库及后续步骤中若发生异常导致回滚 给消息队列发送的消息无法撤回 导致数据库库存显示比实际的少
修改:等事务提交成功后再发送消息 将异步发送消息的语句后移到createOrder()的return前
缺点:@Transactional只有等到方法返回的时候才会commit 如果return失败 依旧会发生相同的问题
修改:将消息放入此种:

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
    @Override
    public void afterCommit() {
    }
});

问题:此时事务完全提交之后才发送消息 但是若发送消息时或者在afterCommit()中发送消息之前失败 则不能回滚
解决:TransactionProducer:换了个producer构造方法 另外开了一个线程 注册了一个transactionListener 投递了sendMessageInTransaction 最后使用注册了一个transactionListener消费

8-2 事务型消息应用(下)

发送事务型消息:二阶段提交
transactionMQProducer.sendMessageInTransaction(message,null);
发送一个prepare状态的消息 broker可以接收到该消息 但consumer不会处理该消息 同时回调executeLocalTransaction()方法 在此方法中执行真正的业务逻辑 根据执行结果的不同返回COMMIT_MESSAGE,ROLLBACK_MESSAGE或UNKNOW 从而决定该事务型消息的状态

若消息一直为UNKNOW或者prepare状态 producer会定期回调checkLocalTransaction方法 可在此处验证下单状态是否为成功 从而重置消息的状态 因此需要引入库存流水

异步同步数据库问题:
(1) 异步消息发送失败
(2)扣减操作执行失败
(3)下单失败无法正确回补库存:需要库存操作流水

操作流水数据类型:
主业务数据:master data
操作型数据:log data (库存流水)

8-3 库存流水状态(1)

DROP TABLE IF EXISTS `stock_log`;
CREATE TABLE `stock_log`  (
  `stock_log_id` varchar(64) NOT NULL,
  `item_id` int(11) NOT NULL DEFAULT 0,
  `amount` int(11) NOT NULL DEFAULT 0,
  `status` int(11) NOT NULL DEFAULT 0 COMMENT '//1表示初始状态 2表示下单扣减库存成功 3表示下单回滚',
  PRIMARY KEY (`stock_log_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;

//在下单之前初始化库存流水 将库存流水的id一步步传入checkLocalTransaction方法中 从而定期检查根据每一条流水的状态设定消息的状态

@Override
@Transactional
public String initStockLog(Integer itemId, Integer amount) {
    StockLogDO stockLogDO = new StockLogDO();
    stockLogDO.setStockLogId(UUID.randomUUID().toString().replace("-",""));
    stockLogDO.setItemId(itemId);
    stockLogDO.setAmount(amount);
    stockLogDO.setStatus(1);

    stockLogDOMapper.insertSelective(stockLogDO);
    return stockLogDO.getStockLogId();
}
//在orderservice的createorder方法中设置库存流水状态为成功 由于该方法有@Transactional注解 所以保持事务的一致性
StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
if (stockLogDO == null) {
    throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
}
stockLogDO.setStatus(2);
stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO);
//修改MqProducer.java
@PostConstruct
public void init() throws MQClientException {
    //mq producer的初始化
    producer = new DefaultMQProducer("producer_group");
    producer.setNamesrvAddr(nameAddr);
    producer.start();

    transactionMQProducer = new TransactionMQProducer("transaction_producer_group");
    transactionMQProducer.setNamesrvAddr(nameAddr);
    transactionMQProducer.start();


    transactionMQProducer.setTransactionListener(new TransactionListener() {
        @Override
        public LocalTransactionState executeLocalTransaction(Message message, Object arg) {
            //真正要做的事 创建订单
            Integer userId = (Integer) ((Map)arg).get("userId");
            Integer itemId = (Integer) ((Map)arg).get("itemId");
            Integer promoId = (Integer) ((Map)arg).get("promoId");
            Integer amount = (Integer) ((Map)arg).get("amount");
            String stockLogId = (String) ((Map)arg).get("stockLogId");

            try {
                orderService.createOrder(userId,itemId,promoId,amount,stockLogId);
            } catch (BusinessException e) {
                e.printStackTrace();
                //设置对应的stockLog为回滚状态
                StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
                stockLogDO.setStatus(3);
                stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO);
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
            return LocalTransactionState.COMMIT_MESSAGE;
        }

        @Override
        public LocalTransactionState checkLocalTransaction(MessageExt msg) {
            //根据扣减库存是否成功 来判断要返回COMMIT_MESSAGE COMMIT_MESSAGE 还是UNKNOW
            String jsonString = new String(msg.getBody());
            Map<String, Object> map = JSON.parseObject(jsonString,Map.class);
            String stockLogId = (String) map.get("stockLogId");
            StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
            if (stockLogDO == null) {
                return LocalTransactionState.UNKNOW;
            }
            if (stockLogDO.getStatus().intValue() == 2) {
                return LocalTransactionState.COMMIT_MESSAGE;
            } else if (stockLogDO.getStatus().intValue() == 1) {
                return LocalTransactionState.UNKNOW;
            }
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    });
}

目前对createorder方法的改造:
1.ItemModel的验证修改为缓存验证
2.UserModel的验证修改为缓存验证
3.校验活动信息修改为内存校验
4.落单减库存修改为缓存修改
5.订单入库必须使用数据库修改
6.商品销量在后续步骤中优化
7.新增了对库存流水数据库表的查找和更新操作:实际上没有减少对数据库性能的优化 之前都在对item表进行操作 都在item表中使用行锁 这里单独在sotck_log表中使用行锁 不需要锁并发的机制 性能消耗小

库存数据库最终一致性保证方案︰
(1)引入库存操作流水
(2)引入事务性消息机制
此时的问题:
(1)redis不可用时如何处理:在保证所有异步消息的同步状态都完成下 可以回源到数据库
(2)扣减流水错误如何处理:让用户下单直接失败或者等待

业务场景决定高可用技术实现
设计原则∶
-宁可少卖,不能超卖
方案∶
(1)redis可以比实际数据库中少 但是若redis产生问题则绝对不能回源到数据库
(2)超时释放:若createorder方法长时间不返回 则不断的产生状态为UNKNOW的订单 但是redis中却一直在减少库存 导致少卖很多 需要设计超时释放的功能 在用户长时间不确定下单 或者下单卡死时 将redis中的额库存加回去

8-7 库存售罄处理方案

此时无论库存为多少 都一定会产生新的库存流水
库存售罄:
库存售罄标识
售罄后不去操作后续流程
售罄后通知各系统售罄来清除缓存
回补上新

8-8 后置流程总结

后置流程:
1.销量逻辑异步化:将item模型的销量字段加一 同样会产生行锁 解决方案一致
2.交易单逻辑异步化:当冻结库存成功之后 直接返回交易成功 并且将所有操作修改为异步化操作
(1)生成交易单sequence后直接异步返回:下单操作可以异步 但支付操作不能异步 只能在支付操作前生成交易订单相关信息
(2)前端轮询异步单状态:本质为假异步化 体验较差 一般不采用异步下单模型 而是异步同步库存模型

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值