8.交易方面的優化,redis和rocketmq

我們對於交易進行一下200個線程壓測,看下應用服務器的資源:

再看下壓測結果:

接下來用1000個線程看下數據庫資源:

並且耗時也加大了:

那麼對於下單我們一般會有幾個步驟:1.校驗商品是否存在,用戶是否合法,購買數量是否正確。2.落單減庫存。3.訂單入庫,加商品銷量。4.返回前端。

通過上面步驟,其實我們對於數據庫至少有6次操作,而且在減數據庫的時候是根據id操作,那還有個行鎖,所以性能亟待優化。

交易驗證的優化,可以分爲兩部分:

用戶風控策略優化:策略緩存模型化

活動校驗策略優化:引入活動發佈流程,模型緩存化,緊急下線的能力

 

例如我們把產品信息放到redis中(用戶信息一樣):

    @Override
    public ItemModel getItemByIdInCache(Integer id) {
        ItemModel itemModel = (ItemModel) redisTemplate.opsForValue().get("item_validate_"+id);
        if (itemModel == null){
            itemModel = this.getItemById(id);
            redisTemplate.opsForValue().set("item_validate_"+id , itemModel);
            redisTemplate.expire("item_validate_"+id , 10 , TimeUnit.MINUTES);
        }
        return itemModel;
    }

壓測一下 

區別不太大的原因是服務器帶寬問題,但是還是有提升。

對於緊急下架功能,我們可以開放一個接口,刪除redis即可。

 

對於庫存行鎖的優化:

扣減庫存緩存化

異步同步數據庫

庫存數據庫最終一致性保證

首先如何做到緩存化?一般活動商品都會有一個上架操作,那麼我們可以寫一個接口來進行數據同步:

    @Override
    public void publicPromo(Integer promoId) {
        //通過活動id獲取活動
        PromoDO promoDo = promoDOMapper.selectByPrimaryKey(promoId);
        if (promoDo.getItemId() == null || promoDo.getItemId().intValue() == 0){
            return;
        }
        ItemModel itemModel = itemService.getItemById(promoDo.getItemId());
        //將庫存同步到redis內
        redisTemplate.opsForValue().set("promo_item_stock_" + itemModel.getId() , itemModel.getStock());

    }

然後controller層調用來同步到redis中。

接下來要做減庫存的操作,思路就是直接減redis中的庫存:

    @Override
    @Transactional
    public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException {
        //int affectedRow =  itemStockDOMapper.decreaseStock(itemId,amount);
        long result = redisTemplate.opsForValue().increment("promo_item_stock_" + itemId , amount * -1);
        if(result >= 0){
            //更新庫存成功
            return true;
        }else{
            //更新庫存失敗
            return false;
        }
    }

當然這種情況下在生產環境中不能用,因爲數據庫數據時不一致的,所以需要優化一下:

用異步消息隊列的方式把扣減的消息發送給消息consumer端,由consumer端完成數據庫扣減操作。這裏我們用rocketmq,他是高性能高併發分佈式消息中間件,典型的應用場景是分佈式事務、異步解耦。

安裝rocketmq非常簡單,wget一下解壓一下就好,啓動和測試在官網可以看到http://rocketmq.apache.org/docs/quick-start/

啓動server:nohup sh bin/mqnamesrv &

broker:nohup sh bin/mqbroker -n localhost:9876 &

這裏就不說了,有幾個坑需要注意:

剛下載好的mq啓動需要很大的內存空間,所以需要改一下:

bin/runserver.sh:

 bin/runbroker.sh:

具體大小配置根據電腦實際情況來。

rocketmq還給我們提供了很多命令,可以看下mqadmin:

新建一個topic試一下:./mqadmin updateTopic -n localhost:9876 -t stock -c DefaultCluster

這裏可能會報錯:

需要更改一下tools.sh的內容,可以用命令找一下find / -name '*ext*' |grep jdk:

還有一個坑需要注意,如果配置在服務器上,broker上面的ip可能是內網ip,可以用sh ./mqbroker -m看一下:

需要修改conf/broker.conf:

啓動命令要加上conf:

nohup sh bin/mqbroker -n localhost:9876 -c conf/broker.conf &


接下來代碼實戰下:

    <dependency>
      <groupId>org.apache.rocketmq</groupId>
      <artifactId>rocketmq-client</artifactId>
      <version>4.3.0</version>
    </dependency>
mq.nameserver.addr=47.107.*.*:9876
mq.topicname=stock
@Component
public class MqProducer {
    private DefaultMQProducer producer;
    @Value("${mq.nameserver.addr}")
    private String nameAddr;
    @Value("${mq.topicname}")
    private String topicName;
    @PostConstruct
    public void init() throws MQClientException {
        //mq producer初始化
        producer = new DefaultMQProducer("producer_group");
        producer.setNamesrvAddr(nameAddr);
        producer.start();
    }

    //同步庫存扣減消息
    public boolean asyncReduceStock(Integer itemId , Integer amount){
        Map<String , Object> map = new HashMap<>();
        map.put("itemId" , itemId);
        map.put("amount" , amount);
        Message message = new Message(topicName , "increas" ,
                JSON.toJSON(map).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;
    }
}
@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<MessageExt> msgs, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                //實現庫存真正在數據庫中扣減
                Message msg = msgs.get(0);
                String jsonString = new String(msg.getBody());
                Map<String , Object> 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();
    }
}

業務層: 

    @Autowired
    private MqProducer mqProducer;

    @Override
    @Transactional
    public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException {
        long result = redisTemplate.opsForValue().increment("promo_item_stock_" + itemId , amount * -1);
        if(result >= 0){
            //更新庫存成功
            boolean mqResult = mqProducer.asyncReduceStock(itemId , amount);
            if (!mqResult){
                //失敗回滾redis
                redisTemplate.opsForValue().increment("promo_item_stock_" + itemId , amount);
                return false;
            }
            return true;
        }else{
            //更新庫存失敗
            redisTemplate.opsForValue().increment("promo_item_stock_" + itemId , amount);
            return false;
        }
    }

 這樣就可以實現同步了,但是這樣仍然會存在幾個問題:

1.異步消息發送失敗怎麼辦

2.扣減操作執行失敗怎麼辦

3.下單失敗無法正確回補庫存怎麼辦

這些問題後面解決

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章