9. Transaction performance optimization-transactional message

Before we talked about a scenario, after verifying the legality of the transaction, we began to deduct inventory, use mq to change the database, and then process the order. But there is a problem that if there is a problem when processing the order afterwards, then the message sent by mq cannot be withdrawn and the data has been changed, then there must be a problem. Based on this situation, we have a simple processing method. Since we use springboot transactions, he provides us with an interface TransactionSynchronizationManager that is executed after the transaction is submitted:

        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                //异步更新库存
                boolean mqResult = itemService.asyncDecreaseStock(itemId , amount);
            }
        });

Of course, there are many rewriting methods to see for yourself.

After the order is completed, the operation of deducting inventory will be carried out, but is this all right? What if there is a problem with mq? This is the transactional message we need to use mq.

Modify the producer code:

@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;
        }
    }
}

 The controller layer directly adjusts this transactionAsyncReduceStock, which generates orders and other operations.

There are a few points that need to be explained here. For the previous producer.send operation, this message is sent out regardless of the three, seventy-one, and the consumer can be notified after it is sent. For transactional messages, there will be a two-phase commit concept. After transactionMQProducer.sendMessageInTransaction, the broker will receive the message, but his state is not a state that can be consumed, but prepare, and the operation will not be executed. In this state, he will execute the executeLocalTransaction method locally, and return Commit or rollback to determine whether the sent message will be executed or rolled back. There is also a status of unknow. For example, it takes more than ten seconds to create an order, then mq must not be returned to return. This status means that the broker will periodically execute the checkLocalTransaction method to query the results, so we have problems when generating orders. Use unknow to let mq query the status by itself. So we need pipeline data.

Improve the following code:

@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;
        }
    }
}

Add a process of querying the pipeline, in the controller layer, init a pipeline before the request, the status is initialization, and then change the pipeline status to complete after the transaction is completed, and change the pipeline status to an error when an error occurs, so in mq When you query yourself, you can decide whether to submit or rollback or continue to wait according to the status.

Of course, in this case, we ensure the final consistency of the database, but if there is a problem with redis, do I superimpose the data in redis or discard it? This is related to the business scenario. Next, for example, there is a need to complete the payment within 15 minutes of placing an order, otherwise it will be invalid. This is also related to the calculation of our pipeline time, and I wo n’t go into details here.

There is also a spike scenario. If there are only 100 products and 10,000 people are going to grab it, then the generation of running water needs to be controlled. Then there is the case of sold out, we can return directly to the controller if it is judged to be sold out. Then we can borrow redis and judge whether it is 0 every time the transaction reduces inventory. If it is 0, make a sold out and put it in redis.

Published 97 original articles · won 28 · 10,000+ views

Guess you like

Origin blog.csdn.net/haozi_rou/article/details/105429774