秒殺業務場景併發量很大,瓶頸在數據庫,怎麼解決?可以加緩存。用戶在發起請求時,從瀏覽器開始,在瀏覽器上做頁面靜態化直接將頁面緩存到用戶的瀏覽器端,然後請求到達網站之前可以部署CDN節點,讓請求先訪問CDN,到達網站的時候使用頁面緩存。頁面緩存再進一步,粒度再細一點的話就是對象緩存,緩存層依次請求完之後,纔是數據庫。通過一層一層的訪問緩存逐步的削減到達數據庫的請求數量,這樣才能保證網站在高併發之下扛住壓力,但是僅僅依靠緩存還不夠,所以還需要進行接口優化,接口優化的核心思路:減少數據庫的訪問(因爲數據庫抗併發的能力有限)
- 使用Redis預減庫存減少對數據庫的訪問
- 使用內存標記減少Redis的訪問
- 使用RabbitMQ消息隊列緩衝,異步下單,增強用戶體驗
具體實現步驟:
- 系統初始化時,把商品庫存數量加載到Redis裏面去
- 收到秒殺請求時,Redis預減庫存(先減少Redis裏面的庫存數量,庫存不足,則直接返回),如果庫存已經到達臨界值的時候,即=0,就不需要繼續往下走,直接返回秒殺失敗
- 將請求放入消息隊列,立即返回排隊中
- 將請求從消息隊列取出來,生成訂單,減少庫存
- 客戶端輪詢秒殺的結果,看是否秒殺成功
流程圖如下:
將商品庫存數量預加載庫存到Redis裏面並標記到內存裏
將MiaoshaController實現InitializingBean接口,重寫afterPropertiesSet方法:
收到秒殺請求後的具體業務邏輯
後端收到秒殺請求,實行Redis預減庫存(先減少Redis裏面的庫存數量,庫存不足,直接返回),如果庫存到達臨界值的時候,即庫存=0,就不需要繼續往下走,直接返回秒殺失敗;如果所有的判斷都通過,則將請求放入消息隊列,具體業務邏輯分析如下圖:
秒殺接口代碼如下:
@RequestMapping(value = "/{path}/do_miaosha", method = RequestMethod.POST)
@ResponseBody
public Result<Integer> miaosha(Model model, MiaoshaUser user,
@RequestParam("goodsId") long goodsId,
@PathVariable("path") String path) {
model.addAttribute("user", user);
try {
//內存標記,減少redis訪問,從map中取出
boolean over = localOverMap.get(goodsId);
if (over) {
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//預減庫存:從緩存中減去庫存
//利用redis中的方法,減去庫存,返回值爲減去1之後的值
long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
//這裏判斷不能小於等於,因爲減去之後等於0說明還有是正常範圍
if (stock < 0) {
localOverMap.put(goodsId, true);
//返回秒殺完畢
return Result.error(CodeMsg.MIAO_SHA_OVER);
}
//判斷是否已經秒殺到了,避免一個賬戶秒殺多個商品
MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
if (order != null) {
//還原庫存
redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
return Result.error(CodeMsg.REPEATE_MIAOSHA);
}
} catch (Exception e) {
/**
* 當最後一個商品下單出現錯誤時,數據庫減少庫存失敗,redis減少庫存成功
* 這時就會出現庫存售不完的情況,所以要將redis緩存還原,即redis庫存數加1
*/
redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
return Result.error(CodeMsg.MIAOSHA_FAIL);
}
//將請求入隊
MiaoshaMessage mm = new MiaoshaMessage();
mm.setUser(user);
mm.setGoodsId(goodsId);
sender.sendMiaoshaMessage(mm);
return Result.success(0);//返回排隊中
}
MiaoshaMessage代碼(消息的封裝類):
package com.javaxl.miaosha_05.rabbitmq;
import com.javaxl.miaosha_05.domain.MiaoshaUser;
public class MiaoshaMessage {
private MiaoshaUser user;
private long goodsId;
public MiaoshaUser getUser() {
return user;
}
public void setUser(MiaoshaUser user) {
this.user = user;
}
public long getGoodsId() {
return goodsId;
}
public void setGoodsId(long goodsId) {
this.goodsId = goodsId;
}
}
MQSender代碼(發送消息到rabbitmq去):
package com.javaxl.miaosha_05.rabbitmq;
import com.javaxl.miaosha_05.redis.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MQSender {
private static Logger log = LoggerFactory.getLogger(MQSender.class);
@Autowired
AmqpTemplate amqpTemplate ;
public void sendMiaoshaMessage(MiaoshaMessage mm) {
String msg = RedisService.beanToString(mm);
log.info("send message:"+msg);
amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
}
}
注:消息隊列這裏的消息只能傳字符串,MiaoshaMessage是個Bean對象,先用beanToString方法,將其轉換爲String,放入隊列,再使用AmqpTemplate發送
MQConfig代碼(創建MQ的config類):
package com.javaxl.miaosha_05.rabbitmq;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MQConfig {
public static final String MIAOSHA_QUEUE = "miaosha.queue";
public static final String QUEUE = "queue";
public static final String MIAOSHA_EXCHANGE = "miaosha.exchange";
/**
* Direct模式 交換機Exchange
* */
@Bean
public Queue queue() {
return new Queue(QUEUE, true);
}
}
application.properties中關於rabbitmq的配置:
#rabbitmq
spring.rabbitmq.host=xxx
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