之前我們說了一個場景,在交易合法性驗證後開始扣減庫存,用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中。