之前我们说了一个场景,在交易合法性验证后开始扣减库存,用mq来更改数据库,然后进行订单处理。但是这样有个问题就是,如果之后处理订单的时候出问题了,那么之前mq发的消息撤不回来,数据已经改了,那肯定就有问题了。基于这个情况,我们有一个简单的处理方式,由于我们使用了springboot的事务,他给我们提供了一个事务提交以后执行的接口TransactionSynchronizationManager:
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
//异步更新库存
boolean mqResult = itemService.asyncDecreaseStock(itemId , amount);
}
});
当然还有很多重写方法可以自己去看。
在订单完成后,再进行扣减库存的操作,但这样就万无一失了吗?如果mq出问题了怎么办?这是我们就需要使用mq的事务型消息了。
修改下producer的代码:
@Component
public class MqProducer {
private TransactionMQProducer transactionMQProducer;
@Value("${mq.nameserver.addr}")
private String nameAddr;
@Value("${mq.topicname}")
private String topicName;
@Autowired
private OrderService orderService;
@PostConstruct
public void init() throws MQClientException {
transactionMQProducer = new TransactionMQProducer("transaction_producer_group");
transactionMQProducer.setNamesrvAddr(nameAddr);
transactionMQProducer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object args) {
//真正要做的事 创建订单
Integer itemId = (Integer) ((Map)args).get("itemId");
Integer promoId = (Integer) ((Map)args).get("promoId");
Integer userId = (Integer) ((Map)args).get("userId");
Integer amount = (Integer) ((Map)args).get("amount");
try {
orderService.createOrder(userId , itemId , promoId , amount);
} catch (BusinessException e) {
e.printStackTrace();
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.COMMIT_MESSAGE;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
return null;
}
});
transactionMQProducer.start();
}
//事务型同步库存扣减消息
public boolean transactionAsyncReduceStock(Integer userId , Integer promoId , Integer itemId , Integer amount){
Map<String , Object> bodyMap = new HashMap<>();
bodyMap.put("itemId" , itemId);
bodyMap.put("amount" , amount);
Map<String , Object> argsMap = new HashMap<>();
argsMap.put("itemId" , itemId);
argsMap.put("amount" , amount);
argsMap.put("userId" , userId);
argsMap.put("promoId" , promoId);
Message message = new Message(topicName , "increas" ,
JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8")));
TransactionSendResult transactionSendResult = null;
try {
transactionSendResult = transactionMQProducer.sendMessageInTransaction(message , argsMap);
} catch (MQClientException e) {
e.printStackTrace();
return false;
}
if (transactionSendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE){
return true;
}else{
return false;
}
}
}
controller层直接调这个的transactionAsyncReduceStock,由他进行生成订单等操作。
这里有几个点需要解释一下,对于之前producer.send的操作,是不管三七二十一都把这条消息发出去,发出去后消费端就可以得到通知。而对于事务型消息,会有一个二阶段提交的概念,transactionMQProducer.sendMessageInTransaction后broker是会收到消息,但是他的状态并不是可被消费的状态,而是prepare,操作是不会被执行的,他在这个状态下会在本地执行executeLocalTransaction方法,通过它返回Commit或rollback来决定发送的消息执行还是回滚。其中还有一个状态是unknow,例如创建订单花了十几秒钟,那么mq肯定不回收到返回,这个状态表示broker会定期执行checkLocalTransaction方法来询问结果,因此我们在生成订单时出问题,也可以通过unknow来让mq自己查询状态。所以我们就需要流水数据了。
完善下代码:
@Component
public class MqProducer {
private TransactionMQProducer transactionMQProducer;
@Value("${mq.nameserver.addr}")
private String nameAddr;
@Value("${mq.topicname}")
private String topicName;
@Autowired
private OrderService orderService;
@Autowired
private StockLogDOMapper stockLogDOMapper;
@PostConstruct
public void init() throws MQClientException {
transactionMQProducer = new TransactionMQProducer("transaction_producer_group");
transactionMQProducer.setNamesrvAddr(nameAddr);
transactionMQProducer.setTransactionListener(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object args) {
//真正要做的事 创建订单
Integer itemId = (Integer) ((Map)args).get("itemId");
Integer promoId = (Integer) ((Map)args).get("promoId");
Integer userId = (Integer) ((Map)args).get("userId");
Integer amount = (Integer) ((Map)args).get("amount");
String stockLogId = (String) ((Map)args).get("stockLogId");
try {
orderService.createOrder(userId , itemId , promoId , amount , stockLogId);
} catch (BusinessException e) {
e.printStackTrace();
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还是rollback还是unknown
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() == 2){
return LocalTransactionState.COMMIT_MESSAGE;
}else if (stockLogDO.getStatus() == 1){
return LocalTransactionState.UNKNOW;
}else {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
});
transactionMQProducer.start();
}
//事务型同步库存扣减消息
public boolean transactionAsyncReduceStock(Integer userId , Integer promoId , Integer itemId , Integer amount , String stockLogId){
Map<String , Object> bodyMap = new HashMap<>();
bodyMap.put("itemId" , itemId);
bodyMap.put("amount" , amount);
bodyMap.put("stockLogId" , stockLogId);
Map<String , Object> argsMap = new HashMap<>();
argsMap.put("itemId" , itemId);
argsMap.put("amount" , amount);
argsMap.put("userId" , userId);
argsMap.put("promoId" , promoId);
argsMap.put("stockLogId" , stockLogId);
Message message = new Message(topicName , "increas" ,
JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8")));
TransactionSendResult transactionSendResult = null;
try {
transactionSendResult = transactionMQProducer.sendMessageInTransaction(message , argsMap);
} catch (MQClientException e) {
e.printStackTrace();
return false;
}
if (transactionSendResult.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE){
return true;
}else{
return false;
}
}
}
加一个查询流水的过程,在controller层中,在请求前先init一条流水,状态是初始化,然后在交易完成后把流水状态改为完成,在出错的时候把流水状态改成出错,这样在mq自己查询的时候就可以根据状态来决定提交还是回滚还是继续等待了。
当然这种情况下,我们保证了数据库的最终一致性,但是假如redis出问题了,那么redis里面的数据我是叠加到数据库还是抛弃?这就跟业务场景有关了。接下来还有一种例如下单15分钟之内需要完成支付,否则失效,这也跟我们流水时间计算有关,这里就不多赘述了。
还有一个秒杀场景,假如只有100件商品,有1万个人去抢,那么生成流水这块需要注意控制了。再有就是售罄的情况,我们可以在controller中假如判断为售罄,直接返回。那么我们可以借用redis,在每次交易减库存的时候判断是否为0,如果为0就做一个售罄表示放redis中。