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)前端轮询异步单状态:本质为假异步化 体验较差 一般不采用异步下单模型 而是异步同步库存模型