性能瓶頸
分析下單時,應用的模型如下:
訪問數據庫的次數如下:(訪問6次數據庫;是否能對訪問數據庫的次數進行一定程序的優化,採用緩存的方式)
性能優化
交易驗證優化(緩存用戶信息,)
原來對用戶,商品進行校驗的方式:都需要訪問數據庫
1.校驗下單狀態,下單的商品是否存在,用戶是否合法,購買數量是否正確
ItemModel itemModel = itemService.getItemById(itemId);
if(itemModel == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"商品信息不存在");
}
可以將商品的詳情以及用戶的詳情存入到redis緩存中,之後用戶再次下單的時候可以從緩存中取出數據
問題是:這裏的商品信息若在之後用戶下過單之後,可能進行更改就存在一個
redis
緩存與數據庫
緩存不一致的情況。
代碼
應用
//使用緩存校驗itemModel
ItemModel itemModel = itemService.getItemByIdInCache(itemId);
if(itemModel == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"商品信息不存在");
}
UserModel userModel = userService.getUserByIdInCache(userId);
if(userModel == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"用戶信息不存在");
}
ItemServiceImpl
@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;
}
UserServiceImpl
@Override
public UserModel getUserByIdInCache(Integer id) {
UserModel userModel = (UserModel)redisTemplate.opsForValue().get("user_validate_"+id);
if(userModel==null){
userModel=this.getUserById(id);
redisTemplate.opsForValue().set("user_validate_"+id,userModel);
redisTemplate.expire("user_validate_"+id,10, TimeUnit.MINUTES);
}
return userModel;
}
扣減庫存優化
在訪問數據庫進行庫存扣減的問題時,由於數據庫行鎖的存在,可能存在數據庫瓶頸的問題
可以將數據庫中的stock緩存到緩存中,庫存緩存化,之後再採用消息隊列將其異步執行更新mysql數據庫中的內容;
庫存緩存化
- 活動發佈同步庫存進緩存
- 下單交易減緩存庫存
代碼
活動發佈同步庫存進緩存
/活動函發佈
void publishPromo(Integer promoId);
@Override
public void publishPromo(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());
}
交易下單減掉緩存庫存
//2.落單減庫存
boolean result = itemService.decreaseStock(itemId,amount);
//減去緩存中的庫存
long result = redisTemplate.opsForValue().increment("promo_item_stock_"+itemId,amount.intValue()*-1);
異步同步數據庫–採用消息隊列
異步消息扣減數據庫內存庫
代碼
生產者
package com.imooc.miaoshaproject.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;
/**
* Created by hzllb on 2019/2/23.
*/
@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> 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;
}
}
消息消費者:數據庫中庫存的扣減操作
package com.imooc.miaoshaproject.mq;
import com.alibaba.fastjson.JSON;
import com.imooc.miaoshaproject.dao.ItemDOMapper;
import com.imooc.miaoshaproject.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;
/**
* Created by hzllb on 2019/2/23.
*/
@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 context) {
// //實現庫存真正到數據庫內扣減的邏輯
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();
}
}
異步同步數據庫執行失敗–採取事務
事務型信息 將下單整個邏輯
防止商品售罄–在緩存中加入一個標誌
在緩存中加入一個標誌,進行判斷:
當緩存中的庫存小於0時,則在緩存中設置一個標誌,每次用戶下單的時候都需要先判斷這個標誌是否存在,若標誌存在,則說明沒有庫存,直接返回不存在
//防止商品售罄
//判斷商品是否已經售罄,若對應的售罄key存在則直接返回下單失敗
if(redisTemplate.hasKey("promo_item_stock_invalid_"+itemId)){
throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
}
隊列異步性事務消息(當全部操作成功,之前發送到隊列中的消息才能被消費,在數據庫中扣減庫存才能成功)
mq異步事務
if(!mqProducer.transactionAsyncReduceStock(userModel.getId(),promoId,itemId,amount,stockLogId)){
throw new BusinessException(EmBusinessError.UNKNOWN_ERROR,"下單失敗");
}
mqproducer
//事務型異步庫存扣減消息
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,"increase" ,
JSON.toJSON(bodyMap).toString().getBytes(Charset.forName("UTF-8")));
TransactionSendResult transactionSendResult=null;
try {
//發送的是事務型消息
//發送完成之後事務處於prepare狀態
//1、往消息隊列中投遞prepare消息,維護在message broker的中間件上
//2、在本地執行localTRranscatopn
transactionSendResult = transactionMQProducer.sendMessageInTransaction(message,argsMap);
} catch (MQClientException e) {
e.printStackTrace();
return false;
}
if(transactionSendResult.getLocalTransactionState()==LocalTransactionState.ROLLBACK_MESSAGE){
return false;
}else if(transactionSendResult.getLocalTransactionState()==LocalTransactionState.COMMIT_MESSAGE){
return true;
}else{
return false;
}
}
@PostConstruct
public void init() throws MQClientException {
//做mq producer的初始化
producer = new DefaultMQProducer("producer_group");
producer.setNamesrvAddr(nameAddr);
producer.start();
transactionMQProducer = new TransactionMQProducer("tran_producer_group");
transactionMQProducer.setNamesrvAddr(nameAddr);
transactionMQProducer.start();
transactionMQProducer.setTransactionListener(new TransactionListener() {
/**
* COMMIT_MESSAGE,
ROLLBACK_MESSAGE, 將之前prepare消息發送
UNKNOW;
* @param message
* @param arg
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object arg) {
//真正要做的事,創建訂單
Integer itemId = (Integer) ((Map)arg).get("itemId");
Integer promoId = (Integer) ((Map)arg).get("promoId");
Integer userId = (Integer) ((Map)arg).get("userId");
Integer amount = (Integer) ((Map)arg).get("amount");
String stockLogId = (String) ((Map)arg).get("stockLogId");
try {
//在這裏真正調用createOrder方法,完成訂單的創建以及對應redis
orderService.createOrder(userId, itemId,promoId,amount,stockLogId);
} catch (BusinessException e) {
e.printStackTrace();
//發生異常,事務回滾
return LocalTransactionState.ROLLBACK_MESSAGE;
}
//只有當COMMIT完成之後,才發生後消息的回調
return LocalTransactionState.COMMIT_MESSAGE;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
//根據是否扣減庫存成功來判斷要返回COMMIT,ROLLBACK還是繼續UNKWON
//實現庫存真正到數據庫內扣減的邏輯
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");
//查詢流水
String stockLogId = (String) map.get("stockLogId");
StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
if(stockLogDO==null) return LocalTransactionState.UNKNOW;
if(stockLogDO.getStatus().intValue()==2){
return LocalTransactionState.COMMIT_MESSAGE;
}else if(stockLogDO.getStatus().intValue()==1){
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}
});
}
創建訂單creatOrder
@Override
@Transactional
public OrderModel createOrder(Integer userId, Integer itemId, Integer promoId, Integer amount,String stockLogId) throws BusinessException {
// 1.校驗下單狀態,下單的商品是否存在,用戶是否合法,購買數量是否正確
// ItemModel itemModel = itemService.getItemById(itemId);
// if(itemModel == null){
// throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"商品信息不存在");
// }
//使用緩存校驗itemModel
ItemModel itemModel = itemService.getItemByIdInCache(itemId);
if(itemModel == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"商品信息不存在");
}
//緩存
UserModel userModel = userService.getUserByIdInCache(userId);
if(userModel == null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"用戶信息不存在");
}
if(amount <= 0 || amount > 99){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"數量信息不正確");
}
//校驗活動信息
if(promoId != null){
//(1)校驗對應活動是否存在這個適用商品
if(promoId.intValue() != itemModel.getPromoModel().getId()){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"活動信息不正確");
//(2)校驗活動是否正在進行中
}else if(itemModel.getPromoModel().getStatus().intValue() != 2) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"活動信息還未開始");
}
}
//2.落單減庫存:這裏扣減的是緩存中的庫存(只有當這些操作全部成功時,事務型的消息纔會被消費者銷燬)
boolean result = itemService.decreaseStock(itemId,amount);
if(!result){
throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
}
//3.訂單入庫:可以將其插入刀片
OrderModel orderModel = new OrderModel();
orderModel.setUserId(userId);
orderModel.setItemId(itemId);
orderModel.setAmount(amount);
if(promoId != null){
orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice());
}else{
orderModel.setItemPrice(itemModel.getPrice());
}
orderModel.setPromoId(promoId);
orderModel.setOrderPrice(orderModel.getItemPrice().multiply(new BigDecimal(amount)));
//生成交易流水號,訂單號
//這裏都可以進行改進,將其發送到隊列中,
orderModel.setId(generateOrderNo());
OrderDO orderDO = convertFromOrderModel(orderModel);
orderDOMapper.insertSelective(orderDO);
//加上商品的銷量
itemService.increaseSales(itemId,amount);
//設置庫存流水狀態位成功
StockLogDO stockLogDO = stockLogDOMapper.selectByPrimaryKey(stockLogId);
if(stockLogDO==null){
throw new BusinessException(EmBusinessError.UNKNOWN_ERROR);
}else{
stockLogDO.setStatus(2);
stockLogDOMapper.updateByPrimaryKeySelective(stockLogDO);
}
//4.返回前端
return orderModel;
}