秒杀系统 | 交易性能优化 | 库存行锁优化(二)异步消息扣减 MySQL 中的库存

异步消息扣减 MySQL 中的库存

RocketMQ 安装
  • 下载二进制的包,解压;
  • runserver.sh 和 runbroker.sh 两个文件虚拟机参数改小;
  • 启动 Name人Server:nohup sh bin/mqnamesrv &,启动 Broker:nohup sh bin/mqbroker -n localhost:9876 &
  • 验证启动是否成功:export NAMESRV_ADDR=localhost:9876sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producersh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
创建主题
  • ./mqadmin updateTopic -n localhost:9876 -t stock -c DefaultCluster
  • application.properties
mq.nameserver.addr=127.0.0.1:9876
mq.topicname=stock
  • 依赖

    org.apache.rocketmq
    rocketmq-client
    4.3.0

  • MQ 生产者封装
package com.lixinlei.miaosha.mq;

import com.alibaba.fastjson.JSON;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

@Component
public class MqProducer {

    private DefaultMQProducer producer;

    @Value("${mq.nameserver.addr}")
    private String nameAddr;

    @Value("${mq.topicname}")
    private String topicName;

    /**
     * 在 Bean 初始化完成之后调用
     */
    @PostConstruct
    public void init() throws MQClientException {
        producer = new DefaultMQProducer("producer_group");
        producer.setNamesrvAddr(nameAddr);
        producer.start();
    }

    /**
     * 让 MySQL 同步 Redis 中库存扣减的消息
     * @param itemId
     * @param amount
     * @return
     */
    public boolean asyncReduceStock(Integer itemId, Integer amount) {
        Map bodyMap = new HashMap<>();
        bodyMap.put("itemId", itemId);
        bodyMap.put("amount", amount);
        Message message = new Message(
                topicName,
                "increase",
                JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8")));
        try {
            producer.send(message);
        } catch (MQClientException e) {
            e.printStackTrace();
            return false;
        } catch (RemotingException e) {
            e.printStackTrace();
            return false;
        } catch (MQBrokerException e) {
            e.printStackTrace();
            return false;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

}
  • MQ 消费者封装
package com.lixinlei.miaosha.mq;

import com.alibaba.fastjson.JSON;
import com.lixinlei.miaosha.dao.ItemStockDOMapper;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Map;

@Component
public class MqConsumer {

    private DefaultMQPushConsumer consumer;

    @Value("${mq.nameserver.addr}")
    private String nameAddr;

    @Value("${mq.topicname}")
    private String topicName;

    @Autowired
    private ItemStockDOMapper itemStockDOMapper;

    @PostConstruct
    public void init() throws MQClientException {
        consumer = new DefaultMQPushConsumer("stock_consumer_group");
        consumer.setNamesrvAddr(nameAddr);
        consumer.subscribe(topicName, "*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List msgs,
                                                            ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                Message msg = msgs.get(0);
                String jsonString = new String(msg.getBody());
                Map map = JSON.parseObject(jsonString, Map.class);
                Integer itemId = (Integer) map.get("itemId");
                Integer amount = (Integer) map.get("amount");
                itemStockDOMapper.decreaseStock(itemId, amount);
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }

}
  • 扣减库存操作
@Override
@Transactional
public boolean decreaseStock(Integer itemId, Integer amount) {
//        int affectedRows = itemStockDOMapper.decreaseStock(itemId, amount);
    long result = redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount * -1);
    if (result >= 0) {
        boolean mqResult = mqProducer.asyncReduceStock(itemId, amount);
        if (!mqResult) {
            redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount);
            return false;
        }
        return true;
    } else {
        redisTemplate.opsForValue().increment("promo_item_stock_" + itemId, amount);
        return false;
    }
}
异步消息扣减 MySQL 中的库存的问题
  • 异步消息发送失败;
  • 扣减操作执行失败;
  • 下单失败无法正确回补库存,比如用户取消支付;

你可能感兴趣的:(秒杀系统 | 交易性能优化 | 库存行锁优化(二)异步消息扣减 MySQL 中的库存)