我們對於交易進行一下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.下單失敗無法正確回補庫存怎麼辦
這些問題後面解決