秒殺專案06-介面優化
- 上一部分回顧
- 超賣問題
- 思路
- 1. Redis預減庫存減少資料庫訪問
- 2. 記憶體標記減少Redis訪問
- 3. 請求先入隊緩沖,異步下單,增強用戶體驗
- 4. RabbitMQ安裝與Spring Boot集成
- 4.1 RabbitMQ安裝
- Erlang與RabbitMQ版本對應關系
- 安裝Erlang
- RabbitMQ下載
- 上傳到服務器上
- 開始安裝erlang
- 開始安裝RabbitMQ
- 4.2 SpringBoot集成RabbitMQ上
- 1. 添加依賴spring-boot-starter-amqp
- 2. 添加組態檔
- 允許guest用戶被遠程訪問
- 3. 創建訊息接收者
- 4. 創建訊息發送者
- 啟動應用,訪問介面
- 4.3 SpringBoot集成RabbitMQ下-四種交換機(exchange)
- Direct Exchange
- Fanout Exchange
- Topic Exchange
- Headers Exchange
- 測驗
- 5. Nginx水平擴展
- 6. 壓測
上一部分回顧
超賣問題
- 資料庫加唯一索引: 防止用戶重復購買
- SQL加庫存數量判斷: 防止庫存變成負數
思路
- 系統初始化,把商品庫存數量加載到Redis
- 收到請求,Redis預減庫存,庫存不足,直接回傳,否則進入3
- 請求入隊,立即回傳排隊中
- 請求出隊,生成訂單,減少庫存
- 客戶端輪詢,是否秒殺成功
1. Redis預減庫存減少資料庫訪問
程式初始化時就把每一個秒殺商品的庫存資訊放到redis中,當有秒殺請求時,我們先通過redis中的庫存資訊進行預減庫存,當redis中的庫存<0時,就直接回傳了,后面的請求對服務器的壓力就很小了,
2. 記憶體標記減少Redis訪問
通過一個Map來存盤一個秒殺商品和秒殺商品是否秒殺結束的映射,當redis快取中的對應的商品庫存資訊小于0時,我們就對map進行添加映射,來表示該商品已經秒殺結束,當后面的請求過來時,直接從map中取商品是否秒殺結束的資訊,秒殺結束直接回傳,這樣對服務器的壓力就更小了,
3. 請求先入隊緩沖,異步下單,增強用戶體驗
通過RabbitMQ訊息佇列進行異步下單,對用戶的秒殺請求進行入隊緩沖,
MiaoshaController.java
@Autowired
private GoodsService goodsService;
@Autowired
private RedisService redisService;
@Autowired
private OrderService orderService;
@Autowired
private MiaoshaService miaoshaService;
@Autowired
private MQSender sender;
private Map<Long, Boolean> localOverMap = new HashMap<>();
@RequestMapping(value = "/do_miaosha", method= RequestMethod.POST)
@ResponseBody
public Result<Integer> doMiaosha(Model model, MiaoshaUser user, @RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if (user == null) {
return Result.fail(CodeMsg.SESSION_ERROR);
}
//記憶體標記,減少redis訪問
boolean over = localOverMap.get(goodsId);
if(over) {
return Result.fail(CodeMsg.MIAO_SHA_OVER);
}
//預減庫存
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10
if(stock < 0) {
localOverMap.put(goodsId, true);
return Result.fail(CodeMsg.MIAO_SHA_OVER);
}
//判斷是否已經秒殺到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
return Result.fail(CodeMsg.REPEAT_MIAOSHA);
}
//入隊
MiaoshaMessage mm = new MiaoshaMessage();
mm.setUser(user);
mm.setGoodsId(goodsId);
sender.sendMiaoshaMessage(mm);
return Result.success(0);//排隊中
/*// 判斷庫存
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
if (stock <= 0) {
return Result.fail(CodeMsg.MIAO_SHA_OVER);
}
// 判斷是否已經秒殺到了
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if (order != null) {
return Result.fail(CodeMsg.REPEAT_MIAOSHA);
}
// 減庫存 下訂單 寫入秒殺訂單
OrderInfo orderInfo = miaoshaService.miaosha(user, goods);
return Result.success(orderInfo);*/
}
/**
* Controller實作了InitializingBean,系統初始化會呼叫該方法
**/
@Override
public void afterPropertiesSet() throws Exception {
List<GoodsVo> goodsList = goodsService.listGoodsVo();
if(goodsList == null) {
return;
}
for(GoodsVo goods : goodsList) {
redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount());
localOverMap.put(goods.getId(), false);
}
}
/**
* orderId:成功
* -1:秒殺失敗
* 0: 排隊中
*
* 該請求獲取秒殺結果資訊
**/
@RequestMapping(value="/result", method=RequestMethod.GET)
@ResponseBody
public Result<Long> miaoshaResult(Model model,MiaoshaUser user,@RequestParam("goodsId")long goodsId) {
model.addAttribute("user", user);
if(user == null) {
return Result.fail(CodeMsg.SESSION_ERROR);
}
long result =miaoshaService.getMiaoshaResult(user.getId(), goodsId);
return Result.success(result);
}
MQSender.java
@Service
public class MQSender {
private static final Logger log = LoggerFactory.getLogger(MQSender.class);
@Autowired
private AmqpTemplate amqpTemplate;
public void sendMiaoshaMessage(MiaoshaMessage mm) {
String msg = RedisService.beanToString(mm);
log.info("send message: " + msg);
amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}
}
MQReceiver.java
@Service
public class MQReceiver {
private static Logger log = LoggerFactory.getLogger(MQReceiver.class);
@Autowired
private RedisService redisService;
@Autowired
private GoodsService goodsService;
@Autowired
private OrderService orderService;
@Autowired
private MiaoshaService miaoshaService;
@RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)
public void receive(String message) {
log.info("receive miaoshamessage: " + message);
MiaoshaMessage mm = redisService.stringToBean(message, MiaoshaMessage.class);
MiaoshaUser user = mm.getUser();
long goodsId = mm.getGoodsId();
GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
int stock = goods.getStockCount();
if(stock <= 0) {
return;
}
//判斷是否已經秒殺到了
/*MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if(order != null) {
return;
}*/
//減庫存 下訂單 寫入秒殺訂單
miaoshaService.miaosha(user, goods);
}
}
MQConfig.java
@Configuration
public class MQConfig {
public static final String MIAOSHA_QUEUE = "miaosha.queue";
/**
* Direct模式 交換機Exchange
* */
@Bean
public Queue queue() {
//return new Queue(QUEUE, true);
return new Queue(MIAOSHA_QUEUE, true);
}
}
MiaoshaService.java
@Service
public class MiaoshaService {
@Autowired
private GoodsService goodsService;
@Autowired
private OrderService orderService;
@Autowired
private RedisService redisService;
@Transactional
public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
// 減庫存 下訂單 寫入秒殺訂單
boolean success = goodsService.reduceStock(goods);
if (success) {
// order_info miaosha_order
return orderService.createOrder(user, goods);
}
setGoodsOver(goods.getId());
return null;
}
private void setGoodsOver(Long goodsId) {
redisService.set(MiaoshaKey.isGoodsOver, ""+goodsId, true);
}
private boolean getGoodsOver(long goodsId) {
return redisService.exists(MiaoshaKey.isGoodsOver, ""+goodsId);
}
public long getMiaoshaResult(Long userId, long goodsId) {
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
if(order != null) {//秒殺成功
return order.getOrderId();
}else {
boolean isOver = getGoodsOver(goodsId);
if(isOver) {
return -1;
}else {
return 0;
}
}
}
}
OrderService.java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RedisService redisService;
public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
//return orderMapper.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
return redisService.get(OrderKey.getMiaoshaOrderByUidGid, ""+userId+"_"+goodsId, MiaoshaOrder.class);
}
@Transactional
public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) {
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCreateDate(new Date());
orderInfo.setDeliveryAddrId(0L);
orderInfo.setGoodsCount(1);
orderInfo.setGoodsId(goods.getId());
orderInfo.setGoodsName(goods.getGoodsName());
orderInfo.setGoodsPrice(goods.getMiaoshaPrice());
orderInfo.setOrderChannel(1);
orderInfo.setStatus(1);
orderInfo.setStatus(0);
orderInfo.setUserId(user.getId());
//insert方法會把自增的主鍵設定到物件屬性中
orderMapper.insert(orderInfo);
MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
miaoshaOrder.setGoodsId(goods.getId());
miaoshaOrder.setOrderId(orderInfo.getId());
miaoshaOrder.setUserId(user.getId());
orderMapper.insertMiaoshaOrder(miaoshaOrder);
redisService.set(OrderKey.getMiaoshaOrderByUidGid, ""+user.getId()+"_"+goods.getId(), miaoshaOrder);
return orderInfo;
}
public OrderInfo getOrderById(long orderId) {
return orderMapper.selectById(orderId);
}
}
4. RabbitMQ安裝與Spring Boot集成
4.1 RabbitMQ安裝
Erlang與RabbitMQ版本對應關系
官方網址
安裝Erlang
下載地址
RabbitMQ下載
下載地址
上傳到服務器上

開始安裝erlang
- 添加依賴
yum -y install ncurses-devel
- 解壓
tar -xzvf otp_src_xxx.tar.gz
- 對erlang進行配置
./configure --prefix=/usr/local/erlang --without-javac
- 編譯安裝erlang
make & make install
- 將erlang中命令添加到環境變數并重繪配置
echo 'export PATH=$PATH:/usr/local/erlang/bin' >> /etc/profile
source /etc/profile
開始安裝RabbitMQ
- 安裝依賴
yum -y install xz
yum -y install python
yum -y install xmlto
yum -y install python-simplejson
- 解壓
xz -d rabbitmqxxxx
tar -xvzf rabbitmqxxxx
- 啟動rabbitmq并查看啟動日志
# 后臺啟動
./rabbitmq-server -detached
tail -f /usr/local/rabbitmq//var/log/rabbitmq/rabbit@hmx.log

4. 配置rabbitmq環境變數
echo 'export PATH=$PATH:/usr/local/rabbitmq/sbin' >> /etc/profile
- 查看rabbitmq啟動狀態
netstat -nap | grep 5672

4.2 SpringBoot集成RabbitMQ上
1. 添加依賴spring-boot-starter-amqp
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.5.2</version>
</dependency>
2. 添加組態檔
#rabbitmq
spring.rabbitmq.host=192.168.174.128
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
# 消費者數量
spring.rabbitmq.listener.simple.concurrency= 10
# 消費者最大數量
spring.rabbitmq.listener.simple.max-concurrency= 10
# 每個消費者可以未確認的最大未確認訊息數,
spring.rabbitmq.listener.simple.prefetch= 1
# 是否在啟動時自動啟動容器,
spring.rabbitmq.listener.simple.auto-startup=true
# 默認情況下是否重新排隊被拒絕的交貨,
spring.rabbitmq.listener.simple.default-requeue-rejected= true
# 是否啟用發布重試,
spring.rabbitmq.template.retry.enabled=true
# 第一次和第二次嘗試傳遞訊息之間的持續時間,
spring.rabbitmq.template.retry.initial-interval=1000
# 最大重試次數
spring.rabbitmq.template.retry.max-attempts=3
# 最大重試間隔
spring.rabbitmq.template.retry.max-interval=10000
# 應用于前一個重試間隔的乘數,
spring.rabbitmq.template.retry.multiplier=1.0
允許guest用戶被遠程訪問
- 在/rabbitmq/etc/rabbitmq/目錄下創建rabbitmq.conf組態檔,并添加如下內容
loopback_users = none
- 重啟rabbitmq
# 停止rabbitmq
rabbitmqctl stop
# 啟動rabbitmq
rabbitmq-server
3. 創建訊息接收者
MQConfig.java
@Configuration
public class MQConfig {
public static final String QUEUE = "queue";
@Bean
public Queue queue() {
return new Queue(QUEUE,true);
}
}
MQReceiver.java
@Service
public class MQReceiver {
private static Logger log = LoggerFactory.getLogger(MQReceiver.class);
@RabbitListener(queues = MQConfig.QUEUE)
public void receive(String message) {
log.info("receive message: " + message);
}
}
4. 創建訊息發送者
將RedisService中的beanToString和stringToBean改成靜態方法
MQSender.java
@Service
public class MQSender {
private static Logger log = LoggerFactory.getLogger(MQSender.class);
@Autowired
private AmqpTemplate amqpTemplate;
public void send(Object message) {
String msg = RedisService.beanToString(message);
log.info("send message: " + msg);
amqpTemplate.convertAndSend(MQConfig.QUEUE, msg);
}
}
DemoController.java
@Controller
@RequestMapping("/demo")
public class DemoController {
@Autowired
private UserService userService;
@Autowired
private RedisService redisService;
@Autowired
MQSender sender;
@RequestMapping("/mq")
@ResponseBody
public Result<String> mq() {
sender.send("Hello, World");
return Result.success("Hello, World");
}
}
啟動應用,訪問介面


4.3 SpringBoot集成RabbitMQ下-四種交換機(exchange)
Direct Exchange
處理路由鍵,需要將一個佇列系結到交換機上,要求該訊息與一個特定的路由鍵完全匹配,這是一個完整的匹配,如果一個佇列系結到該交換機上要求路由鍵 “abc”,則只有被標記為“abc”的訊息才被轉發,不會轉發abc.def,也不會轉發dog.ghi,只會轉發abc,

Fanout Exchange
不處理路由鍵,你只需要簡單的將佇列系結到交換機上,一個發送到交換機的訊息都會被轉發到與該交換機系結的所有佇列上,很像子網廣播,每臺子網內的主機都獲得了一份復制的訊息,Fanout交換機轉發訊息是最快的,

Topic Exchange
將路由鍵和某模式進行匹配,此時佇列需要系結要一個模式上,符號“#”匹配一個或多個詞,符號“*”匹配不多不少一個詞,因此“abc.#”能夠匹配到“abc.def.ghi”,但是“abc.*” 只會匹配到“abc.def”,

Headers Exchange
不處理路由鍵,而是根據發送的訊息內容中的headers屬性進行匹配,在系結Queue與Exchange時指定一組鍵值對;當訊息發送到RabbitMQ時會取到該訊息的headers與Exchange系結時指定的鍵值對進行匹配;如果完全匹配則訊息會路由到該佇列,否則不會路由到該佇列,headers屬性是一個鍵值對,可以是Hashtable,鍵值對的值可以是任何型別,而fanout,direct,topic 的路由鍵都需要要字串形式的,
匹配規則x-match有下列兩種型別:
x-match = all :表示所有的鍵值對都匹配才能接受到訊息
x-match = any :表示只要有鍵值對匹配就能接受到訊息
MQConfig.java
@Configuration
public class MQConfig {
public static final String MIAOSHA_QUEUE = "miaosha.queue";
public static final String QUEUE = "queue";
public static final String TOPIC_QUEUE1 = "topic.queue1";
public static final String TOPIC_QUEUE2 = "topic.queue2";
public static final String HEADER_QUEUE = "header.queue";
public static final String TOPIC_EXCHANGE = "topicExchage";
public static final String FANOUT_EXCHANGE = "fanoutxchage";
public static final String HEADERS_EXCHANGE = "headersExchage";
/**
* Direct模式 交換機Exchange
* */
@Bean
public Queue queue() {
return new Queue(QUEUE, true);
}
/**
* Topic模式 交換機Exchange
* */
@Bean
public Queue topicQueue1() {
return new Queue(TOPIC_QUEUE1, true);
}
@Bean
public Queue topicQueue2() {
return new Queue(TOPIC_QUEUE2, true);
}
@Bean
public TopicExchange topicExchage(){
return new TopicExchange(TOPIC_EXCHANGE);
}
@Bean
public Binding topicBinding1() {
return BindingBuilder.bind(topicQueue1()).to(topicExchage()).with("topic.key1");
}
@Bean
public Binding topicBinding2() {
return BindingBuilder.bind(topicQueue2()).to(topicExchage()).with("topic.#");
}
/**
* Fanout模式 交換機Exchange
* */
@Bean
public FanoutExchange fanoutExchage(){
return new FanoutExchange(FANOUT_EXCHANGE);
}
@Bean
public Binding FanoutBinding1() {
return BindingBuilder.bind(topicQueue1()).to(fanoutExchage());
}
@Bean
public Binding FanoutBinding2() {
return BindingBuilder.bind(topicQueue2()).to(fanoutExchage());
}
/**
* Header模式 交換機Exchange
* */
@Bean
public HeadersExchange headersExchage(){
return new HeadersExchange(HEADERS_EXCHANGE);
}
@Bean
public Queue headerQueue1() {
return new Queue(HEADER_QUEUE, true);
}
@Bean
public Binding headerBinding() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("header1", "value1");
map.put("header2", "value2");
return BindingBuilder.bind(headerQueue1()).to(headersExchage()).whereAll(map).match();
}
}
MQSender.java
@Service
public class MQSender {
private static Logger log = LoggerFactory.getLogger(MQSender.class);
@Autowired
private AmqpTemplate amqpTemplate;
public void send(Object message) {
String msg = RedisService.beanToString(message);
log.info("send message: " + msg);
amqpTemplate.convertAndSend(MQConfig.QUEUE, msg);
}
public void sendTopic(Object message) {
String msg = RedisService.beanToString(message);
log.info("send topic message:"+msg);
amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key1", msg+"1");
amqpTemplate.convertAndSend(MQConfig.TOPIC_EXCHANGE, "topic.key2", msg+"2");
}
public void sendFanout(Object message) {
String msg = RedisService.beanToString(message);
log.info("send fanout message:"+msg);
amqpTemplate.convertAndSend(MQConfig.FANOUT_EXCHANGE, "", msg);
}
public void sendHeader(Object message) {
String msg = RedisService.beanToString(message);
log.info("send fanout message:"+msg);
MessageProperties properties = new MessageProperties();
properties.setHeader("header1", "value1");
properties.setHeader("header2", "value2");
Message obj = new Message(msg.getBytes(), properties);
amqpTemplate.convertAndSend(MQConfig.HEADERS_EXCHANGE, "", obj);
}
}
MQReceiver.java
@Service
public class MQReceiver {
private static Logger log = LoggerFactory.getLogger(MQReceiver.class);
@RabbitListener(queues = MQConfig.QUEUE)
public void receive(String message) {
log.info("receive message: " + message);
}
@RabbitListener(queues=MQConfig.TOPIC_QUEUE1)
public void receiveTopic1(String message) {
log.info(" topic queue1 message:"+message);
}
@RabbitListener(queues=MQConfig.TOPIC_QUEUE2)
public void receiveTopic2(String message) {
log.info(" topic queue2 message:"+message);
}
@RabbitListener(queues=MQConfig.HEADER_QUEUE)
public void receiveHeaderQueue(byte[] message) {
log.info(" header queue message:"+new String(message));
}
}
DemoController.java
@Controller
@RequestMapping("/demo")
public class DemoController {
@Autowired
private UserService userService;
@Autowired
private RedisService redisService;
@Autowired
private GoodsService goodsService;
@Autowired
MQSender sender;
@RequestMapping("/mq/header")
@ResponseBody
public Result<String> mqHeaders() {
sender.sendHeader("Hello, World");
return Result.success("Hello, World");
}
@RequestMapping("/mq/fanout")
@ResponseBody
public Result<String> mqFanout() {
sender.sendFanout("Hello, World");
return Result.success("Hello, World");
}
@RequestMapping("/mq/topic")
@ResponseBody
public Result<String> mqTopic() {
sender.sendTopic("Hello, World");
return Result.success("Hello, World");
}
@RequestMapping("/mq")
@ResponseBody
public Result<String> mq() {
sender.send("Hello, World");
return Result.success("Hello, World");
}
}
測驗
- Direct Exchange


- Topic Exchange


- Fanout Exchange


- Headers Exchange


5. Nginx水平擴展
可以將專案的jar包放到多臺器上運行,配置一下反向代理的路徑,負載均衡,達到高可用的效果,
6. 壓測
將專案更新后的新的jar包放到服務器上運行,然后進行壓測,在使用這么多的優化手段后,可以感覺到qps高了很多,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/335485.html
標籤:其他
下一篇:大資料之hive安裝
